diff --git a/README.md b/README.md index 7d455b3..6e8dc81 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,6 @@ npm run deploy ## TODO - [ ] Write `README.md` -- [ ] Change the document title dynamically (with nested `Suspense`) - [ ] Improve GUI - [x] add additional info about the repo - [x] implement dark/light themes @@ -26,16 +25,38 @@ npm run deploy - [x] add reload button - [x] fix header hidden on small screens - [ ] add colors for languages + - [ ] refactor code wit [shadcn](https://ui.shadcn.com/) UI + - [ ] fix cookie storage/deletion (and interferences with GUI) + - [ ] Change the document title dynamically (with nested `Suspense`) + - [ ] use nested renderer to render multiple components + - [ ] use `useRequestContext` to have conditional render + - [ ] implement Swagger with API + - [ ] add "Browse API" in addition to "Browse GitHub repositories" - [ ] Define unit tests - [ ] Fix interfaces and type definitions - [ ] Setup GitHub Actions - [ ] to deploy the website to GitHub - [x] to run tests automatically + - [ ] fix GitHub security detection on `tests` files - [ ] Make sure the project is compatible with multiple deployment types - [x] Cloudflare - [ ] GitHub - [ ] Heroku -- [ ] Setup GitHub App (to avoid refreshing PAT every year) +- [ ] Setup GitHub App + - [x] fix oauth workflow + - [ ] define default demo/GitHub choice page + - [ ] error handling when `access_token` is not defined (go back to authentication or use default token) + - [ ] display error messages in a page + - [ ] validate the token used before using them + - [ ] fix `access_token`/`refresh_token` cookie storage issue + - [ ] optimize performance + - [ ] change GitHub App icon + - [x] handle `state` verification + - [ ] handle `access_token` refresh with `refresh_token` +- [ ] Use middlewares + - [ ] to declare the octokit instance + - [x] to add auth header if access_token is valid (to allow API use when `access_token` is defined) +- [x] fix API by adding 404 not found if no repo by id is found - [x] Add icons in footer - [x] Find `MAX_ID` dynamically and store in Cookies - [x] Require GitHub API (Bearer Auth) for `/api/*` requests diff --git a/package-lock.json b/package-lock.json index b10c0ef..4102bb3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "petithub", - "version": "0.6.0", + "version": "0.6.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "petithub", - "version": "0.6.0", + "version": "0.6.1", "license": "MIT", "dependencies": { - "hono": "^4.4.9", + "hono": "^4.4.10", "octokit": "^4.0.2" }, "devDependencies": { @@ -1757,9 +1757,9 @@ } }, "node_modules/hono": { - "version": "4.4.9", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.4.9.tgz", - "integrity": "sha512-VW1hnYipHL/XsnSYiCTLJ+Z7iisZYWwSOiKXm9RBV2NKPxNqjfaHqeMFiDl11fK893ofmErvRpX20+FTNjZIjA==", + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.4.10.tgz", + "integrity": "sha512-z6918u9rXRU5CCisMHd2uUVoQXcNyUrUMmYY7VH10v4HJG7+hqgMK/G8YNTd13C6s4rBfzF09iz8VpOip9qG3A==", "engines": { "node": ">=16.0.0" } @@ -4413,9 +4413,9 @@ } }, "hono": { - "version": "4.4.9", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.4.9.tgz", - "integrity": "sha512-VW1hnYipHL/XsnSYiCTLJ+Z7iisZYWwSOiKXm9RBV2NKPxNqjfaHqeMFiDl11fK893ofmErvRpX20+FTNjZIjA==" + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.4.10.tgz", + "integrity": "sha512-z6918u9rXRU5CCisMHd2uUVoQXcNyUrUMmYY7VH10v4HJG7+hqgMK/G8YNTd13C6s4rBfzF09iz8VpOip9qG3A==" }, "human-signals": { "version": "5.0.0", diff --git a/package.json b/package.json index c44c0a0..cca5e75 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "petithub", - "version": "0.6.0", + "version": "0.6.1", "private": false, "description": "PetitHub - Explore obscure GitHub repositories", "author": { @@ -30,7 +30,7 @@ "deploy": "$npm_execpath run build && wrangler pages deploy" }, "dependencies": { - "hono": "^4.4.9", + "hono": "^4.4.10", "octokit": "^4.0.2" }, "devDependencies": { diff --git a/public/static/script.js b/public/static/script.js index f621df9..cbd7e00 100644 --- a/public/static/script.js +++ b/public/static/script.js @@ -2,12 +2,7 @@ function getCookie(name) { const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`)); return match ? match[1] : undefined; } - -const maxIdCookie = getCookie("max_id"); +const maxIdCookie = getCookie(`__Secure-max_id`); if (!maxIdCookie) { fetch("/id"); } - -const refresh = () => { - window.location.reload(); -}; diff --git a/src/index.tsx b/src/index.tsx index 647d096..fba2b70 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,127 +1,94 @@ import { Context, Hono } from "hono"; import { Suspense } from "hono/jsx"; -import { prettyJSON } from "hono/pretty-json"; -import { bearerAuth } from "hono/bearer-auth"; -import { cors } from "hono/cors"; +import { logger } from "hono/logger"; -import { - renderer, - Loader, - Container, - RepositoryContainer, -} from "./utils/renderer"; -import { - getOctokitInstance, - verifyToken, - getRepos, - getRepository, - getRandomRepository, - getMaxId, -} from "./utils/octokit"; -import { getCookieId, setCookieId } from "./utils/cookie"; +import { renderer, Loader, RepositoryContainer, Login } from "./utils/renderer"; +import { getOctokitInstance } from "./utils/octokit"; +import { handleMaxId, handleTokens } from "./utils/cookie"; +import { generateState, handleState } from "./utils/state"; +import api from "./routes/api"; +import id from "./routes/id"; +import template from "./routes/template"; +import petithub from "./routes/petithub"; +import github from "./routes/github"; +import welcome from "./routes/welcome"; + +/* TYPES */ export type Bindings = { GITHUB_TOKEN: string; + CLIENT_ID: string; + CLIENT_SECRET: string; }; -/* APP */ -const app = new Hono<{ Bindings: Bindings }>(); +export type Variables = { + max_id: number; + access_token?: string; + refresh_token?: string; + state?: string; +}; -/* GLOBAL VARIABLES */ -const MAX_ID = 815471592; // TBU +/* APP */ +const app = new Hono<{ Bindings: Bindings; Variables: Variables }>(); /* MIDDLEWARES */ +app.use(logger()); app.use(renderer); -app.use(prettyJSON()); -app.use("/api/*", cors({ origin: "*", allowMethods: ["GET"] })); -app.use("/api/*", bearerAuth({ verifyToken })); - -/* ROUTES */ -app.get( - "/api", - async (c: Context<{ Bindings: Bindings }>): Promise => { - const octokit = getOctokitInstance(c); - try { - const repository = await getRandomRepository(octokit, Number(MAX_ID)); - return c.json(repository); - } catch (error: any) { - console.error("Error fetching repository data:", error); - return c.json({ error: "Failed to fetch repository data" }, 500); +app.use(handleMaxId()); +app.use(handleTokens()); +app.use("/", async (c, next) => { + const { access_token, refresh_token } = c.var; + console.log(access_token, refresh_token); + if (!access_token) { + // TODO allow some use without access_token and only redirect when out of free API usage + if (!refresh_token) { + // return c.redirect("/login", 302); + } else { + return c.redirect( + `/github/access_token?refresh_token=${refresh_token}&callback_url=/` + ); } } -); - -app.get( - "/api/:id", - async (c: Context<{ Bindings: Bindings }>): Promise => { - const { id } = c.req.param(); - const octokit = getOctokitInstance(c); - try { - const repository = await getRepository(octokit, Number(id)); - return c.json(repository); - } catch (error: any) { - console.error("Error fetching repository data:", error); - return c.json({ error: "Failed to fetch repository data" }, 500); - } - } -); + await next(); +}); // TODO handle token verification +app.use("/github/login", generateState()); +app.use("/github/callback", handleState()); -app.get( - "/id", - async (c: Context<{ Bindings: Bindings }>): Promise => { - const octokit = getOctokitInstance(c); - const cookieId = getCookieId(c); - const id = await getMaxId(octokit, Number(cookieId || MAX_ID)); - setCookieId(c, id); - const timestamp = new Date(); - return c.json({ id, timestamp }); - } -); +/* ROUTES */ +app.route("/api", api); +app.route("/id", id); +app.route("/template", template); +app.route("/petithub", petithub); +app.route("/github", github); +app.route("/welcome", welcome); app.get( - "/template", - async (c: Context<{ Bindings: Bindings }>): Promise => { - const octokit = getOctokitInstance(c); - const { data: repository } = await getRepos( - octokit, - "octocat", - "Hello-World" - ); - return c.render( - }> - - , - { title: "PetitHub - octocat/Hello-world" } - ); + "/login", + (c: Context<{ Bindings: Bindings; Variables: Variables }>): Response => { + return c.render(, { + title: "PetitHub - login", + }); } -); +); // TODO suppress login route and add specific button to login +/* ROOT */ app.get( - "/petithub", - async (c: Context<{ Bindings: Bindings }>): Promise => { + "/", + async ( + c: Context<{ Bindings: Bindings; Variables: Variables }> + ): Promise => { const octokit = getOctokitInstance(c); - const { data: repository } = await getRepos(octokit, "cletqui", "petithub"); + const { max_id } = c.var; return c.render( }> - + , - { title: "PetitHub - cletqui/petithub" } + { title: "PetitHub" } // TODO change this title dynamically ); } ); -app.get("/", async (c: Context<{ Bindings: Bindings }>): Promise => { - const octokit = getOctokitInstance(c); - const cookieId = getCookieId(c); - const maxId = cookieId || MAX_ID; - return c.render( - }> - - , - { title: "PetitHub" } // TODO change this title dynamically - ); -}); - +/* DEFAULT */ app.get("*", (c) => { return c.redirect("/", 301); }); diff --git a/src/routes/api.tsx b/src/routes/api.tsx new file mode 100644 index 0000000..ee32c17 --- /dev/null +++ b/src/routes/api.tsx @@ -0,0 +1,66 @@ +import { Context, Hono } from "hono"; +import { poweredBy } from "hono/powered-by"; +import { prettyJSON } from "hono/pretty-json"; +import { cors } from "hono/cors"; + +import { Bindings, Variables } from ".."; +import { + apiAuth, + getOctokitInstance, + getRandomRepository, + getRepository, +} from "../utils/octokit"; + +const app = new Hono<{ Bindings: Bindings; Variables: Variables }>(); + +/* MIDDLEWARES */ +app.use(apiAuth()); +app.use(poweredBy()); +app.use(prettyJSON()); +app.use(cors({ origin: "*", allowMethods: ["GET"], credentials: true })); + +/* ENDPOINTS */ +app.get( + "", + async ( + c: Context<{ Bindings: Bindings; Variables: Variables }> + ): Promise => { + const octokit = getOctokitInstance(c); + const { max_id } = c.var; + try { + const repository = await getRandomRepository(octokit, max_id); + return c.json(repository); + } catch (error: any) { + return c.json({ error: "Failed to fetch repository data" }, 500); + } + } +); // TODO fix request to "/api" that redirects to "/" + +app.get( + "/:id", + async ( + c: Context<{ Bindings: Bindings; Variables: Variables }> + ): Promise => { + const id = c.req.param("id"); + const octokit = getOctokitInstance(c); + try { + const repository = await getRepository(octokit, Number(id)); + const { id: repositoryId } = repository; + if (Number(id) === repositoryId) { + return c.json(repository); + } else { + return c.json( + { + message: `Repository id:${id} not found.`, + nextId: repositoryId, + }, + 404 + ); + } + } catch (error: any) { + return c.json({ error: "Failed to fetch repository data" }, 500); + } + } +); + +export default app; diff --git a/src/routes/github.tsx b/src/routes/github.tsx new file mode 100644 index 0000000..039f3bb --- /dev/null +++ b/src/routes/github.tsx @@ -0,0 +1,107 @@ +import { Context, Hono } from "hono"; + +import { Bindings, Variables } from ".."; +import { setCookieToken } from "../utils/cookie"; + +const app = new Hono<{ Bindings: Bindings; Variables: Variables }>(); + +app.get( + "/login", + async ( + c: Context<{ Bindings: Bindings; Variables: Variables }> + ): Promise => { + const { CLIENT_ID } = c.env; + const { state } = c.var; + const redirect_url = new URL(c.req.url); + redirect_url.pathname = "/github/callback"; + const redirect_uri = redirect_url.toString(); + return c.redirect( + `https://github.com/login/oauth/authorize?client_id=${CLIENT_ID}&redirect_uri=${redirect_uri}&state=${state}`, + 302 + ); + } +); + +app.get( + "/callback", + async (c: Context<{ Bindings: Bindings; Variables: Variables }>) => { + const { CLIENT_ID, CLIENT_SECRET } = c.env; + const { state: secret } = c.var; + const code = c.req.query("code"); + const state = c.req.query("state"); + if (state === secret) { + const response = await fetch( + `https://github.com/login/oauth/access_token?client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&code=${code}`, + { method: "POST" } + ); + const tokens = await response.text(); + const responseParams = new URLSearchParams(tokens); + const error = responseParams.get("error"); + const refreshToken = responseParams.get("refresh_token"); + const refreshTokenExpiresIn = responseParams.get( + "refresh_token_expires_in" + ); + if (refreshToken && refreshTokenExpiresIn) { + setCookieToken( + c, + "refresh_token", + refreshToken, + Number(refreshTokenExpiresIn) + ); + } else if (error) { + const errorDescription = responseParams.get("error_description"); + console.error(error, errorDescription); + } + return c.redirect( + `/github/access_token?${tokens}&callback_url=/welcome`, + 302 + ); + } else { + console.error( + `State mismatched: expected ${secret} and received ${state}` + ); + return c.text(`State mismatched`, 500); + } + } +); // TODO fix oauth workflow (send tokens in a new URL as query params) + +app.get( + "/access_token", + async (c: Context<{ Bindings: Bindings; Variables: Variables }>) => { + const { CLIENT_ID, CLIENT_SECRET } = c.env; + const callbackUrl = c.req.query("callback_url"); + const refreshToken = c.req.query("refresh_token"); + const accessToken = c.req.query("access_token"); + const expiresIn = c.req.query("expires_in"); + if (accessToken) { + setCookieToken(c, "access_token", accessToken, Number(expiresIn)); + } else if (refreshToken) { + const response = await fetch( + `https://github.com/login/oauth/access_token?client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&refresh_token=${refreshToken}&grant_type=refresh_token`, + { method: "POST" } + ); + const tokens = await response.text(); + const responseParams = new URLSearchParams(tokens); + const error = responseParams.get("error"); + const refreshedAccessToken = responseParams.get("access_token"); + const refreshedExpiresIn = responseParams.get("expires_in"); + if (refreshedAccessToken) { + setCookieToken( + c, + "access_token", + refreshedAccessToken, + Number(refreshedExpiresIn) + ); + } else if (error) { + const errorDescription = responseParams.get("error_description"); + console.error(error, errorDescription); + } + return c.redirect("/github/login", 302); + } else { + return c.redirect("/login", 302); + } + return c.redirect(callbackUrl ? callbackUrl : "/welcome", 302); // TODO redirect with access_token to display account on /welcome + } +); + +export default app; diff --git a/src/routes/id.tsx b/src/routes/id.tsx new file mode 100644 index 0000000..500bb55 --- /dev/null +++ b/src/routes/id.tsx @@ -0,0 +1,30 @@ +import { Context, Hono } from "hono"; + +import { Bindings, Variables } from ".."; +import { getMaxId, getOctokitInstance } from "../utils/octokit"; +import { setCookie } from "hono/cookie"; + +const app = new Hono<{ Bindings: Bindings; Variables: Variables }>(); + +app.get( + "/", + async ( + c: Context<{ Bindings: Bindings; Variables: Variables }> + ): Promise => { + const { max_id, access_token } = c.var; + const octokit = getOctokitInstance(c); + const now = new Date().getTime(); + const id = access_token ? await getMaxId(octokit, max_id) : max_id; + setCookie(c, "max_id", `${id}`, { + path: "/", + secure: true, + httpOnly: false /* true */, + maxAge: access_token ? 86400 : 600, + sameSite: "Strict", + prefix: "secure", + }); + return c.json({ id, timestamp: now }); + } +); + +export default app; diff --git a/src/routes/petithub.tsx b/src/routes/petithub.tsx new file mode 100644 index 0000000..327c84f --- /dev/null +++ b/src/routes/petithub.tsx @@ -0,0 +1,28 @@ +import { Context, Hono } from "hono"; +import { Suspense } from "hono/jsx"; + +import { Bindings, Variables } from ".."; +import { Loader, Container } from "../utils/renderer"; +import { getOctokitInstance, getRepos } from "../utils/octokit"; + +const app = new Hono<{ Bindings: Bindings; Variables: Variables }>(); + +app.get( + "/", + async ( + c: Context<{ Bindings: Bindings; Variables: Variables }> + ): Promise => { + const octokit = getOctokitInstance(c); + const owner = "cletqui"; + const repo = "petithub"; + const { data: repository } = await getRepos(octokit, owner, repo); + return c.render( + }> + + , + { title: `PetitHub - ${owner}/${repo}` } + ); + } +); + +export default app; diff --git a/src/routes/template.tsx b/src/routes/template.tsx new file mode 100644 index 0000000..afc73f2 --- /dev/null +++ b/src/routes/template.tsx @@ -0,0 +1,28 @@ +import { Context, Hono } from "hono"; +import { Suspense } from "hono/jsx"; + +import { Bindings, Variables } from ".."; +import { Loader, Container } from "../utils/renderer"; +import { getOctokitInstance, getRepos } from "../utils/octokit"; + +const app = new Hono<{ Bindings: Bindings; Variables: Variables }>(); + +app.get( + "/", + async ( + c: Context<{ Bindings: Bindings; Variables: Variables }> + ): Promise => { + const octokit = getOctokitInstance(c); + const owner = "octocat"; + const repo = "Hello-World"; + const { data: repository } = await getRepos(octokit, owner, repo); + return c.render( + }> + + , + { title: `PetitHub - ${owner}/${repo}` } + ); + } +); + +export default app; diff --git a/src/routes/welcome.tsx b/src/routes/welcome.tsx new file mode 100644 index 0000000..25955da --- /dev/null +++ b/src/routes/welcome.tsx @@ -0,0 +1,18 @@ +import { Context, Hono } from "hono"; + +import { Bindings, Variables } from ".."; +import { Welcome } from "../utils/renderer"; + +const app = new Hono<{ Bindings: Bindings; Variables: Variables }>(); + +app.get( + "/welcome", + (c: Context<{ Bindings: Bindings; Variables: Variables }>): Response => { + // deleteCookie(c, "state", stateCookieOptions); // TODO fix cookie storage/deletion + return c.render(, { + title: "PetitHub - welcome", + }); + } +); + +export default app; diff --git a/src/utils/cookie.tsx b/src/utils/cookie.tsx index 91b5852..293a0e2 100644 --- a/src/utils/cookie.tsx +++ b/src/utils/cookie.tsx @@ -1,16 +1,55 @@ -import { Context } from "hono"; +import { Context, MiddlewareHandler, Next } from "hono"; import { setCookie, getCookie } from "hono/cookie"; +import { CookieOptions } from "hono/utils/cookie"; -export const setCookieId = (c: Context, id: number): void => { - setCookie(c, "max_id", String(id), { +import { Bindings, Variables } from ".."; + +export const handleMaxId = (): MiddlewareHandler => { + return async function handleMaxId( + c: Context<{ Bindings: Bindings; Variables: Variables }>, + next: Next + ) { + const max_id = getCookie(c, "max_id", "secure"); + const MAX_ID = 822080279; // TODO TBU + c.set("max_id", Number(max_id) || MAX_ID); + await next(); + }; +}; + +export const handleTokens = (): MiddlewareHandler => { + return async function handleTokens( + c: Context<{ Bindings: Bindings; Variables: Variables }>, + next: Next + ) { + const accessToken = getCookie(c, "access_token", "secure"); + const refreshToken = getCookie(c, "refresh_token", "secure"); + c.set("access_token", accessToken); + c.set("refresh_token", refreshToken); + await next(); + }; +}; + +export const setCookieToken = ( + c: Context, + name: string, + value: string, + expires: number +): void => { + setCookie(c, name, value, { path: "/", secure: true, - httpOnly: false, - maxAge: 86400, + httpOnly: true, + maxAge: expires, sameSite: "Strict", + prefix: "secure", }); }; -export const getCookieId = (c: Context): number => { - return Number(getCookie(c, "max_id")); +export const stateCookieOptions: CookieOptions = { + path: "/github", + secure: true, + httpOnly: true, + maxAge: 600, + sameSite: "none", + prefix: "secure", }; diff --git a/src/utils/octokit.tsx b/src/utils/octokit.tsx index de23ed9..b6ace82 100644 --- a/src/utils/octokit.tsx +++ b/src/utils/octokit.tsx @@ -1,16 +1,35 @@ -import { Context } from "hono"; +import { Context, MiddlewareHandler, Next } from "hono"; import { Octokit } from "@octokit/core"; import { OctokitResponse } from "@octokit/types"; import { Repository } from "./renderer"; -import { Bindings } from ".."; +import { Bindings, Variables } from ".."; + +export const apiAuth = (): MiddlewareHandler => { + /* inspired from hono/bearer-auth */ + return async function apiAuth( + c: Context<{ Bindings: Bindings; Variables: Variables }>, + next: Next + ) { + const { access_token } = c.var; + const accessToken = access_token || c.req.header("Authorization"); + if (accessToken) { + if (await verifyToken(accessToken, c)) { + await next(); + } + } + return c.text("Unauthorized", 401); + }; +}; export const getOctokitInstance = ( - c: Context<{ Bindings: Bindings }> + c: Context<{ Bindings: Bindings; Variables: Variables }>, + token?: string ): Octokit => { - const { GITHUB_TOKEN } = c.env; + const { access_token } = c.var; + const accessToken = token || access_token; return new Octokit({ - auth: GITHUB_TOKEN, + auth: accessToken, headers: { accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", @@ -20,10 +39,9 @@ export const getOctokitInstance = ( export const verifyToken = async ( token: string, - c: Context<{ Bindings: Bindings }> -) => { - c.env.GITHUB_TOKEN = token; - const octokit = getOctokitInstance(c); + c: Context<{ Bindings: Bindings; Variables: Variables }> +): Promise => { + const octokit = getOctokitInstance(c, token); try { const { status } = await getRepos(octokit, "octocat", "Hello-World"); return status === 200; @@ -39,7 +57,7 @@ const getRepositories = async ( try { return await octokit.request("GET /repositories", { since }); } catch (error: any) { - throw new Error(`Error fetching repositories since ${since}: ${error}`); + throw error; } }; @@ -51,7 +69,7 @@ export const getRepos = async ( try { return await octokit.request("GET /repos/{owner}/{repo}", { owner, repo }); } catch (error: any) { - throw new Error(`Error fetching repository for ${owner}/${repo}: ${error}`); + throw error; } }; @@ -80,7 +98,7 @@ export const getRepository = async ( throw new Error(`${status} error at ${url}`); } } catch (error: any) { - throw new Error(`Error fetching repository for id=${id}: ${error}`); + throw error; } }; @@ -117,7 +135,7 @@ export const getRandomRepository = async ( } throw new Error(`No repository found with ${maxIterations} iterations`); } catch (error: any) { - throw new Error(`Error fetching data: ${error}`); + throw error; } }; diff --git a/src/utils/renderer.tsx b/src/utils/renderer.tsx index b50f76f..eb0a654 100644 --- a/src/utils/renderer.tsx +++ b/src/utils/renderer.tsx @@ -7,6 +7,15 @@ import { Octokit } from "@octokit/core"; import { getRandomRepository } from "./octokit"; import { timeAgo } from "./time"; +interface OctokitErrorResponse { + status: number; + response: { + data: { + message: string; + }; + }; +} + export interface Repository { id: number; name: string; @@ -44,21 +53,58 @@ export const RepositoryContainer = async ({ octokit: Octokit; maxId: number; }): Promise => { - const repository = await getRandomRepository(octokit, maxId); - /* const { full_name } = repository; - const title = `PetitHub - ${full_name}`; */ - return ; + try { + const repository = await getRandomRepository(octokit, maxId); + /* const { full_name } = repository; + const title = `PetitHub - ${full_name}`; */ + return ; + } catch (error: any) { + const { + status, + response: { + data: { message }, + }, + } = (error as OctokitErrorResponse) || {}; + if (status && status === 403) { + return ; + } else { + return ; + } + } }; export const Loader = (): JSX.Element => { return (
-
-
+
+
); }; +export const Login = ({ message }: { message: string }): JSX.Element => { + return ( +
+
{"Login"}
+

{message}

+ +
+ ); +}; // TODO implement Demo/Github login + +export const Welcome = ({}): JSX.Element => { + return ( + + ); +}; // TODO implement Demo/Github login + export const Container = ({ repository, }: { @@ -85,7 +131,6 @@ export const Container = ({ default_branch, subscribers_count, } = repository; - console.log(repository); const dateOptions: Intl.DateTimeFormatOptions = { weekday: "short", year: "numeric", @@ -399,12 +444,7 @@ export const renderer = jsxRenderer(
- + GitHub

{"PetitHub"}

- +
+ + +
{children}