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: Private tasks list #24

Merged
merged 16 commits into from Mar 8, 2024
Merged
1 change: 0 additions & 1 deletion README.md
Expand Up @@ -36,4 +36,3 @@ open http://localhost:8080
#### Mobile

![screenshot 2](https://github.com/ubiquity/devpool-directory-ui/assets/4975670/b7861ce7-1f1f-49a9-b8e2-ebb20724ee67)

3 changes: 2 additions & 1 deletion cypress/e2e/devpool.cy.ts
@@ -1,5 +1,6 @@
import { RestEndpointMethodTypes } from "@octokit/rest";
import { OAuthToken } from "../../src/home/getters/get-github-access-token";
import { SUPABASE_STORAGE_KEY } from "../../src/home/github-types";

describe("DevPool", () => {
let issue1: RestEndpointMethodTypes["issues"]["get"]["response"]["data"];
Expand Down Expand Up @@ -139,7 +140,7 @@ describe("DevPool", () => {
statusCode: 200,
});
// Simulate login token
window.localStorage.setItem("sb-wfzpewmlyiozupulbuur-auth-token", JSON.stringify(loginToken));
window.localStorage.setItem(`sb-${SUPABASE_STORAGE_KEY}-auth-token`, JSON.stringify(loginToken));
}).as("githubLogin");
cy.visit("/");
cy.get("#github-login-button").click();
Expand Down
52 changes: 49 additions & 3 deletions src/home/fetch-github/fetch-issues-preview.ts
@@ -1,18 +1,64 @@
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: getGitHubAccessToken() });
const username = getGitHubUserName();

try {
await octokit.request("GET /repos/ubiquity/devpool-directory-private/collaborators/{username}", {
devpanther marked this conversation as resolved.
Show resolved Hide resolved
username,
});

// If the response is successful, it means the user has access to the private repository
return true;
} 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;
}
}
}

export async function fetchIssuePreviews(): Promise<TaskNoFull[]> {
const octokit = new Octokit({ auth: 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 publicResponse = await octokit.paginate<GitHubIssue>("GET /repos/ubiquity/devpool-directory/issues", { state: "open" });
devpanther marked this conversation as resolved.
Show resolved Hide resolved
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 privateResponse = await octokit.paginate<GitHubIssue>("GET /repos/ubiquity/devpool-directory-private/issues", { state: "open" });
const privateIssues = privateResponse.filter((issue: GitHubIssue) => !issue.pull_request);

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

// Combine public and private issues
freshIssues = [...publicIssues, ...privateIssuesWithFlag];
} 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
16 changes: 14 additions & 2 deletions src/home/getters/get-github-access-token.ts
@@ -1,12 +1,13 @@
import { SUPABASE_STORAGE_KEY } from "../github-types";
import { getLocalStore } from "./get-local-store";

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

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 +20,17 @@ export function getGitHubAccessToken(): string | null {
return null;
}

export function getGitHubUserName(): string {
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 "";
devpanther marked this conversation as resolved.
Show resolved Hide resolved
}

export interface OAuthToken {
provider_token: string;
access_token: string;
Expand Down
4 changes: 2 additions & 2 deletions src/home/getters/get-github-user.ts
@@ -1,5 +1,5 @@
import { Octokit } from "@octokit/rest";
import { GitHubUser, GitHubUserResponse } from "../github-types";
import { GitHubUser, GitHubUserResponse, SUPABASE_STORAGE_KEY } from "../github-types";
import { OAuthToken } from "./get-github-access-token";
import { getLocalStore } from "./get-local-store";

Expand All @@ -13,7 +13,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
6 changes: 5 additions & 1 deletion src/home/github-types.ts
devpanther marked this conversation as resolved.
Show resolved Hide resolved
@@ -1,5 +1,4 @@
import { TaskNoState } from "./fetch-github/preview-to-full-mapping";

export interface GitHubUser {
avatar_url: string;
bio: string;
Expand Down Expand Up @@ -115,6 +114,7 @@ export interface GitHubIssue {
created_at: string;
updated_at: string;
closed_by?: GitHubUser;
private?: boolean;
}

export interface AvatarCache {
Expand All @@ -123,6 +123,10 @@ export interface AvatarCache {

export const GITHUB_TASKS_STORAGE_KEY = "gitHubTasks";

// supabase key should be dynamic incase of change and testing
const supabaseUrl = process.env.SUPABASE_URL ? process.env.SUPABASE_URL.split(".")[0] : "";
devpanther marked this conversation as resolved.
Show resolved Hide resolved
export const SUPABASE_STORAGE_KEY = supabaseUrl.substring(supabaseUrl.lastIndexOf("/") + 1);

export type TaskStorageItems = {
timestamp: number;
tasks: TaskNoState[];
Expand Down