Skip to content

Commit

Permalink
Merge pull request #24 from devpanther/private-tasks-list
Browse files Browse the repository at this point in the history
Feat: Private tasks list
  • Loading branch information
0x4007 committed Mar 8, 2024
2 parents 7f35ce4 + 0858a7f commit 04e1f25
Show file tree
Hide file tree
Showing 14 changed files with 194 additions and 145 deletions.
45 changes: 39 additions & 6 deletions build/esbuild-build.ts
@@ -1,13 +1,15 @@
import * as dotenv from "dotenv";
import { config } from "dotenv";
import esbuild from "esbuild";
import { invertColors } from "./plugins/invert-colors";
import { pwaManifest } from "./plugins/pwa-manifest";
import { execSync } from "child_process";
config();

const typescriptEntries = ["src/home/home.ts", "src/progressive-web-app.ts"];
const cssEntries = ["static/style/style.css"];
const entries = [...typescriptEntries, ...cssEntries, "static/manifest.json", "static/favicon.svg", "static/icon-512x512.png"];

export const esBuildContext: esbuild.BuildOptions = {
define: createEnvDefines(["SUPABASE_URL", "SUPABASE_ANON_KEY"]),
plugins: [invertColors, pwaManifest],
sourcemap: true,
entryPoints: entries,
Expand All @@ -23,23 +25,54 @@ export const esBuildContext: esbuild.BuildOptions = {
".json": "file",
},
outdir: "static/dist",
define: createEnvDefines(["SUPABASE_URL", "SUPABASE_ANON_KEY"], {
SUPABASE_STORAGE_KEY: generateSupabaseStorageKey(),
commitHash: execSync(`git rev-parse --short HEAD`).toString().trim(),
}),
};

esbuild
.build(esBuildContext)
.then(() => console.log("\tesbuild complete"))
.catch(console.error);

function createEnvDefines(variableNames: string[]): Record<string, string> {
function createEnvDefines(environmentVariables: string[], generatedAtBuild: Record<string, unknown>): Record<string, string> {
const defines: Record<string, string> = {};
dotenv.config();
for (const name of variableNames) {
for (const name of environmentVariables) {
const envVar = process.env[name];
if (envVar !== undefined) {
defines[`process.env.${name}`] = JSON.stringify(envVar);
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);
}
3 changes: 1 addition & 2 deletions cypress/e2e/devpool.cy.ts
Expand Up @@ -50,7 +50,6 @@ describe("DevPool", () => {
}).as("getIssues");
cy.visit("/");
cy.get('div[id="issues-container"]').children().should("have.length", 1);
cy.get("#issues-container > :nth-child(1)").should("have.class", "new-task");

// needed to make sure data is written to the local storage
cy.wait(3000);
Expand All @@ -65,7 +64,6 @@ describe("DevPool", () => {
}).as("getIssues");
cy.visit("/");
cy.get('div[id="issues-container"]').children().should("have.length", 1);
cy.get("#issues-container > :nth-child(1)").should("not.have.class", "new-task");

// needed to make sure data is written to the local storage
cy.wait(3000);
Expand Down Expand Up @@ -139,6 +137,7 @@ describe("DevPool", () => {
statusCode: 200,
});
// Simulate login token
// cSpell: ignore wfzpewmlyiozupulbuur
window.localStorage.setItem("sb-wfzpewmlyiozupulbuur-auth-token", JSON.stringify(loginToken));
}).as("githubLogin");
cy.visit("/");
Expand Down
2 changes: 1 addition & 1 deletion src/home/authentication.ts
Expand Up @@ -5,7 +5,7 @@ import { displayGitHubUserInformation } from "./rendering/display-github-user-in
import { renderGitHubLoginButton } from "./rendering/render-github-login-button";

export async function authentication() {
const accessToken = getGitHubAccessToken();
const accessToken = await getGitHubAccessToken();
if (!accessToken) {
renderGitHubLoginButton();
}
Expand Down
16 changes: 16 additions & 0 deletions src/home/fetch-github/fetch-and-display-previews.ts
@@ -1,3 +1,4 @@
import { getGitHubAccessToken } from "../getters/get-github-access-token";
import { getImageFromCache } from "../getters/get-indexed-db";
import { getLocalStore } from "../getters/get-local-store";
import { GITHUB_TASKS_STORAGE_KEY, TaskStorageItems } from "../github-types";
Expand All @@ -16,11 +17,26 @@ export type Options = {

export async function fetchAndDisplayPreviewsFromCache(sorting?: Sorting, options = { ordering: "normal" }) {
let _cachedTasks = getLocalStore(GITHUB_TASKS_STORAGE_KEY) as TaskStorageItems;
const _accessToken = await getGitHubAccessToken();

// Refresh the storage if there is no logged-in object in cachedTasks but there is one now.
if (_cachedTasks && !_cachedTasks.loggedIn && _accessToken) {
localStorage.removeItem(GITHUB_TASKS_STORAGE_KEY);
return fetchAndDisplayPreviewsFromNetwork(sorting, options);
}

// If previously logged in but not anymore, clear cache and fetch from network.
if (_cachedTasks && _cachedTasks.loggedIn && !_accessToken) {
localStorage.removeItem(GITHUB_TASKS_STORAGE_KEY);
return fetchAndDisplayPreviewsFromNetwork(sorting, options);
}

// makes sure tasks have a timestamp to know how old the cache is, or refresh if older than 15 minutes
if (!_cachedTasks || !_cachedTasks.timestamp || _cachedTasks.timestamp + 60 * 1000 * 15 <= Date.now()) {
_cachedTasks = {
timestamp: Date.now(),
tasks: [],
loggedIn: _accessToken !== null,
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/home/fetch-github/fetch-avatar.ts
Expand Up @@ -19,7 +19,7 @@ export async function fetchAvatar(orgName: string) {
}

// If not in IndexedDB, fetch from network
const octokit = new Octokit({ auth: getGitHubAccessToken() });
const octokit = new Octokit({ auth: await getGitHubAccessToken() });
try {
const {
data: { avatar_url: avatarUrl },
Expand Down
2 changes: 1 addition & 1 deletion src/home/fetch-github/fetch-issues-full.ts
Expand Up @@ -8,7 +8,7 @@ import { TaskMaybeFull, TaskWithFull } from "./preview-to-full-mapping";
export const organizationImageCache = new Map<string, Blob | null>();

export async function fetchIssuesFull(taskPreviews: TaskMaybeFull[]): Promise<TaskWithFull[]> {
const octokit = new Octokit({ auth: getGitHubAccessToken() });
const octokit = new Octokit({ auth: await getGitHubAccessToken() });
const urlPattern = /https:\/\/github\.com\/(?<org>[^/]+)\/(?<repo>[^/]+)\/issues\/(?<issue_number>\d+)/;

const fullTaskPromises = taskPreviews.map(async (task) => {
Expand Down
71 changes: 67 additions & 4 deletions src/home/fetch-github/fetch-issues-preview.ts
@@ -1,18 +1,81 @@
import { Octokit } from "@octokit/rest";
import { getGitHubAccessToken } from "../getters/get-github-access-token";
import { getGitHubAccessToken, getGitHubUserName } from "../getters/get-github-access-token";
import { GitHubIssue } from "../github-types";
import { taskManager } from "../home";
import { displayPopupMessage } from "../rendering/display-popup-modal";
import { TaskNoFull } from "./preview-to-full-mapping";

async function checkPrivateRepoAccess(): Promise<boolean> {
const octokit = new Octokit({ auth: await getGitHubAccessToken() });
const username = getGitHubUserName();

if (username) {
try {
const response = await octokit.repos.checkCollaborator({
owner: "ubiquity",
repo: "devpool-directory-private",
username,
});

if (response.status === 204) {
// If the response is successful, it means the user has access to the private repository
return true;
}
return false;
} catch (error) {
if (error.status === 404) {
// If the status is 404, it means the user is not a collaborator, hence no access
return false;
} else {
// Handle other errors if needed
console.error("Error checking repository access:", error);
throw error;
}
}
}

return false;
}

export async function fetchIssuePreviews(): Promise<TaskNoFull[]> {
const octokit = new Octokit({ auth: getGitHubAccessToken() });
const octokit = new Octokit({ auth: await getGitHubAccessToken() });

let freshIssues: GitHubIssue[] = [];
let hasPrivateRepoAccess = false; // Flag to track access to the private repository

try {
const response = await octokit.paginate<GitHubIssue>("GET /repos/ubiquity/devpool-directory/issues", { state: "open" });
// Check if the user has access to the private repository
hasPrivateRepoAccess = await checkPrivateRepoAccess();

// Fetch issues from public repository
const { data: publicResponse } = await octokit.issues.listForRepo({
owner: "ubiquity",
repo: "devpool-directory",
state: "open",
});

const publicIssues = publicResponse.filter((issue: GitHubIssue) => !issue.pull_request);

freshIssues = response.filter((issue: GitHubIssue) => !issue.pull_request);
// Fetch issues from the private repository only if the user has access
if (hasPrivateRepoAccess) {
const { data: privateResponse } = await octokit.issues.listForRepo({
owner: "ubiquity",
repo: "devpool-directory-private",
state: "open",
});
const privateIssues = privateResponse.filter((issue: GitHubIssue) => !issue.pull_request);

// Mark private issues
const privateIssuesWithFlag = privateIssues.map((issue) => {
return issue;
});

// Combine public and private issues
freshIssues = [...privateIssuesWithFlag, ...publicIssues];
} else {
// If user doesn't have access, only load issues from the public repository
freshIssues = publicIssues;
}
} catch (error) {
if (403 === error.status) {
console.error(`GitHub API rate limit exceeded.`);
Expand Down
20 changes: 17 additions & 3 deletions src/home/getters/get-github-access-token.ts
@@ -1,12 +1,15 @@
declare const SUPABASE_STORAGE_KEY: string; // @DEV: passed in at build time check build/esbuild-build.ts
import { checkSupabaseSession } from "../rendering/render-github-login-button";
import { getLocalStore } from "./get-local-store";

export function getGitHubAccessToken(): string | null {
const oauthToken = getLocalStore("sb-wfzpewmlyiozupulbuur-auth-token") as OAuthToken | null;
export async function getGitHubAccessToken(): Promise<string | null> {
// better to use official function, looking up localstorage has flaws
const oauthToken = await checkSupabaseSession();

const expiresAt = oauthToken?.expires_at;
if (expiresAt) {
if (expiresAt < Date.now() / 1000) {
localStorage.removeItem("sb-wfzpewmlyiozupulbuur-auth-token");
localStorage.removeItem(`sb-${SUPABASE_STORAGE_KEY}-auth-token`);
return null;
}
}
Expand All @@ -19,6 +22,17 @@ export function getGitHubAccessToken(): string | null {
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;
}

export interface OAuthToken {
provider_token: string;
access_token: string;
Expand Down
3 changes: 2 additions & 1 deletion src/home/getters/get-github-user.ts
Expand Up @@ -2,6 +2,7 @@ import { Octokit } from "@octokit/rest";
import { GitHubUser, GitHubUserResponse } from "../github-types";
import { OAuthToken } from "./get-github-access-token";
import { getLocalStore } from "./get-local-store";
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();
Expand All @@ -13,7 +14,7 @@ export async function getGitHubUser(): Promise<GitHubUser | null> {
}

async function getSessionToken(): Promise<string | null> {
const cachedSessionToken = getLocalStore("sb-wfzpewmlyiozupulbuur-auth-token") as OAuthToken | null;
const cachedSessionToken = getLocalStore(`sb-${SUPABASE_STORAGE_KEY}-auth-token`) as OAuthToken | null;
if (cachedSessionToken) {
return cachedSessionToken.provider_token;
}
Expand Down

0 comments on commit 04e1f25

Please sign in to comment.