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

First steps #1

Open
wants to merge 15 commits into
base: development
Choose a base branch
from
32 changes: 0 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,32 +0,0 @@
# `@ubiquity/ts-template`

This template repository includes support for the following:

- TypeScript
- Environment Variables
- Conventional Commits
- Automatic deployment to Cloudflare Pages

## Testing

### Cypress

To test with Cypress Studio UI, run

```shell
yarn cy:open
```

Otherwise to simply run the tests through the console, run

```shell
yarn cy:run
```

### Jest

To start Jest tests, run

```shell
yarn test
```
56 changes: 54 additions & 2 deletions build/esbuild-build.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { execSync } from "child_process";
import esbuild from "esbuild";
import { config } from "dotenv";
config()

const typescriptEntries = ["static/main.ts"];
// const cssEntries = ["static/style.css"];
const cssEntries = ["static/style.css"];

const entries = [
...typescriptEntries,
// ...cssEntries
...cssEntries
];

export const esBuildContext: esbuild.BuildOptions = {
Expand All @@ -20,6 +25,11 @@ export const esBuildContext: esbuild.BuildOptions = {
".svg": "dataurl",
},
outdir: "static/dist",
define: createEnvDefines([], {
commitHash: execSync(`git rev-parse --short HEAD`).toString().trim(),
NODE_ENV: process.env.NODE_ENV || "development",
SALT: process.env.SALT || "south-tube-human-wise-fashion-village-south-tube-human-wise-fashion-village"
}),
};

esbuild
Expand All @@ -31,3 +41,45 @@ esbuild
console.error(err);
process.exit(1);
});


function createEnvDefines(environmentVariables: string[], generatedAtBuild: Record<string, unknown>): Record<string, string> {
const defines: Record<string, string> = {};
for (const name of environmentVariables) {
const envVar = process.env[name];
if (envVar !== undefined) {
defines[name] = JSON.stringify(envVar);
} else {
throw new Error(`Missing environment variable: ${name}`);
}
}
for (const key in generatedAtBuild) {
if (Object.prototype.hasOwnProperty.call(generatedAtBuild, key)) {
defines[key] = JSON.stringify(generatedAtBuild[key]);
}
}
return defines;
}

export function generateSupabaseStorageKey(): string | null {
const SUPABASE_URL = process.env.SUPABASE_URL;
if (!SUPABASE_URL) {
console.error("SUPABASE_URL environment variable is not set");
return null;
}

const urlParts = SUPABASE_URL.split(".");
if (urlParts.length === 0) {
console.error("Invalid SUPABASE_URL environment variable");
return null;
}

const domain = urlParts[0];
const lastSlashIndex = domain.lastIndexOf("/");
if (lastSlashIndex === -1) {
console.error("Invalid SUPABASE_URL format");
return null;
}

return domain.substring(lastSlashIndex + 1);
}
13 changes: 11 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,15 @@
"open-source"
],
"dependencies": {
"dotenv": "^16.4.4"
"@keyrxng/webauthn-evm-signer": "^1.0.0",
"@octokit/request-error": "^6.1.1",
"@octokit/rest": "^21.0.0",
"@safe-global/api-kit": "^2.4.2",
"@safe-global/protocol-kit": "^4.0.2",
"@safe-global/safe-core-sdk-types": "^5.0.2",
"@supabase/supabase-js": "^2.43.5",
"dotenv": "^16.4.4",
"ethers": "^6.13.1"
},
"devDependencies": {
"@commitlint/cli": "^18.6.1",
Expand Down Expand Up @@ -77,5 +85,6 @@
"extends": [
"@commitlint/config-conventional"
]
}
},
"packageManager": "[email protected]+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
}
1 change: 0 additions & 1 deletion static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
<link rel="stylesheet" href="style.css" />
</head>
<body>
<h1>Ubiquity TypeScript Template</h1>
<script type="module" src="dist/main.js"></script>
</body>
</html>
17 changes: 10 additions & 7 deletions static/main.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
export async function mainModule() {
console.log(`Hello from mainModule`);
}
import { authentication } from "./src/auth/authentication";
import { renderSafeUI } from "./src/webauthn/rendering";
import { webAuthn } from "./src/webauthn/webauthn";

mainModule()
.then(() => {
console.log("mainModule loaded");
authentication()
.then((ghUser) => {
if (!ghUser) return;
webAuthn(ghUser).then((result) => {
renderSafeUI(result)
});
})
.catch((error) => {
console.error(error);
});
});
20 changes: 20 additions & 0 deletions static/src/auth/authentication.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { getGitHubAccessToken } from "../github/get-access-token";
import { getGitHubUser } from "../github/get-user";
import { GitHubUser } from "../types/github";
import { renderGitHubLoginButton, renderUserInfo } from "./rendering";

export async function authentication() {
const accessToken = await getGitHubAccessToken();
if (!accessToken) {
renderGitHubLoginButton();
}

const gitHubUser: null | GitHubUser = await getGitHubUser();
if (gitHubUser) {
renderUserInfo(gitHubUser);
}

return gitHubUser;
}


58 changes: 58 additions & 0 deletions static/src/auth/rendering.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { getSupabase } from "../supabase/session";
import { GitHubUser } from "../types/github";

export async function renderGitHubLoginButton() {
const existingButton = document.getElementById("auth-btn");
if (existingButton) {
return existingButton;
}
const btn = document.createElement("button");
btn.textContent = "Login with GitHub";
btn.id = "auth-btn";
btn.onclick = async () => {
const supabase = getSupabase();
const { error } = await supabase.auth.signInWithOAuth({
provider: "github",
});
if (error) {
console.error("GitHub login error", error);
}
};

document.body.appendChild(btn);
return btn;
}

export function renderUserInfo(
user: GitHubUser
) {
const newInfo = document.createElement("div");
newInfo.id = "info-container"
newInfo.innerHTML = `
<div id="user-info-container">
<img id="gh-pp" src="${user.avatar_url}" alt="User avatar" style="width: 320px; height: 320px; margin-top: 20px;" />
<h2>Hello, ${user.login}.</h2>
</div>
`;

document.body.appendChild(newInfo);

const userInfoContainer = document.getElementById("user-info-container")
const logoutButton = document.createElement("button");

if (!logoutButton) {
throw new Error("no login btn")
}

logoutButton.onclick = () => {
const supabase = getSupabase();
supabase.auth.signOut().then(() => {
window.location.reload();
})
};
logoutButton.textContent = "Logout";

userInfoContainer?.appendChild(logoutButton)

return newInfo;
}
51 changes: 51 additions & 0 deletions static/src/funding/balance-check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { ethers, JsonRpcProvider, Wallet } from "ethers";

export const provider = new JsonRpcProvider("https://rpc-amoy.polygon.technology");

export async function walletNeedsFunded(signer: Wallet) {
const balance = await provider.getBalance(signer.address);
return balance < ethers.parseEther("0.0009")
}

export async function fundWalletFromFaucet(signer: Wallet) {
const workerUrl = "https://ubq-gas-faucet.keyrxng7749.workers.dev/?address=" + signer.address;
let res: Response | null = null;
const options = {
method: "POST",
headers: {
"Content-Type": "application/json",
},
};
try {
res = await fetch(workerUrl, options);
} catch (e) {
await retryWrapper(async () => {
res = await fetch(workerUrl, options);
});

if (!res || !res.ok) {
throw new Error("Faucet work has likely exceeded limits, try again shortly.")
}
}

if (!res || !res.ok) {
return null;
}

return res.json();
}

async function retryWrapper(fn: () => Promise<any>, retries = 3) {
let res;
let backoff = 7500;
for (let i = 0; i < retries; i++) {
try {
res = await fn();
break;
} catch (e) {
console.error(e);
}
await new Promise((resolve) => setTimeout(resolve, backoff * (i + 1)));
}
return res;
}
29 changes: 29 additions & 0 deletions static/src/github/get-access-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { checkSupabaseSession, SUPABASE_STORAGE_KEY } from "../supabase/session";


export async function getGitHubAccessToken(): Promise<string | null> {
// better to use official function, looking up localstorage has flaws
const oauthToken = await checkSupabaseSession();

if (!oauthToken) {
return null;
} else if (typeof oauthToken === "string") {
// it's the access token
return oauthToken;
}

const expiresAt = oauthToken?.expires_at;
if (expiresAt) {
if (expiresAt < Date.now() / 1000) {
localStorage.removeItem(`sb-${SUPABASE_STORAGE_KEY}-auth-token`);
return null;
}
}

const accessToken = oauthToken?.provider_token;
if (accessToken) {
return accessToken;
}

return null;
}
27 changes: 27 additions & 0 deletions static/src/github/get-url-session-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { OAuthToken } from "../types/auth";
import { getLocalStore } from "../utils/local-storage";
declare const SUPABASE_STORAGE_KEY: string; // @DEV: passed in at build time check build/esbuild-build.ts

export async function getNewSessionToken(): Promise<string | null> {
const hash = window.location.hash;
const params = new URLSearchParams(hash.substring(1)); // remove the '#' and parse
const providerToken = params.get("provider_token");
if (!providerToken) {
const error = params.get("error_description");
// supabase auth provider has failed for some reason
console.error(`GitHub login provider: ${error}`);
}
return providerToken || null;
}

export async function getSessionToken(): Promise<string | null> {
const cachedSessionToken = getLocalStore(`sb-${SUPABASE_STORAGE_KEY}-auth-token`) as OAuthToken | null;
if (cachedSessionToken) {
return cachedSessionToken.provider_token;
}
const newSessionToken = await getNewSessionToken();
if (newSessionToken) {
return newSessionToken;
}
return null;
}
36 changes: 36 additions & 0 deletions static/src/github/get-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Octokit } from "@octokit/rest";
import { getSessionToken } from "../supabase/session";
import { GitHubUser, GitHubUserResponse } from "../types/github";
import { getLocalStore } from "../utils/local-storage";
import { OAuthToken } from "../types/auth";
declare const SUPABASE_STORAGE_KEY: string; // @DEV: passed in at build time check build/esbuild-build.ts

export async function getGitHubUser(): Promise<GitHubUser | null> {
const activeSessionToken = await getSessionToken();
return getNewGitHubUser(activeSessionToken);
}

async function getNewGitHubUser(providerToken: string | null): Promise<GitHubUser | null> {
const octokit = new Octokit({ auth: providerToken });
try {
const response = (await octokit.request("GET /user")) as GitHubUserResponse;
return response.data;
} catch (error) {
if (!!error && typeof error === "object" && "status" in error && error.status === 403) {
console.error("GitHub API error", error);
}
console.warn("You have been logged out. Please login again.", error);
}
return null;
}

export function getGitHubUserName(): string | null {
const oauthToken = getLocalStore(`sb-${SUPABASE_STORAGE_KEY}-auth-token`) as OAuthToken | null;

const username = oauthToken?.user?.user_metadata?.user_name;
if (username) {
return username;
}

return null;
}
Loading
Loading