Skip to content

Commit

Permalink
Add a local test environment with TLS and domain names.
Browse files Browse the repository at this point in the history
  • Loading branch information
lionell-pack-ttd committed Sep 30, 2024
1 parent 8ae649b commit 5097865
Show file tree
Hide file tree
Showing 20 changed files with 3,497 additions and 957 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules
dist
lib
lib
ca
57 changes: 57 additions & 0 deletions createCA.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { createCA, createCert } from 'mkcert';
import { devDomains } from './localtest/siteDetails';
import fs from 'node:fs/promises';

const domains = devDomains;

const caFolder = './ca/';
const caFile = `${caFolder}ca.crt`;
const caKey = `${caFolder}ca.key`;
const certFile = `${caFolder}cert.crt`;
const certKey = `${caFolder}cert.key`;

const overwriteFileOptions = {
flag: 'w',
};
const failIfExistsFileOptions = {
flag: 'wx',
};

const fileExists = async (path) => !!(await fs.stat(path).catch((e) => false));
const getOrCreateCA = async () => {
if (await fileExists(caFile)) {
console.log('Found existing CA, loading...');
return {
cert: await fs.readFile(caFile, { encoding: 'utf8' }),
key: await fs.readFile(caKey, { encoding: 'utf8' }),
};
} else {
console.log('Creating new CA...');
const ca = await createCA({
organization: 'UID2 local dev CA',
countryCode: 'AU',
state: 'NSW',
locality: 'Sydney',
validity: 3650,
});
await fs.mkdir(caFolder, { recursive: true });
await fs.writeFile(caFile, ca.cert, failIfExistsFileOptions);
await fs.writeFile(caKey, ca.key, failIfExistsFileOptions);
return ca;
}
};

async function createCerts() {
const ca = await getOrCreateCA();
const cert = await createCert({
ca: { key: ca.key, cert: ca.cert },
domains,
validity: 3650,
});
await fs.writeFile(certFile, `${cert.cert}${ca.cert}`, overwriteFileOptions);
await fs.writeFile(certKey, cert.key, overwriteFileOptions);

console.log('New certificate saved.');
}

createCerts();
18 changes: 18 additions & 0 deletions localtest/auth/auth.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Homepage</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"
rel="stylesheet"
/>
<script src="https://cdn.integ.uidapi.com/uid2-sdk-3.3.2.js"></script>
<link href="../style.scss" rel="stylesheet" />
<script src="./auth.tsx" defer="defer"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
69 changes: 69 additions & 0 deletions localtest/auth/auth.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { createApp } from '../shared/createApp';
import { Layout } from '../shared/layout';
import { setUid2Identity, useUid2Identity } from '../shared/uid2Identity';
import type { Identity } from '../../src/exports';
import { FormEventHandler, useState } from 'react';
import { getEmailCookie, setEmailCookie } from '../shared/user';
import { initUid2Sdk } from '../shared/uid2Helper';
import { devSiteMap } from '../siteDetails';

initUid2Sdk();
const mainSiteUrl = devSiteMap.www.url;

type LoggedInProps = Readonly<{ identity: Identity; email?: string }>;
function LoggedIn({ identity, email }: LoggedInProps) {
return (
<div>
<div>Advertising token: {identity.advertising_token}</div>
<div>
Email: {!!email && email}
{!email && '<Not logged in>'}
</div>
<div>
<a href={mainSiteUrl}>Back to the main site</a>
</div>
</div>
);
}

type LoginFormProps = Readonly<{
setEmail: (email: string) => void;
}>;
function LoginForm({ setEmail }: LoginFormProps) {
const handleSubmit: FormEventHandler<HTMLFormElement> = async (e) => {
e.preventDefault();
const email = (e.currentTarget.elements as any).email.value;
if (email) {
console.log(`Sending CSTG request for ${email}...`);
await setUid2Identity(email);
console.log(`CSTG request for ${email} complete`);
setEmailCookie(email);
setEmail(email);
}
};
return (
<form onSubmit={handleSubmit}>
<input type='text' id='email' />
<button type='submit'>Log in</button>
</form>
);
}

function Auth() {
const [email, setEmail] = useState(getEmailCookie());
const identity = useUid2Identity();
return (
<div>
{!!identity && <LoggedIn identity={identity} email={email} />}
{!identity && <LoginForm setEmail={setEmail} />}
</div>
);
}

createApp(
<Layout siteName='auth'>
<div className='content'>
<Auth />
</div>
</Layout>
);
45 changes: 45 additions & 0 deletions localtest/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Webpack from 'webpack';
import WebpackDevServer from 'webpack-dev-server';
import webpackConfig from './webpack.config.js';
import { devSites } from './siteDetails.js';

// @ts-ignore
const compiler = Webpack(webpackConfig);
const hosts = Object.fromEntries(
devSites.map((s) => [s.domain, { ...s, index: `/${s.name}.html` }])
);
const hostnames = Object.keys(hosts);
const devServerOptions = {
...webpackConfig.devServer,
historyApiFallback: {
verbose: true,
rewrites: [
{
from: /^[^.]+$/,
to: (context: any) => {
const hostname = context.request.header('Host');
return hosts[hostname].index;
},
},
],
},
open: hostnames.map((host) => `https://${host}/`),
port: 443,
allowedHosts: hostnames,
server: {
type: 'https',
options: {
ca: '../ca/ca.crt',
cert: '../ca/cert.crt',
key: '../ca/cert.key',
},
},
};
const server = new WebpackDevServer(devServerOptions, compiler);

const runServer = async () => {
console.log('Starting server...');
await server.start();
};

runServer();
18 changes: 18 additions & 0 deletions localtest/shared/createApp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { createRoot } from 'react-dom/client';

function domReady(callBack: () => void) {
document.addEventListener('DOMContentLoaded', callBack);
if (document.readyState === 'interactive' || document.readyState === 'complete') {
callBack();
}
}

export function createApp(app: React.ReactNode) {
let appCreated = false;
domReady(() => {
if (appCreated) return;
appCreated = true;
const root = createRoot(document.getElementById('app')!);
root.render(app);
});
}
14 changes: 14 additions & 0 deletions localtest/shared/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
type LayoutProps = Readonly<
React.PropsWithChildren & { siteName: string; extraHeader?: React.ReactNode }
>;
export function Layout({ children, siteName, extraHeader }: LayoutProps) {
return (
<>
<div className='header'>
<h1>UID2 local dev setup: {siteName}</h1>
</div>
{extraHeader}
<div className='content'>{children}</div>
</>
);
}
7 changes: 7 additions & 0 deletions localtest/shared/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { UID2, SDKSetup, CallbackHandler } from '../../src/uid2Sdk';

declare global {
interface Window {
__uid2: UID2 | SDKSetup | undefined;
}
}
20 changes: 20 additions & 0 deletions localtest/shared/uid2Helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { UID2 } from '../../src/uid2Sdk';
import { topLevelDomain } from '../siteDetails';

export function isUid2Sdk(sdk: any): sdk is UID2 {
if (typeof sdk?.init === 'function') return true;
return false;
}

export function initUid2Sdk() {
window.__uid2 = window.__uid2 ?? { callbacks: [] };
window.__uid2.callbacks?.push((event) => {
if (event === 'SdkLoaded') {
(window.__uid2 as UID2).init({
baseUrl: 'https://operator-integ.uidapi.com',
cookieDomain: topLevelDomain,
useCookie: true,
});
}
});
}
34 changes: 34 additions & 0 deletions localtest/shared/uid2Identity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useEffect, useState } from 'react';
import type { Identity, CallbackHandler } from '../../src/exports';
import { UID2 } from '../../src/uid2Sdk';
import { isUid2Sdk } from './uid2Helper';

export function useUid2Identity() {
const [user, setUser] = useState<Identity | null>(null);
const uid2Handler: CallbackHandler = (_, payload) => {
if (payload.identity) {
setUser(payload.identity);
} else {
setUser(null);
}
};
useEffect(() => {
window.__uid2 = window.__uid2 ?? { callbacks: [] };
window.__uid2.callbacks!.push(uid2Handler);
return () => {
const handlerIndex = window.__uid2?.callbacks?.indexOf(uid2Handler);
if (!handlerIndex) return;
window.__uid2?.callbacks?.splice(handlerIndex, 1);
};
}, []);
return user;
}

export function setUid2Identity(email: string) {
if (!isUid2Sdk(window.__uid2)) throw Error('SDK not available');
return window.__uid2.setIdentityFromEmail(email, {
serverPublicKey:
'UID2-X-I-MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENLGSNRNncEb0D2FAaws7ZuymOBYAc7eKN53mady4sBiWCFRyRIB4sgHvBm1TsC8OLbLK41vHqRutoOaNp44YBA==',
subscriptionId: 'T3NaRGYBaG',
});
}
20 changes: 20 additions & 0 deletions localtest/shared/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { topLevelDomain } from '../siteDetails';

const emailStorageKey = 'loggedInUserEmail';

export function setEmailCookie(email: string) {
const cookie = `${emailStorageKey}=${encodeURIComponent(
email
)};domain=${topLevelDomain};max-age=86400;`;
document.cookie = cookie;
}

export function getEmailCookie() {
const docCookie = document.cookie;
if (docCookie) {
const payload = docCookie.split('; ').find((row) => row.startsWith(emailStorageKey + '='));
if (payload) {
return decodeURIComponent(payload.split('=')[1]);
}
}
}
18 changes: 18 additions & 0 deletions localtest/siteDetails.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const sites = [
{
name: 'www',
domain: 'www.uid2-local-dev-setup.com',
},
{
name: 'auth',
domain: 'auth.uid2-local-dev-setup.com',
},
];

export const devSites = sites.map((s) => ({ ...s, url: `https://${s.domain}/` }));

export const devDomains = Object.values(devSites).map((s) => s.domain);

export const devSiteMap = Object.fromEntries(devSites.map((s) => [s.name, s]));

export const topLevelDomain = 'uid2-local-dev-setup.com';
27 changes: 27 additions & 0 deletions localtest/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
body {
font-family: 'Roboto', sans-serif;
font-weight: 400;
font-style: normal;

line-height: 1.88889;
color: #0a1300;
background: #bacdd8;
margin: 0;
padding: 0;
width: 100%;

.app {
width: 100%;
}

.header {
width: 100%;
display: flex;
justify-content: center;
padding: 0;
}

.content {
padding: 20px;
}
}
23 changes: 23 additions & 0 deletions localtest/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Node 16",

"compilerOptions": {
"lib": ["es2021", "dom"],
"module": "commonjs",
"target": "es6",
"allowJs": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"outDir": "lib",
"sourceMap": true,
"typeRoots": ["../node_modules/@types"],
"jsx": "react-jsx"
},
"include": ["."],
"exclude": ["../node_modules", "dist"]
}
Loading

0 comments on commit 5097865

Please sign in to comment.