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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion src/home/authentication.ts
Original file line number Diff line number Diff line change
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
17 changes: 17 additions & 0 deletions src/home/fetch-github/fetch-and-display-previews.ts
Original file line number Diff line number Diff line change
@@ -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);
}
0x4007 marked this conversation as resolved.
Show resolved Hide resolved

// 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,
};
}
const cachedTasks = _cachedTasks.tasks.map((task) => ({ ...task, isNew: false, isModified: false })) as TaskMaybeFull[];
Expand Down Expand Up @@ -48,6 +64,7 @@ export async function fetchAvatars() {
const urlPattern = /https:\/\/github\.com\/(?<org>[^/]+)\/(?<repo>[^/]+)\/issues\/(?<issue_number>\d+)/;

const avatarPromises = cachedTasks.map(async (task) => {
if (!task.preview.body) return;
devpanther marked this conversation as resolved.
Show resolved Hide resolved
const match = task.preview.body.match(urlPattern);
const orgName = match?.groups?.org;
if (orgName) {
Expand Down
2 changes: 1 addition & 1 deletion src/home/fetch-github/fetch-avatar.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
59 changes: 55 additions & 4 deletions src/home/fetch-github/fetch-issues-preview.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,69 @@
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();

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;
}
}
}

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 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 = [...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
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { SUPABASE_STORAGE_KEY } from "../github-types";
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 {
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
Original file line number Diff line number Diff line change
@@ -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
72 changes: 10 additions & 62 deletions src/home/github-types.ts
devpanther marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { TaskNoState } from "./fetch-github/preview-to-full-mapping";

import { RestEndpointMethodTypes } from "@octokit/plugin-rest-endpoint-methods";
export interface GitHubUser {
avatar_url: string;
bio: string;
Expand Down Expand Up @@ -55,75 +55,23 @@ export interface GitHubUserResponse {
data: GitHubUser;
}

interface GitHubLabel {
id: number;
node_id: string;
url: string;
name: string;
description: string;
color: string;
default: boolean;
}

interface GitHubMilestone {
url: string;
html_url: string;
labels_url: string;
id: number;
node_id: string;
number: number;
state: string;
title: string;
description: string;
creator: GitHubUser;
open_issues: number;
closed_issues: number;
created_at: string;
updated_at: string;
closed_at: string | null;
due_on: string | null;
}

export interface GitHubIssue {
id: number;
node_id: string;
url: string;
repository_url: string;
labels_url: string;
comments_url: string;
events_url: string;
html_url: string;
number: number;
state: string;
title: string;
body: string;
user: GitHubUser;
labels: GitHubLabel[];
assignee: GitHubUser | null;
assignees: GitHubUser[];
milestone: GitHubMilestone | null;
locked: boolean;
active_lock_reason: string | null;
comments: number;
pull_request?: {
url: string;
html_url: string;
diff_url: string;
patch_url: string;
};
closed_at: string | null;
created_at: string;
updated_at: string;
closed_by?: GitHubUser;
}
export type GitHubIssue = RestEndpointMethodTypes["issues"]["get"]["response"]["data"];

export interface AvatarCache {
[organization: string]: string | null;
}

export const GITHUB_TASKS_STORAGE_KEY = "gitHubTasks";

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

export type TaskStorageItems = {
timestamp: number;
tasks: TaskNoState[];
loggedIn: boolean;
};
2 changes: 1 addition & 1 deletion src/home/home.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ void (async function home() {
const fullTasks = await fetchIssuesFull(previews);
taskManager.syncTasks(fullTasks);
console.trace({ fullTasks });
taskManager.writeToStorage();
await taskManager.writeToStorage();
return fullTasks;
} catch (error) {
console.error(error);
Expand Down
21 changes: 21 additions & 0 deletions src/home/rendering/render-github-issues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ function everyNewIssue({ taskPreview, container }: { taskPreview: TaskMaybeFull;
}

const urlPattern = /https:\/\/github\.com\/([^/]+)\/([^/]+)\//;
if (!taskPreview.preview.body) {
console.warn(`No body found for issue ${taskPreview.preview.id}.`);
return;
}
const match = taskPreview.preview.body.match(urlPattern);
const organizationName = match?.[1];

Expand Down Expand Up @@ -112,6 +116,22 @@ function parseAndGenerateLabels(task: TaskMaybeFull) {

const { labels, otherLabels } = task.preview.labels.reduce(
(acc, label) => {
// check if label is a single string
if (typeof label === "string") {
return {
labels: [],
otherLabels: [],
};
}

// check if label.name exists
if (!label.name) {
return {
labels: [],
otherLabels: [],
};
}

const match = label.name.match(/^(Pricing|Time|Priority): /);
if (match) {
const name = label.name.replace(match[0], "");
Expand Down Expand Up @@ -156,6 +176,7 @@ export function viewIssueDetails(full: GitHubIssue) {
// Update the title and body for the new issue
titleHeader.textContent = full.title;
titleAnchor.href = full.html_url;
if (!full.body) return;
previewBodyInner.innerHTML = marked(full.body) as string;

// Show the preview
Expand Down
15 changes: 14 additions & 1 deletion src/home/rendering/render-github-login-button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,21 @@ export function getSupabase() {
return supabase;
}

export async function checkSupabaseSession() {
const {
data: { session },
} = await supabase.auth.getSession();

return session;
}

async function gitHubLoginButtonHandler() {
const { error } = await supabase.auth.signInWithOAuth({ provider: "github" });
const { error } = await supabase.auth.signInWithOAuth({
provider: "github",
options: {
scopes: "repo",
},
});
if (error) {
console.error("Error logging in:", error);
}
Expand Down
6 changes: 4 additions & 2 deletions src/home/task-manager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { TaskMaybeFull } from "./fetch-github/preview-to-full-mapping";
import { getGitHubAccessToken } from "./getters/get-github-access-token";
import { setLocalStore } from "./getters/get-local-store";
import { GITHUB_TASKS_STORAGE_KEY } from "./github-types";

Expand Down Expand Up @@ -50,7 +51,8 @@ export class TaskManager {
return this._container;
}

public writeToStorage() {
setLocalStore(GITHUB_TASKS_STORAGE_KEY, { timestamp: Date.now(), tasks: this._tasks });
public async writeToStorage() {
const _accessToken = await getGitHubAccessToken();
setLocalStore(GITHUB_TASKS_STORAGE_KEY, { timestamp: Date.now(), tasks: this._tasks, loggedIn: _accessToken !== null });
}
}
Loading