mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-19 17:01:13 +00:00
Initial Move from Azure DevOps to GitHub
This commit is contained in:
92
src/GitHub/GitHubClient.test.ts
Normal file
92
src/GitHub/GitHubClient.test.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import ko from "knockout";
|
||||
import { HttpStatusCodes } from "../Common/Constants";
|
||||
import { GitHubClient, IGitHubBranch, IGitHubRepo } from "./GitHubClient";
|
||||
|
||||
const invalidTokenCallback = jest.fn();
|
||||
// Use a dummy token to get around API rate limit (same as AZURESAMPLESCOSMOSDBPAT in webpack.config.js)
|
||||
const gitHubClient = new GitHubClient("99e38770e29b4a61d7c49f188780504efd35cc86", invalidTokenCallback);
|
||||
const samplesRepo: IGitHubRepo = {
|
||||
name: "cosmos-notebooks",
|
||||
owner: {
|
||||
login: "Azure-Samples"
|
||||
},
|
||||
private: false
|
||||
};
|
||||
const samplesBranch: IGitHubBranch = {
|
||||
name: "master"
|
||||
};
|
||||
const sampleFilePath = ".gitignore";
|
||||
const sampleDirPath = ".github";
|
||||
|
||||
describe.skip("GitHubClient", () => {
|
||||
it("getRepoAsync returns valid repo", async () => {
|
||||
const response = await gitHubClient.getRepoAsync(samplesRepo.owner.login, samplesRepo.name);
|
||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||
expect(response.data.name).toBe(samplesRepo.name);
|
||||
expect(response.data.owner.login).toBe(samplesRepo.owner.login);
|
||||
});
|
||||
|
||||
it("getReposAsync returns repos for authenticated user", async () => {
|
||||
const response = await gitHubClient.getReposAsync(1, 1);
|
||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||
expect(response.data.length).toBe(1);
|
||||
});
|
||||
|
||||
it("getBranchesAsync returns branches for a repo", async () => {
|
||||
const response = await gitHubClient.getBranchesAsync(samplesRepo.owner.login, samplesRepo.name, 1, 1);
|
||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||
expect(response.data.length).toBe(1);
|
||||
});
|
||||
|
||||
it("getCommitsAsync returns commits for a file", async () => {
|
||||
const response = await gitHubClient.getCommitsAsync(
|
||||
samplesRepo.owner.login,
|
||||
samplesRepo.name,
|
||||
samplesBranch.name,
|
||||
sampleFilePath,
|
||||
1,
|
||||
1
|
||||
);
|
||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||
expect(response.data.length).toBe(1);
|
||||
});
|
||||
|
||||
it("getDirContentsAsync returns files in the repo", async () => {
|
||||
const response = await gitHubClient.getDirContentsAsync(
|
||||
samplesRepo.owner.login,
|
||||
samplesRepo.name,
|
||||
samplesBranch.name,
|
||||
""
|
||||
);
|
||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||
expect(response.data.length).toBeGreaterThan(0);
|
||||
expect(response.data[0].repo).toEqual(samplesRepo);
|
||||
expect(response.data[0].branch).toEqual(samplesBranch);
|
||||
});
|
||||
|
||||
it("getDirContentsAsync returns files in a dir", async () => {
|
||||
const response = await gitHubClient.getDirContentsAsync(
|
||||
samplesRepo.owner.login,
|
||||
samplesRepo.name,
|
||||
samplesBranch.name,
|
||||
sampleDirPath
|
||||
);
|
||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||
expect(response.data.length).toBeGreaterThan(0);
|
||||
expect(response.data[0].repo).toEqual(samplesRepo);
|
||||
expect(response.data[0].branch).toEqual(samplesBranch);
|
||||
});
|
||||
|
||||
it("getFileContentsAsync returns a file", async () => {
|
||||
const response = await gitHubClient.getFileContentsAsync(
|
||||
samplesRepo.owner.login,
|
||||
samplesRepo.name,
|
||||
samplesBranch.name,
|
||||
sampleFilePath
|
||||
);
|
||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||
expect(response.data.path).toBe(sampleFilePath);
|
||||
expect(response.data.repo).toEqual(samplesRepo);
|
||||
expect(response.data.branch).toEqual(samplesBranch);
|
||||
});
|
||||
});
|
||||
528
src/GitHub/GitHubClient.ts
Normal file
528
src/GitHub/GitHubClient.ts
Normal file
@@ -0,0 +1,528 @@
|
||||
import { Octokit } from "@octokit/rest";
|
||||
import { RequestHeaders } from "@octokit/types";
|
||||
import { HttpStatusCodes } from "../Common/Constants";
|
||||
|
||||
export interface IGitHubResponse<T> {
|
||||
status: number;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface IGitHubRepo {
|
||||
// API properties
|
||||
name: string;
|
||||
owner: {
|
||||
login: string;
|
||||
};
|
||||
private: boolean;
|
||||
|
||||
// Custom properties
|
||||
children?: IGitHubFile[];
|
||||
}
|
||||
|
||||
export interface IGitHubFile {
|
||||
// API properties
|
||||
type: "file" | "dir" | "symlink" | "submodule";
|
||||
encoding?: string;
|
||||
size: number;
|
||||
name: string;
|
||||
path: string;
|
||||
content?: string;
|
||||
sha: string;
|
||||
url: string;
|
||||
html_url: string;
|
||||
|
||||
// Custom properties
|
||||
children?: IGitHubFile[];
|
||||
repo?: IGitHubRepo;
|
||||
branch?: IGitHubBranch;
|
||||
}
|
||||
|
||||
export interface IGitHubCommit {
|
||||
// API properties
|
||||
sha: string;
|
||||
message: string;
|
||||
committer: {
|
||||
date: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IGitHubBranch {
|
||||
// API properties
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface IGitHubUser {
|
||||
// API properties
|
||||
login: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class GitHubClient {
|
||||
private static readonly gitHubApiEndpoint = "https://api.github.com";
|
||||
|
||||
private static readonly samplesRepo: IGitHubRepo = {
|
||||
name: "cosmos-notebooks",
|
||||
private: false,
|
||||
owner: {
|
||||
login: "Azure-Samples"
|
||||
}
|
||||
};
|
||||
|
||||
private static readonly samplesTopCommit: IGitHubCommit = {
|
||||
sha: "41b964f442b638097a75a3f3b6a6451db05a12bf",
|
||||
committer: {
|
||||
date: "2020-05-19T05:03:30Z"
|
||||
},
|
||||
message: "Fixing formatting"
|
||||
};
|
||||
|
||||
private static readonly samplesFiles: IGitHubFile[] = [
|
||||
{
|
||||
name: ".github",
|
||||
path: ".github",
|
||||
sha: "5e6794a8177a0c07a8719f6e1d7b41cce6f92e1e",
|
||||
size: 0,
|
||||
url: "https://api.github.com/repos/Azure-Samples/cosmos-notebooks/contents/.github?ref=master",
|
||||
html_url: "https://github.com/Azure-Samples/cosmos-notebooks/tree/master/.github",
|
||||
type: "dir"
|
||||
},
|
||||
{
|
||||
name: ".gitignore",
|
||||
path: ".gitignore",
|
||||
sha: "3e759b75bf455ac809d0987d369aab89137b5689",
|
||||
size: 5582,
|
||||
url: "https://api.github.com/repos/Azure-Samples/cosmos-notebooks/contents/.gitignore?ref=master",
|
||||
html_url: "https://github.com/Azure-Samples/cosmos-notebooks/blob/master/.gitignore",
|
||||
type: "file"
|
||||
},
|
||||
{
|
||||
name: "1. GettingStarted.ipynb",
|
||||
path: "1. GettingStarted.ipynb",
|
||||
sha: "0732ff5366e4aefdc4c378c61cbd968664f0acec",
|
||||
size: 3933,
|
||||
url: "https://api.github.com/repos/Azure-Samples/cosmos-notebooks/contents/1.%20GettingStarted.ipynb?ref=master",
|
||||
html_url: "https://github.com/Azure-Samples/cosmos-notebooks/blob/master/1.%20GettingStarted.ipynb",
|
||||
type: "file"
|
||||
},
|
||||
{
|
||||
name: "2. Visualization.ipynb",
|
||||
path: "2. Visualization.ipynb",
|
||||
sha: "f480134ac4adf2f50ce5fe66836c6966749d3ca1",
|
||||
size: 814261,
|
||||
url: "https://api.github.com/repos/Azure-Samples/cosmos-notebooks/contents/2.%20Visualization.ipynb?ref=master",
|
||||
html_url: "https://github.com/Azure-Samples/cosmos-notebooks/blob/master/2.%20Visualization.ipynb",
|
||||
type: "file"
|
||||
},
|
||||
{
|
||||
name: "3. RequestUnits.ipynb",
|
||||
path: "3. RequestUnits.ipynb",
|
||||
sha: "252b79a4adc81e9f2ffde453231b695d75e270e8",
|
||||
size: 9490,
|
||||
url: "https://api.github.com/repos/Azure-Samples/cosmos-notebooks/contents/3.%20RequestUnits.ipynb?ref=master",
|
||||
html_url: "https://github.com/Azure-Samples/cosmos-notebooks/blob/master/3.%20RequestUnits.ipynb",
|
||||
type: "file"
|
||||
},
|
||||
{
|
||||
name: "4. Indexing.ipynb",
|
||||
path: "4. Indexing.ipynb",
|
||||
sha: "e10dd67bd1c55c345226769e4f80e43659ef9cd5",
|
||||
size: 10394,
|
||||
url: "https://api.github.com/repos/Azure-Samples/cosmos-notebooks/contents/4.%20Indexing.ipynb?ref=master",
|
||||
html_url: "https://github.com/Azure-Samples/cosmos-notebooks/blob/master/4.%20Indexing.ipynb",
|
||||
type: "file"
|
||||
},
|
||||
{
|
||||
name: "5. StoredProcedures.ipynb",
|
||||
path: "5. StoredProcedures.ipynb",
|
||||
sha: "949941949920de4d2d111149e2182e9657cc8134",
|
||||
size: 11818,
|
||||
url:
|
||||
"https://api.github.com/repos/Azure-Samples/cosmos-notebooks/contents/5.%20StoredProcedures.ipynb?ref=master",
|
||||
html_url: "https://github.com/Azure-Samples/cosmos-notebooks/blob/master/5.%20StoredProcedures.ipynb",
|
||||
type: "file"
|
||||
},
|
||||
{
|
||||
name: "6. GlobalDistribution.ipynb",
|
||||
path: "6. GlobalDistribution.ipynb",
|
||||
sha: "b91c31dacacbc9e35750d9054063dda4a5309f3b",
|
||||
size: 11375,
|
||||
url:
|
||||
"https://api.github.com/repos/Azure-Samples/cosmos-notebooks/contents/6.%20GlobalDistribution.ipynb?ref=master",
|
||||
html_url: "https://github.com/Azure-Samples/cosmos-notebooks/blob/master/6.%20GlobalDistribution.ipynb",
|
||||
type: "file"
|
||||
},
|
||||
{
|
||||
name: "7. IoTAnomalyDetection.ipynb",
|
||||
path: "7. IoTAnomalyDetection.ipynb",
|
||||
sha: "82057ae52a67721a5966e2361317f5dfbd0ee595",
|
||||
size: 377939,
|
||||
url:
|
||||
"https://api.github.com/repos/Azure-Samples/cosmos-notebooks/contents/7.%20IoTAnomalyDetection.ipynb?ref=master",
|
||||
html_url: "https://github.com/Azure-Samples/cosmos-notebooks/blob/master/7.%20IoTAnomalyDetection.ipynb",
|
||||
type: "file"
|
||||
},
|
||||
{
|
||||
name: "All_API_quickstarts",
|
||||
path: "All_API_quickstarts",
|
||||
sha: "07054293e6c8fc00771fccd0cde207f5c8053978",
|
||||
size: 0,
|
||||
url: "https://api.github.com/repos/Azure-Samples/cosmos-notebooks/contents/All_API_quickstarts?ref=master",
|
||||
html_url: "https://github.com/Azure-Samples/cosmos-notebooks/tree/master/All_API_quickstarts",
|
||||
type: "dir"
|
||||
},
|
||||
{
|
||||
name: "CSharp_quickstarts",
|
||||
path: "CSharp_quickstarts",
|
||||
sha: "10e7f5704e6b56a40cac74bc39f15b7708954f52",
|
||||
size: 0,
|
||||
url: "https://api.github.com/repos/Azure-Samples/cosmos-notebooks/contents/CSharp_quickstarts?ref=master",
|
||||
html_url: "https://github.com/Azure-Samples/cosmos-notebooks/tree/master/CSharp_quickstarts",
|
||||
type: "dir"
|
||||
}
|
||||
];
|
||||
|
||||
private ocktokit: Octokit;
|
||||
|
||||
constructor(token: string, private errorCallback: (error: any) => void) {
|
||||
this.initOctokit(token);
|
||||
}
|
||||
|
||||
public setToken(token: string): void {
|
||||
this.initOctokit(token);
|
||||
}
|
||||
|
||||
public async getRepoAsync(owner: string, repo: string): Promise<IGitHubResponse<IGitHubRepo>> {
|
||||
if (GitHubClient.isSamplesCall(owner, repo)) {
|
||||
return {
|
||||
status: HttpStatusCodes.OK,
|
||||
data: GitHubClient.samplesRepo
|
||||
};
|
||||
}
|
||||
|
||||
const response = await this.ocktokit.repos.get({
|
||||
owner,
|
||||
repo,
|
||||
headers: GitHubClient.getDisableCacheHeaders()
|
||||
});
|
||||
|
||||
let data: IGitHubRepo;
|
||||
if (response.data) {
|
||||
data = GitHubClient.toGitHubRepo(response.data);
|
||||
}
|
||||
|
||||
return { status: response.status, data };
|
||||
}
|
||||
|
||||
public async getReposAsync(page: number, perPage: number): Promise<IGitHubResponse<IGitHubRepo[]>> {
|
||||
const response = await this.ocktokit.repos.listForAuthenticatedUser({
|
||||
page,
|
||||
per_page: perPage,
|
||||
headers: GitHubClient.getDisableCacheHeaders()
|
||||
});
|
||||
|
||||
let data: IGitHubRepo[];
|
||||
if (response.data) {
|
||||
data = [];
|
||||
response.data?.forEach((element: any) => data.push(GitHubClient.toGitHubRepo(element)));
|
||||
}
|
||||
|
||||
return { status: response.status, data };
|
||||
}
|
||||
|
||||
public async getBranchesAsync(
|
||||
owner: string,
|
||||
repo: string,
|
||||
page: number,
|
||||
perPage: number
|
||||
): Promise<IGitHubResponse<IGitHubBranch[]>> {
|
||||
const response = await this.ocktokit.repos.listBranches({
|
||||
owner,
|
||||
repo,
|
||||
page,
|
||||
per_page: perPage,
|
||||
headers: GitHubClient.getDisableCacheHeaders()
|
||||
});
|
||||
|
||||
let data: IGitHubBranch[];
|
||||
if (response.data) {
|
||||
data = [];
|
||||
response.data?.forEach(element => data.push(GitHubClient.toGitHubBranch(element)));
|
||||
}
|
||||
|
||||
return { status: response.status, data };
|
||||
}
|
||||
|
||||
public async getCommitsAsync(
|
||||
owner: string,
|
||||
repo: string,
|
||||
branch: string,
|
||||
path: string,
|
||||
page: number,
|
||||
perPage: number
|
||||
): Promise<IGitHubResponse<IGitHubCommit[]>> {
|
||||
if (GitHubClient.isSamplesCall(owner, repo, branch) && path === "" && page === 1 && perPage === 1) {
|
||||
return {
|
||||
status: HttpStatusCodes.OK,
|
||||
data: [GitHubClient.samplesTopCommit]
|
||||
};
|
||||
}
|
||||
|
||||
const response = await this.ocktokit.repos.listCommits({
|
||||
owner,
|
||||
repo,
|
||||
sha: branch,
|
||||
path,
|
||||
page,
|
||||
per_page: perPage,
|
||||
headers: GitHubClient.getDisableCacheHeaders()
|
||||
});
|
||||
|
||||
let data: IGitHubCommit[];
|
||||
if (response.data) {
|
||||
data = [];
|
||||
response.data?.forEach(element =>
|
||||
data.push(GitHubClient.toGitHubCommit({ ...element.commit, sha: element.sha }))
|
||||
);
|
||||
}
|
||||
|
||||
return { status: response.status, data };
|
||||
}
|
||||
|
||||
public async getDirContentsAsync(
|
||||
owner: string,
|
||||
repo: string,
|
||||
branch: string,
|
||||
path: string
|
||||
): Promise<IGitHubResponse<IGitHubFile[]>> {
|
||||
return (await this.getContentsAsync(owner, repo, branch, path)) as IGitHubResponse<IGitHubFile[]>;
|
||||
}
|
||||
|
||||
public async getFileContentsAsync(
|
||||
owner: string,
|
||||
repo: string,
|
||||
branch: string,
|
||||
path: string
|
||||
): Promise<IGitHubResponse<IGitHubFile>> {
|
||||
return (await this.getContentsAsync(owner, repo, branch, path)) as IGitHubResponse<IGitHubFile>;
|
||||
}
|
||||
|
||||
public async getContentsAsync(
|
||||
owner: string,
|
||||
repo: string,
|
||||
branch: string,
|
||||
path: string
|
||||
): Promise<IGitHubResponse<IGitHubFile | IGitHubFile[]>> {
|
||||
if (GitHubClient.isSamplesCall(owner, repo, branch) && path === "") {
|
||||
return {
|
||||
status: HttpStatusCodes.OK,
|
||||
data: GitHubClient.samplesFiles
|
||||
};
|
||||
}
|
||||
|
||||
const response = await this.ocktokit.repos.getContents({
|
||||
owner,
|
||||
repo,
|
||||
path,
|
||||
ref: branch,
|
||||
headers: GitHubClient.getDisableCacheHeaders()
|
||||
});
|
||||
|
||||
let data: IGitHubFile | IGitHubFile[];
|
||||
if (response.data) {
|
||||
const repoResponse = await this.getRepoAsync(owner, repo);
|
||||
if (repoResponse.data) {
|
||||
const fileRepo: IGitHubRepo = GitHubClient.toGitHubRepo(repoResponse.data);
|
||||
const fileBranch: IGitHubBranch = { name: branch };
|
||||
|
||||
if (Array.isArray(response.data)) {
|
||||
const contents: IGitHubFile[] = [];
|
||||
response.data.forEach((element: any) =>
|
||||
contents.push(GitHubClient.toGitHubFile(element, fileRepo, fileBranch))
|
||||
);
|
||||
data = contents;
|
||||
} else {
|
||||
data = GitHubClient.toGitHubFile(
|
||||
{ ...response.data, type: response.data.type as "file" | "dir" | "symlink" | "submodule" },
|
||||
fileRepo,
|
||||
fileBranch
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { status: response.status, data };
|
||||
}
|
||||
|
||||
public async createOrUpdateFileAsync(
|
||||
owner: string,
|
||||
repo: string,
|
||||
branch: string,
|
||||
path: string,
|
||||
message: string,
|
||||
content: string,
|
||||
sha?: string
|
||||
): Promise<IGitHubResponse<IGitHubCommit>> {
|
||||
const response = await this.ocktokit.repos.createOrUpdateFile({
|
||||
owner,
|
||||
repo,
|
||||
branch,
|
||||
path,
|
||||
message,
|
||||
content,
|
||||
sha
|
||||
});
|
||||
|
||||
let data: IGitHubCommit;
|
||||
if (response.data) {
|
||||
data = GitHubClient.toGitHubCommit(response.data.commit);
|
||||
}
|
||||
|
||||
return { status: response.status, data };
|
||||
}
|
||||
|
||||
public async renameFileAsync(
|
||||
owner: string,
|
||||
repo: string,
|
||||
branch: string,
|
||||
message: string,
|
||||
oldPath: string,
|
||||
newPath: string
|
||||
): Promise<IGitHubResponse<IGitHubCommit>> {
|
||||
const ref = `heads/${branch}`;
|
||||
const currentRef = await this.ocktokit.git.getRef({
|
||||
owner,
|
||||
repo,
|
||||
ref,
|
||||
headers: GitHubClient.getDisableCacheHeaders()
|
||||
});
|
||||
|
||||
const currentTree = await this.ocktokit.git.getTree({
|
||||
owner,
|
||||
repo,
|
||||
tree_sha: currentRef.data.object.sha,
|
||||
recursive: "1",
|
||||
headers: GitHubClient.getDisableCacheHeaders()
|
||||
});
|
||||
|
||||
// API infers tree from paths so we need to filter them out
|
||||
const currentTreeItems = currentTree.data.tree.filter(item => item.type !== "tree");
|
||||
currentTreeItems.forEach(item => {
|
||||
if (item.path === newPath) {
|
||||
throw new Error("File with the path already exists");
|
||||
}
|
||||
});
|
||||
|
||||
const updatedTree = await this.ocktokit.git.createTree({
|
||||
owner,
|
||||
repo,
|
||||
tree: currentTreeItems.map(item => ({
|
||||
path: item.path === oldPath ? newPath : item.path,
|
||||
mode: item.mode as "100644" | "100755" | "040000" | "160000" | "120000",
|
||||
type: item.type as "blob" | "tree" | "commit",
|
||||
sha: item.sha
|
||||
}))
|
||||
});
|
||||
|
||||
const newCommit = await this.ocktokit.git.createCommit({
|
||||
owner,
|
||||
repo,
|
||||
message,
|
||||
parents: [currentRef.data.object.sha],
|
||||
tree: updatedTree.data.sha
|
||||
});
|
||||
|
||||
const updatedRef = await this.ocktokit.git.updateRef({
|
||||
owner,
|
||||
repo,
|
||||
ref,
|
||||
sha: newCommit.data.sha
|
||||
});
|
||||
|
||||
return {
|
||||
status: updatedRef.status,
|
||||
data: GitHubClient.toGitHubCommit(newCommit.data)
|
||||
};
|
||||
}
|
||||
|
||||
public async deleteFileAsync(file: IGitHubFile, message: string): Promise<IGitHubResponse<IGitHubCommit>> {
|
||||
const response = await this.ocktokit.repos.deleteFile({
|
||||
owner: file.repo.owner.login,
|
||||
repo: file.repo.name,
|
||||
path: file.path,
|
||||
message,
|
||||
sha: file.sha,
|
||||
branch: file.branch.name
|
||||
});
|
||||
|
||||
let data: IGitHubCommit;
|
||||
if (response.data) {
|
||||
data = GitHubClient.toGitHubCommit(response.data.commit);
|
||||
}
|
||||
|
||||
return { status: response.status, data };
|
||||
}
|
||||
|
||||
private initOctokit(token: string) {
|
||||
this.ocktokit = new Octokit({
|
||||
auth: token,
|
||||
baseUrl: GitHubClient.gitHubApiEndpoint
|
||||
});
|
||||
|
||||
this.ocktokit.hook.error("request", error => {
|
||||
this.errorCallback(error);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
private static getDisableCacheHeaders(): RequestHeaders {
|
||||
return {
|
||||
"If-None-Match": ""
|
||||
};
|
||||
}
|
||||
|
||||
private static toGitHubRepo(element: IGitHubRepo): IGitHubRepo {
|
||||
return {
|
||||
name: element.name,
|
||||
owner: {
|
||||
login: element.owner.login
|
||||
},
|
||||
private: element.private
|
||||
};
|
||||
}
|
||||
|
||||
private static toGitHubBranch(element: IGitHubBranch): IGitHubBranch {
|
||||
return {
|
||||
name: element.name
|
||||
};
|
||||
}
|
||||
|
||||
private static toGitHubCommit(element: IGitHubCommit): IGitHubCommit {
|
||||
return {
|
||||
sha: element.sha,
|
||||
message: element.message,
|
||||
committer: {
|
||||
date: element.committer.date
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static toGitHubFile(element: IGitHubFile, repo: IGitHubRepo, branch: IGitHubBranch): IGitHubFile {
|
||||
return {
|
||||
type: element.type,
|
||||
encoding: element.encoding,
|
||||
size: element.size,
|
||||
name: element.name,
|
||||
path: element.path,
|
||||
content: element.content,
|
||||
sha: element.sha,
|
||||
url: element.url,
|
||||
html_url: element.html_url,
|
||||
repo,
|
||||
branch
|
||||
};
|
||||
}
|
||||
|
||||
private static isSamplesCall(owner: string, repo: string, branch?: string): boolean {
|
||||
return owner === "Azure-Samples" && repo === "cosmos-notebooks" && (!branch || branch === "master");
|
||||
}
|
||||
}
|
||||
30
src/GitHub/GitHubConnector.ts
Normal file
30
src/GitHub/GitHubConnector.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export interface IGitHubConnectorParams {
|
||||
state: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export const GitHubConnectorMsgType = "GitHubConnectorMsgType";
|
||||
|
||||
export class GitHubConnector {
|
||||
public start(params: URLSearchParams, window: Window & typeof globalThis) {
|
||||
window.postMessage(
|
||||
{
|
||||
type: GitHubConnectorMsgType,
|
||||
data: {
|
||||
state: params.get("state"),
|
||||
code: params.get("code")
|
||||
} as IGitHubConnectorParams
|
||||
},
|
||||
window.location.origin
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var connector = new GitHubConnector();
|
||||
window.addEventListener("load", () => {
|
||||
const openerWindow = window.opener;
|
||||
if (openerWindow) {
|
||||
connector.start(new URLSearchParams(document.location.search), openerWindow);
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
312
src/GitHub/GitHubContentProvider.test.ts
Normal file
312
src/GitHub/GitHubContentProvider.test.ts
Normal file
@@ -0,0 +1,312 @@
|
||||
import { IContent } from "@nteract/core";
|
||||
import { fixture } from "@nteract/fixtures";
|
||||
import { HttpStatusCodes } from "../Common/Constants";
|
||||
import { GitHubClient, IGitHubCommit, IGitHubFile } from "./GitHubClient";
|
||||
import { GitHubContentProvider } from "./GitHubContentProvider";
|
||||
|
||||
const gitHubClient = new GitHubClient("token", () => {});
|
||||
const gitHubContentProvider = new GitHubContentProvider({
|
||||
gitHubClient,
|
||||
promptForCommitMsg: () => Promise.resolve("commit msg")
|
||||
});
|
||||
const sampleGitHubUri = `https://github.com/login/repo/blob/branch/dir/name.ipynb`;
|
||||
const sampleFile: IGitHubFile = {
|
||||
type: "file",
|
||||
encoding: "encoding",
|
||||
size: 0,
|
||||
name: "name.ipynb",
|
||||
path: "dir/name.ipynb",
|
||||
content: btoa(fixture),
|
||||
sha: "sha",
|
||||
url: "url",
|
||||
html_url: sampleGitHubUri,
|
||||
repo: {
|
||||
owner: {
|
||||
login: "login"
|
||||
},
|
||||
name: "repo",
|
||||
private: false
|
||||
},
|
||||
branch: {
|
||||
name: "branch"
|
||||
}
|
||||
};
|
||||
const sampleNotebookModel: IContent<"notebook"> = {
|
||||
name: sampleFile.name,
|
||||
path: sampleFile.html_url,
|
||||
type: "notebook",
|
||||
writable: true,
|
||||
created: "",
|
||||
last_modified: "date",
|
||||
mimetype: "application/x-ipynb+json",
|
||||
content: sampleFile.content ? JSON.parse(atob(sampleFile.content)) : null,
|
||||
format: "json"
|
||||
};
|
||||
const gitHubCommit: IGitHubCommit = {
|
||||
sha: "sha",
|
||||
message: "message",
|
||||
committer: {
|
||||
date: "date"
|
||||
}
|
||||
};
|
||||
|
||||
describe("GitHubContentProvider remove", () => {
|
||||
it("errors on invalid path", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync");
|
||||
|
||||
const response = await gitHubContentProvider.remove(null, "invalid path").toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||
expect(gitHubClient.getContentsAsync).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("errors on failed read", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
||||
|
||||
const response = await gitHubContentProvider.remove(null, sampleGitHubUri).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(888);
|
||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||
});
|
||||
|
||||
it("errors on failed delete", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })
|
||||
);
|
||||
spyOn(GitHubClient.prototype, "deleteFileAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
||||
|
||||
const response = await gitHubContentProvider.remove(null, sampleGitHubUri).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(888);
|
||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||
expect(gitHubClient.deleteFileAsync).toBeCalled();
|
||||
});
|
||||
|
||||
it("removes notebook", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })
|
||||
);
|
||||
spyOn(GitHubClient.prototype, "deleteFileAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit })
|
||||
);
|
||||
|
||||
const response = await gitHubContentProvider.remove(null, sampleGitHubUri).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(HttpStatusCodes.NoContent);
|
||||
expect(gitHubClient.deleteFileAsync).toBeCalled();
|
||||
expect(response.response).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHubContentProvider get", () => {
|
||||
it("errors on invalid path", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync");
|
||||
|
||||
const response = await gitHubContentProvider.get(null, "invalid path", null).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||
expect(gitHubClient.getContentsAsync).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("errors on failed read", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
||||
|
||||
const response = await gitHubContentProvider.get(null, sampleGitHubUri, null).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(888);
|
||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||
});
|
||||
|
||||
it("reads notebook", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })
|
||||
);
|
||||
spyOn(GitHubClient.prototype, "getCommitsAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.OK, data: [gitHubCommit] })
|
||||
);
|
||||
|
||||
const response = await gitHubContentProvider.get(null, sampleGitHubUri, {}).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||
expect(response.response).toEqual(sampleNotebookModel);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHubContentProvider update", () => {
|
||||
it("errors on invalid path", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync");
|
||||
|
||||
const response = await gitHubContentProvider.update(null, "invalid path", null).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||
expect(gitHubClient.getContentsAsync).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("errors on failed read", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
||||
|
||||
const response = await gitHubContentProvider.update(null, sampleGitHubUri, null).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(888);
|
||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||
});
|
||||
|
||||
it("errors on failed rename", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })
|
||||
);
|
||||
spyOn(GitHubClient.prototype, "renameFileAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
||||
|
||||
const response = await gitHubContentProvider.update(null, sampleGitHubUri, sampleNotebookModel).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(888);
|
||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||
expect(gitHubClient.renameFileAsync).toBeCalled();
|
||||
});
|
||||
|
||||
it("updates notebook", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })
|
||||
);
|
||||
spyOn(GitHubClient.prototype, "renameFileAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit })
|
||||
);
|
||||
spyOn(GitHubClient.prototype, "getCommitsAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.OK, data: [gitHubCommit] })
|
||||
);
|
||||
|
||||
const response = await gitHubContentProvider.update(null, sampleGitHubUri, sampleNotebookModel).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||
expect(gitHubClient.renameFileAsync).toBeCalled();
|
||||
expect(response.response.type).toEqual(sampleNotebookModel.type);
|
||||
expect(response.response.name).toEqual(sampleNotebookModel.name);
|
||||
expect(response.response.path).toEqual(sampleNotebookModel.path);
|
||||
expect(response.response.content).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHubContentProvider create", () => {
|
||||
it("errors on invalid path", async () => {
|
||||
spyOn(GitHubClient.prototype, "createOrUpdateFileAsync");
|
||||
|
||||
const response = await gitHubContentProvider.create(null, "invalid path", sampleNotebookModel).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||
expect(gitHubClient.createOrUpdateFileAsync).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("errors on failed create", async () => {
|
||||
spyOn(GitHubClient.prototype, "createOrUpdateFileAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
||||
|
||||
const response = await gitHubContentProvider.create(null, sampleGitHubUri, sampleNotebookModel).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(888);
|
||||
expect(gitHubClient.createOrUpdateFileAsync).toBeCalled();
|
||||
});
|
||||
|
||||
it("creates notebook", async () => {
|
||||
spyOn(GitHubClient.prototype, "createOrUpdateFileAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.Created, data: gitHubCommit })
|
||||
);
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })
|
||||
);
|
||||
|
||||
const response = await gitHubContentProvider.create(null, sampleGitHubUri, sampleNotebookModel).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(HttpStatusCodes.Created);
|
||||
expect(gitHubClient.createOrUpdateFileAsync).toBeCalled();
|
||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||
expect(response.response.type).toEqual(sampleNotebookModel.type);
|
||||
expect(response.response.name).toEqual(sampleNotebookModel.name);
|
||||
expect(response.response.path).toEqual(sampleNotebookModel.path);
|
||||
expect(response.response.content).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHubContentProvider save", () => {
|
||||
it("errors on invalid path", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync");
|
||||
|
||||
const response = await gitHubContentProvider.save(null, "invalid path", null).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||
expect(gitHubClient.getContentsAsync).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("errors on failed read", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
||||
|
||||
const response = await gitHubContentProvider.save(null, sampleGitHubUri, null).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(888);
|
||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||
});
|
||||
|
||||
it("errors on failed update", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })
|
||||
);
|
||||
spyOn(GitHubClient.prototype, "createOrUpdateFileAsync").and.returnValue(Promise.resolve({ status: 888 }));
|
||||
|
||||
const response = await gitHubContentProvider.save(null, sampleGitHubUri, sampleNotebookModel).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(888);
|
||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||
expect(gitHubClient.createOrUpdateFileAsync).toBeCalled();
|
||||
});
|
||||
|
||||
it("saves notebook", async () => {
|
||||
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })
|
||||
);
|
||||
spyOn(GitHubClient.prototype, "createOrUpdateFileAsync").and.returnValue(
|
||||
Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit })
|
||||
);
|
||||
|
||||
const response = await gitHubContentProvider.save(null, sampleGitHubUri, sampleNotebookModel).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||
expect(gitHubClient.getContentsAsync).toBeCalled();
|
||||
expect(gitHubClient.createOrUpdateFileAsync).toBeCalled();
|
||||
expect(response.response.type).toEqual(sampleNotebookModel.type);
|
||||
expect(response.response.name).toEqual(sampleNotebookModel.name);
|
||||
expect(response.response.path).toEqual(sampleNotebookModel.path);
|
||||
expect(response.response.content).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHubContentProvider listCheckpoints", () => {
|
||||
it("errors for everything", async () => {
|
||||
const response = await gitHubContentProvider.listCheckpoints(null, null).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHubContentProvider createCheckpoint", () => {
|
||||
it("errors for everything", async () => {
|
||||
const response = await gitHubContentProvider.createCheckpoint(null, null).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHubContentProvider deleteCheckpoint", () => {
|
||||
it("errors for everything", async () => {
|
||||
const response = await gitHubContentProvider.deleteCheckpoint(null, null, null).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHubContentProvider restoreFromCheckpoint", () => {
|
||||
it("errors for everything", async () => {
|
||||
const response = await gitHubContentProvider.restoreFromCheckpoint(null, null, null).toPromise();
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toBe(GitHubContentProvider.SelfErrorCode);
|
||||
});
|
||||
});
|
||||
406
src/GitHub/GitHubContentProvider.ts
Normal file
406
src/GitHub/GitHubContentProvider.ts
Normal file
@@ -0,0 +1,406 @@
|
||||
import { Notebook, stringifyNotebook, makeNotebookRecord, toJS } from "@nteract/commutable";
|
||||
import { FileType, IContent, IContentProvider, IEmptyContent, IGetParams, ServerConfig } from "@nteract/core";
|
||||
import { from, Observable, of } from "rxjs";
|
||||
import { AjaxResponse } from "rxjs/ajax";
|
||||
import { HttpStatusCodes } from "../Common/Constants";
|
||||
import { Logger } from "../Common/Logger";
|
||||
import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil";
|
||||
import { GitHubClient, IGitHubFile, IGitHubResponse, IGitHubCommit } from "./GitHubClient";
|
||||
import { GitHubUtils } from "../Utils/GitHubUtils";
|
||||
|
||||
export interface GitHubContentProviderParams {
|
||||
gitHubClient: GitHubClient;
|
||||
promptForCommitMsg: (title: string, primaryButtonLabel: string) => Promise<string>;
|
||||
}
|
||||
|
||||
class GitHubContentProviderError extends Error {
|
||||
constructor(error: string, public errno: number = GitHubContentProvider.SelfErrorCode) {
|
||||
super(error);
|
||||
}
|
||||
}
|
||||
|
||||
// Provides 'contents' API for GitHub
|
||||
// http://jupyter-api.surge.sh/#!/contents
|
||||
export class GitHubContentProvider implements IContentProvider {
|
||||
public static readonly SelfErrorCode = 555;
|
||||
|
||||
constructor(private params: GitHubContentProviderParams) {}
|
||||
|
||||
public remove(_: ServerConfig, uri: string): Observable<AjaxResponse> {
|
||||
return from(
|
||||
this.getContent(uri).then(async (content: IGitHubResponse<IGitHubFile | IGitHubFile[]>) => {
|
||||
try {
|
||||
const commitMsg = await this.validateContentAndGetCommitMsg(content, "Delete", "Delete");
|
||||
const response = await this.params.gitHubClient.deleteFileAsync(content.data as IGitHubFile, commitMsg);
|
||||
if (response.status !== HttpStatusCodes.OK) {
|
||||
throw new GitHubContentProviderError("Failed to delete", response.status);
|
||||
}
|
||||
|
||||
return this.createSuccessAjaxResponse(HttpStatusCodes.NoContent, undefined);
|
||||
} catch (error) {
|
||||
Logger.logError(error, "GitHubContentProvider/remove", error.errno);
|
||||
return this.createErrorAjaxResponse(error);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public get(_: ServerConfig, uri: string, params: Partial<IGetParams>): Observable<AjaxResponse> {
|
||||
return from(
|
||||
this.getContent(uri).then(async (content: IGitHubResponse<IGitHubFile | IGitHubFile[]>) => {
|
||||
try {
|
||||
if (content.status !== HttpStatusCodes.OK) {
|
||||
throw new GitHubContentProviderError("Failed to get content", content.status);
|
||||
}
|
||||
|
||||
const gitHubInfo = GitHubUtils.fromGitHubUri(uri);
|
||||
const commitResponse = await this.params.gitHubClient.getCommitsAsync(
|
||||
gitHubInfo.owner,
|
||||
gitHubInfo.repo,
|
||||
gitHubInfo.branch,
|
||||
gitHubInfo.path,
|
||||
1,
|
||||
1
|
||||
);
|
||||
if (commitResponse.status !== HttpStatusCodes.OK) {
|
||||
throw new GitHubContentProviderError("Failed to get commit", commitResponse.status);
|
||||
}
|
||||
|
||||
return this.createSuccessAjaxResponse(
|
||||
HttpStatusCodes.OK,
|
||||
this.createContentModel(uri, content.data, commitResponse.data[0], params)
|
||||
);
|
||||
} catch (error) {
|
||||
Logger.logError(error, "GitHubContentProvider/get", error.errno);
|
||||
return this.createErrorAjaxResponse(error);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public update<FT extends FileType>(
|
||||
_: ServerConfig,
|
||||
uri: string,
|
||||
model: Partial<IContent<FT>>
|
||||
): Observable<AjaxResponse> {
|
||||
return from(
|
||||
this.getContent(uri).then(async (content: IGitHubResponse<IGitHubFile | IGitHubFile[]>) => {
|
||||
try {
|
||||
const gitHubFile = content.data as IGitHubFile;
|
||||
const commitMsg = await this.validateContentAndGetCommitMsg(content, "Rename", "Rename");
|
||||
const newUri = model.path;
|
||||
const response = await this.params.gitHubClient.renameFileAsync(
|
||||
gitHubFile.repo.owner.login,
|
||||
gitHubFile.repo.name,
|
||||
gitHubFile.branch.name,
|
||||
commitMsg,
|
||||
gitHubFile.path,
|
||||
GitHubUtils.fromGitHubUri(newUri).path
|
||||
);
|
||||
if (response.status !== HttpStatusCodes.OK) {
|
||||
throw new GitHubContentProviderError("Failed to rename", response.status);
|
||||
}
|
||||
|
||||
const updatedContentResponse = await this.getContent(model.path);
|
||||
if (updatedContentResponse.status !== HttpStatusCodes.OK) {
|
||||
throw new GitHubContentProviderError("Failed to get content after renaming", updatedContentResponse.status);
|
||||
}
|
||||
|
||||
return this.createSuccessAjaxResponse(
|
||||
HttpStatusCodes.OK,
|
||||
this.createContentModel(newUri, updatedContentResponse.data, response.data, { content: 0 })
|
||||
);
|
||||
} catch (error) {
|
||||
Logger.logError(error, "GitHubContentProvider/update", error.errno);
|
||||
return this.createErrorAjaxResponse(error);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public create<FT extends FileType>(
|
||||
_: ServerConfig,
|
||||
uri: string,
|
||||
model: Partial<IContent<FT>> & { type: FT }
|
||||
): Observable<AjaxResponse> {
|
||||
return from(
|
||||
this.params.promptForCommitMsg("Create New Notebook", "Create").then(async (commitMsg: string) => {
|
||||
try {
|
||||
if (!commitMsg) {
|
||||
throw new GitHubContentProviderError("Couldn't get a commit message");
|
||||
}
|
||||
|
||||
if (model.type !== "notebook") {
|
||||
throw new GitHubContentProviderError("Unsupported content type");
|
||||
}
|
||||
|
||||
const gitHubInfo = GitHubUtils.fromGitHubUri(uri);
|
||||
if (!gitHubInfo) {
|
||||
throw new GitHubContentProviderError(`Failed to parse ${uri}`);
|
||||
}
|
||||
|
||||
const content = btoa(stringifyNotebook(toJS(makeNotebookRecord())));
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric",
|
||||
hour12: false
|
||||
};
|
||||
const name = `Untitled-${new Date().toLocaleString("default", options)}.ipynb`;
|
||||
let path = name;
|
||||
if (gitHubInfo.path) {
|
||||
path = `${gitHubInfo.path}/${name}`;
|
||||
}
|
||||
|
||||
const response = await this.params.gitHubClient.createOrUpdateFileAsync(
|
||||
gitHubInfo.owner,
|
||||
gitHubInfo.repo,
|
||||
gitHubInfo.branch,
|
||||
path,
|
||||
commitMsg,
|
||||
content
|
||||
);
|
||||
if (response.status !== HttpStatusCodes.Created) {
|
||||
throw new GitHubContentProviderError("Failed to create", response.status);
|
||||
}
|
||||
|
||||
const newUri = GitHubUtils.toGitHubUriForRepoAndBranch(
|
||||
gitHubInfo.owner,
|
||||
gitHubInfo.repo,
|
||||
gitHubInfo.branch,
|
||||
path
|
||||
);
|
||||
const newContentResponse = await this.getContent(newUri);
|
||||
if (newContentResponse.status !== HttpStatusCodes.OK) {
|
||||
throw new GitHubContentProviderError("Failed to get content after creating", newContentResponse.status);
|
||||
}
|
||||
|
||||
return this.createSuccessAjaxResponse(
|
||||
HttpStatusCodes.Created,
|
||||
this.createContentModel(newUri, newContentResponse.data, response.data, { content: 0 })
|
||||
);
|
||||
} catch (error) {
|
||||
Logger.logError(error, "GitHubContentProvider/create", error.errno);
|
||||
return this.createErrorAjaxResponse(error);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public save<FT extends FileType>(
|
||||
_: ServerConfig,
|
||||
uri: string,
|
||||
model: Partial<IContent<FT>>
|
||||
): Observable<AjaxResponse> {
|
||||
return from(
|
||||
this.getContent(uri).then(async (content: IGitHubResponse<IGitHubFile | IGitHubFile[]>) => {
|
||||
try {
|
||||
const commitMsg = await this.validateContentAndGetCommitMsg(content, "Save", "Save");
|
||||
let updatedContent: string;
|
||||
if (model.type === "notebook") {
|
||||
updatedContent = btoa(stringifyNotebook(model.content as Notebook));
|
||||
} else if (model.type === "file") {
|
||||
updatedContent = model.content as string;
|
||||
if (model.format !== "base64") {
|
||||
updatedContent = btoa(updatedContent);
|
||||
}
|
||||
} else {
|
||||
throw new GitHubContentProviderError("Unsupported content type");
|
||||
}
|
||||
|
||||
const gitHubFile = content.data as IGitHubFile;
|
||||
const response = await this.params.gitHubClient.createOrUpdateFileAsync(
|
||||
gitHubFile.repo.owner.login,
|
||||
gitHubFile.repo.name,
|
||||
gitHubFile.branch.name,
|
||||
gitHubFile.path,
|
||||
commitMsg,
|
||||
updatedContent,
|
||||
gitHubFile.sha
|
||||
);
|
||||
if (response.status !== HttpStatusCodes.OK) {
|
||||
throw new GitHubContentProviderError("Failed to update", response.status);
|
||||
}
|
||||
|
||||
const savedContentResponse = await this.getContent(uri);
|
||||
if (savedContentResponse.status !== HttpStatusCodes.OK) {
|
||||
throw new GitHubContentProviderError("Failed to get content after updating", savedContentResponse.status);
|
||||
}
|
||||
|
||||
return this.createSuccessAjaxResponse(
|
||||
HttpStatusCodes.OK,
|
||||
this.createContentModel(uri, savedContentResponse.data, response.data, { content: 0 })
|
||||
);
|
||||
} catch (error) {
|
||||
Logger.logError(error, "GitHubContentProvider/update", error.errno);
|
||||
return this.createErrorAjaxResponse(error);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public listCheckpoints(_: ServerConfig, path: string): Observable<AjaxResponse> {
|
||||
const error = new GitHubContentProviderError("Not implemented");
|
||||
Logger.logError(error, "GitHubContentProvider/listCheckpoints", error.errno);
|
||||
return of(this.createErrorAjaxResponse(error));
|
||||
}
|
||||
|
||||
public createCheckpoint(_: ServerConfig, path: string): Observable<AjaxResponse> {
|
||||
const error = new GitHubContentProviderError("Not implemented");
|
||||
Logger.logError(error, "GitHubContentProvider/createCheckpoint", error.errno);
|
||||
return of(this.createErrorAjaxResponse(error));
|
||||
}
|
||||
|
||||
public deleteCheckpoint(_: ServerConfig, path: string, checkpointID: string): Observable<AjaxResponse> {
|
||||
const error = new GitHubContentProviderError("Not implemented");
|
||||
Logger.logError(error, "GitHubContentProvider/deleteCheckpoint", error.errno);
|
||||
return of(this.createErrorAjaxResponse(error));
|
||||
}
|
||||
|
||||
public restoreFromCheckpoint(_: ServerConfig, path: string, checkpointID: string): Observable<AjaxResponse> {
|
||||
const error = new GitHubContentProviderError("Not implemented");
|
||||
Logger.logError(error, "GitHubContentProvider/restoreFromCheckpoint", error.errno);
|
||||
return of(this.createErrorAjaxResponse(error));
|
||||
}
|
||||
|
||||
private async validateContentAndGetCommitMsg(
|
||||
content: IGitHubResponse<IGitHubFile | IGitHubFile[]>,
|
||||
promptTitle: string,
|
||||
promptPrimaryButtonLabel: string
|
||||
): Promise<string> {
|
||||
if (content.status !== HttpStatusCodes.OK) {
|
||||
throw new GitHubContentProviderError("Failed to get content", content.status);
|
||||
}
|
||||
|
||||
if (Array.isArray(content.data)) {
|
||||
throw new GitHubContentProviderError("Operation not supported for collections");
|
||||
}
|
||||
|
||||
const commitMsg = await this.params.promptForCommitMsg(promptTitle, promptPrimaryButtonLabel);
|
||||
if (!commitMsg) {
|
||||
throw new GitHubContentProviderError("Couldn't get a commit message");
|
||||
}
|
||||
|
||||
return commitMsg;
|
||||
}
|
||||
|
||||
private getContent(uri: string): Promise<IGitHubResponse<IGitHubFile | IGitHubFile[]>> {
|
||||
const gitHubInfo = GitHubUtils.fromGitHubUri(uri);
|
||||
if (gitHubInfo) {
|
||||
const { owner, repo, branch, path } = gitHubInfo;
|
||||
return this.params.gitHubClient.getContentsAsync(owner, repo, branch, path);
|
||||
}
|
||||
|
||||
return Promise.resolve({ status: GitHubContentProvider.SelfErrorCode, data: undefined });
|
||||
}
|
||||
|
||||
private createContentModel(
|
||||
uri: string,
|
||||
content: IGitHubFile | IGitHubFile[],
|
||||
commit: IGitHubCommit,
|
||||
params: Partial<IGetParams>
|
||||
): IContent<FileType> {
|
||||
if (Array.isArray(content)) {
|
||||
return this.createDirectoryModel(uri, content, commit);
|
||||
}
|
||||
|
||||
if (content.type !== "file") {
|
||||
return this.createDirectoryModel(uri, undefined, commit);
|
||||
}
|
||||
|
||||
if (NotebookUtil.isNotebookFile(uri)) {
|
||||
return this.createNotebookModel(content, commit, params);
|
||||
}
|
||||
|
||||
return this.createFileModel(content, commit, params);
|
||||
}
|
||||
|
||||
private createDirectoryModel(
|
||||
uri: string,
|
||||
gitHubFiles: IGitHubFile[] | undefined,
|
||||
commit: IGitHubCommit
|
||||
): IContent<"directory"> {
|
||||
return {
|
||||
name: NotebookUtil.getContentName(uri),
|
||||
path: uri,
|
||||
type: "directory",
|
||||
writable: true, // TODO: tamitta: we don't know this info here
|
||||
created: "", // TODO: tamitta: we don't know this info here
|
||||
last_modified: commit.committer.date,
|
||||
mimetype: undefined,
|
||||
content: gitHubFiles?.map(
|
||||
(file: IGitHubFile) =>
|
||||
this.createContentModel(GitHubUtils.toGitHubUriForFile(file), file, commit, {
|
||||
content: 0
|
||||
}) as IEmptyContent<FileType>
|
||||
),
|
||||
format: "json"
|
||||
};
|
||||
}
|
||||
|
||||
private createNotebookModel(
|
||||
gitHubFile: IGitHubFile,
|
||||
commit: IGitHubCommit,
|
||||
params: Partial<IGetParams>
|
||||
): IContent<"notebook"> {
|
||||
const content: Notebook =
|
||||
gitHubFile.content && params.content !== 0 ? JSON.parse(atob(gitHubFile.content)) : undefined;
|
||||
return {
|
||||
name: gitHubFile.name,
|
||||
path: GitHubUtils.toGitHubUriForFile(gitHubFile),
|
||||
type: "notebook",
|
||||
writable: true, // TODO: tamitta: we don't know this info here
|
||||
created: "", // TODO: tamitta: we don't know this info here
|
||||
last_modified: commit.committer.date,
|
||||
mimetype: content ? "application/x-ipynb+json" : undefined,
|
||||
content,
|
||||
format: content ? "json" : undefined
|
||||
};
|
||||
}
|
||||
|
||||
private createFileModel(
|
||||
gitHubFile: IGitHubFile,
|
||||
commit: IGitHubCommit,
|
||||
params: Partial<IGetParams>
|
||||
): IContent<"file"> {
|
||||
const content: string = gitHubFile.content && params.content !== 0 ? atob(gitHubFile.content) : undefined;
|
||||
return {
|
||||
name: gitHubFile.name,
|
||||
path: GitHubUtils.toGitHubUriForFile(gitHubFile),
|
||||
type: "file",
|
||||
writable: true, // TODO: tamitta: we don't know this info here
|
||||
created: "", // TODO: tamitta: we don't know this info here
|
||||
last_modified: commit.committer.date,
|
||||
mimetype: content ? "text/plain" : undefined,
|
||||
content,
|
||||
format: content ? "text" : undefined
|
||||
};
|
||||
}
|
||||
|
||||
private createSuccessAjaxResponse(status: number, content: IContent<FileType>): AjaxResponse {
|
||||
return {
|
||||
originalEvent: new Event("no-op"),
|
||||
xhr: new XMLHttpRequest(),
|
||||
request: {},
|
||||
status,
|
||||
response: content ? content : undefined,
|
||||
responseText: content ? JSON.stringify(content) : undefined,
|
||||
responseType: "json"
|
||||
};
|
||||
}
|
||||
|
||||
private createErrorAjaxResponse(error: GitHubContentProviderError): AjaxResponse {
|
||||
return {
|
||||
originalEvent: new Event("no-op"),
|
||||
xhr: new XMLHttpRequest(),
|
||||
request: {},
|
||||
status: error.errno,
|
||||
response: error,
|
||||
responseText: JSON.stringify(error),
|
||||
responseType: "json"
|
||||
};
|
||||
}
|
||||
}
|
||||
162
src/GitHub/GitHubOAuthService.test.ts
Normal file
162
src/GitHub/GitHubOAuthService.test.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import ko from "knockout";
|
||||
import { HttpStatusCodes } from "../Common/Constants";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { JunoClient } from "../Juno/JunoClient";
|
||||
import { GitHubConnector, IGitHubConnectorParams } from "./GitHubConnector";
|
||||
import { GitHubOAuthService } from "./GitHubOAuthService";
|
||||
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
|
||||
const sampleDatabaseAccount: ViewModels.DatabaseAccount = {
|
||||
id: "id",
|
||||
name: "name",
|
||||
location: "location",
|
||||
type: "type",
|
||||
kind: "kind",
|
||||
tags: [],
|
||||
properties: {
|
||||
documentEndpoint: "documentEndpoint",
|
||||
gremlinEndpoint: "gremlinEndpoint",
|
||||
tableEndpoint: "tableEndpoint",
|
||||
cassandraEndpoint: "cassandraEndpoint"
|
||||
}
|
||||
};
|
||||
|
||||
describe("GitHubOAuthService", () => {
|
||||
let junoClient: JunoClient;
|
||||
let gitHubOAuthService: GitHubOAuthService;
|
||||
let originalDataExplorer: ViewModels.Explorer;
|
||||
|
||||
beforeEach(() => {
|
||||
junoClient = new JunoClient(ko.observable<ViewModels.DatabaseAccount>(sampleDatabaseAccount));
|
||||
gitHubOAuthService = new GitHubOAuthService(junoClient);
|
||||
originalDataExplorer = window.dataExplorer;
|
||||
window.dataExplorer = {
|
||||
...originalDataExplorer,
|
||||
gitHubOAuthService,
|
||||
logConsoleData: (data): void =>
|
||||
data.type === ConsoleDataType.Error ? console.error(data.message) : console.log(data.message)
|
||||
} as ViewModels.Explorer;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
window.dataExplorer = originalDataExplorer;
|
||||
originalDataExplorer = undefined;
|
||||
gitHubOAuthService = undefined;
|
||||
junoClient = undefined;
|
||||
});
|
||||
|
||||
it("logout deletes app authorization and resets token", async () => {
|
||||
const deleteAppAuthorizationCallback = jest.fn().mockReturnValue({ status: HttpStatusCodes.NoContent });
|
||||
junoClient.deleteAppAuthorization = deleteAppAuthorizationCallback;
|
||||
|
||||
await gitHubOAuthService.logout();
|
||||
expect(deleteAppAuthorizationCallback).toBeCalled();
|
||||
expect(gitHubOAuthService.getTokenObservable()()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("resetToken resets token", () => {
|
||||
gitHubOAuthService.resetToken();
|
||||
expect(gitHubOAuthService.getTokenObservable()()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("startOAuth resets OAuth state", () => {
|
||||
let url: string;
|
||||
const windowOpenCallback = jest.fn().mockImplementation((value: string) => {
|
||||
url = value;
|
||||
});
|
||||
window.open = windowOpenCallback;
|
||||
|
||||
gitHubOAuthService.startOAuth("scope");
|
||||
expect(windowOpenCallback).toBeCalled();
|
||||
|
||||
const initialParams = new URLSearchParams(new URL(url).search);
|
||||
expect(initialParams.get("state")).toBeDefined();
|
||||
|
||||
gitHubOAuthService.startOAuth("another scope");
|
||||
expect(windowOpenCallback).toBeCalled();
|
||||
|
||||
const newParams = new URLSearchParams(new URL(url).search);
|
||||
expect(newParams.get("state")).toBeDefined();
|
||||
expect(newParams.get("state")).not.toEqual(initialParams.get("state"));
|
||||
});
|
||||
|
||||
it("finishOAuth is called whenever GitHubConnector is started", async () => {
|
||||
const finishOAuthCallback = jest.fn().mockImplementation();
|
||||
gitHubOAuthService.finishOAuth = finishOAuthCallback;
|
||||
|
||||
const params: IGitHubConnectorParams = {
|
||||
state: "state",
|
||||
code: "code"
|
||||
};
|
||||
const searchParams = new URLSearchParams({ ...params });
|
||||
|
||||
const gitHubConnector = new GitHubConnector();
|
||||
gitHubConnector.start(searchParams, window);
|
||||
|
||||
// GitHubConnector uses Window.postMessage and there's no good way to know when the message has received
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
expect(finishOAuthCallback).toBeCalledWith(params);
|
||||
});
|
||||
|
||||
it("finishOAuth updates token", async () => {
|
||||
const data = { key: "value" };
|
||||
const getGitHubTokenCallback = jest.fn().mockReturnValue({ status: HttpStatusCodes.OK, data });
|
||||
junoClient.getGitHubToken = getGitHubTokenCallback;
|
||||
|
||||
const initialToken = gitHubOAuthService.getTokenObservable()();
|
||||
const state = gitHubOAuthService.startOAuth("scope");
|
||||
|
||||
const params: IGitHubConnectorParams = {
|
||||
state,
|
||||
code: "code"
|
||||
};
|
||||
await gitHubOAuthService.finishOAuth(params);
|
||||
const updatedToken = gitHubOAuthService.getTokenObservable()();
|
||||
|
||||
expect(getGitHubTokenCallback).toBeCalledWith("code");
|
||||
expect(initialToken).not.toEqual(updatedToken);
|
||||
});
|
||||
|
||||
it("finishOAuth updates token to error if state doesn't match", async () => {
|
||||
gitHubOAuthService.startOAuth("scope");
|
||||
|
||||
const params: IGitHubConnectorParams = {
|
||||
state: "state",
|
||||
code: "code"
|
||||
};
|
||||
await gitHubOAuthService.finishOAuth(params);
|
||||
|
||||
expect(gitHubOAuthService.getTokenObservable()().error).toBeDefined();
|
||||
});
|
||||
|
||||
it("finishOAuth updates token to error if unable to fetch token", async () => {
|
||||
const getGitHubTokenCallback = jest.fn().mockReturnValue({ status: HttpStatusCodes.NotFound });
|
||||
junoClient.getGitHubToken = getGitHubTokenCallback;
|
||||
|
||||
const state = gitHubOAuthService.startOAuth("scope");
|
||||
|
||||
const params: IGitHubConnectorParams = {
|
||||
state,
|
||||
code: "code"
|
||||
};
|
||||
await gitHubOAuthService.finishOAuth(params);
|
||||
|
||||
expect(getGitHubTokenCallback).toBeCalledWith("code");
|
||||
expect(gitHubOAuthService.getTokenObservable()().error).toBeDefined();
|
||||
});
|
||||
|
||||
it("isLoggedIn returns false if resetToken is called", () => {
|
||||
gitHubOAuthService.resetToken();
|
||||
expect(gitHubOAuthService.isLoggedIn()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("isLoggedIn returns false if logout is called", async () => {
|
||||
const deleteAppAuthorizationCallback = jest.fn().mockReturnValue({ status: HttpStatusCodes.NoContent });
|
||||
junoClient.deleteAppAuthorization = deleteAppAuthorizationCallback;
|
||||
|
||||
await gitHubOAuthService.logout();
|
||||
expect(gitHubOAuthService.isLoggedIn()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
112
src/GitHub/GitHubOAuthService.ts
Normal file
112
src/GitHub/GitHubOAuthService.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import ko from "knockout";
|
||||
import { HttpStatusCodes } from "../Common/Constants";
|
||||
import { Logger } from "../Common/Logger";
|
||||
import { config } from "../Config";
|
||||
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { JunoClient } from "../Juno/JunoClient";
|
||||
import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
|
||||
import { NotificationConsoleUtils } from "../Utils/NotificationConsoleUtils";
|
||||
import { GitHubConnectorMsgType, IGitHubConnectorParams } from "./GitHubConnector";
|
||||
|
||||
window.addEventListener("message", (event: MessageEvent) => {
|
||||
if (isInvalidParentFrameOrigin(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const msg = event.data;
|
||||
if (msg.type === GitHubConnectorMsgType) {
|
||||
const params = msg.data as IGitHubConnectorParams;
|
||||
window.dataExplorer.gitHubOAuthService.finishOAuth(params);
|
||||
}
|
||||
});
|
||||
|
||||
export interface IGitHubOAuthToken {
|
||||
// API properties
|
||||
access_token?: string;
|
||||
scope?: string;
|
||||
token_type?: string;
|
||||
error?: string;
|
||||
error_description?: string;
|
||||
}
|
||||
|
||||
export class GitHubOAuthService {
|
||||
private static readonly OAuthEndpoint = "https://github.com/login/oauth/authorize";
|
||||
|
||||
private state: string;
|
||||
private token: ko.Observable<IGitHubOAuthToken>;
|
||||
|
||||
constructor(private junoClient: JunoClient) {
|
||||
this.token = ko.observable<IGitHubOAuthToken>();
|
||||
}
|
||||
|
||||
public startOAuth(scope: string): string {
|
||||
const params = {
|
||||
scope,
|
||||
client_id: config.GITHUB_CLIENT_ID,
|
||||
redirect_uri: new URL("./connectToGitHub.html", window.location.href).href,
|
||||
state: this.resetState()
|
||||
};
|
||||
|
||||
window.open(`${GitHubOAuthService.OAuthEndpoint}?${new URLSearchParams(params).toString()}`);
|
||||
return params.state;
|
||||
}
|
||||
|
||||
public async finishOAuth(params: IGitHubConnectorParams) {
|
||||
try {
|
||||
this.validateState(params.state);
|
||||
const response = await this.junoClient.getGitHubToken(params.code);
|
||||
|
||||
if (response.status === HttpStatusCodes.OK && !response.data.error) {
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Successfully connected to GitHub");
|
||||
this.token(response.data);
|
||||
} else {
|
||||
let errorMsg = response.data.error;
|
||||
if (response.data.error_description) {
|
||||
errorMsg = `${errorMsg}: ${response.data.error_description}`;
|
||||
}
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Failed to connect to GitHub: ${error}`);
|
||||
this.token({ error });
|
||||
}
|
||||
}
|
||||
|
||||
public getTokenObservable(): ko.Observable<IGitHubOAuthToken> {
|
||||
return this.token;
|
||||
}
|
||||
|
||||
public async logout() {
|
||||
try {
|
||||
const response = await this.junoClient.deleteAppAuthorization(this.token()?.access_token);
|
||||
if (response.status !== HttpStatusCodes.NoContent) {
|
||||
throw new Error(`Received HTTP ${response.status}: ${response.data} when deleting app authorization`);
|
||||
}
|
||||
|
||||
this.resetToken();
|
||||
} catch (error) {
|
||||
const message = `Failed to delete app authorization: ${error}`;
|
||||
Logger.logError(message, "GitHubOAuthService/logout");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||
}
|
||||
}
|
||||
|
||||
public isLoggedIn(): boolean {
|
||||
return !!this.token()?.access_token;
|
||||
}
|
||||
|
||||
private resetState(): string {
|
||||
this.state = Math.floor(Math.random() * Math.floor(Number.MAX_SAFE_INTEGER)).toString();
|
||||
return this.state;
|
||||
}
|
||||
|
||||
public resetToken() {
|
||||
this.token(undefined);
|
||||
}
|
||||
|
||||
private validateState(state: string) {
|
||||
if (state !== this.state) {
|
||||
throw new Error("State didn't match. Possibility of cross-site request forgery attack.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user