mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-08-16 00:43:48 +01:00
* Mongo Proxy backend API * merge main into current * allow mongo proxy endpoints to be constants * allow mongo proxy endpoints to be constants * fix test * check for allowed mongo proxy endpoint * check for allowed mongo proxy endpoint --------- Co-authored-by: Asier Isayas <aisayas@microsoft.com>
537 lines
14 KiB
TypeScript
537 lines
14 KiB
TypeScript
import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointUtils";
|
|
import { GetGithubClientId } from "Utils/GitHubUtils";
|
|
import ko from "knockout";
|
|
import { HttpHeaders, HttpStatusCodes } from "../Common/Constants";
|
|
import { configContext } from "../ConfigContext";
|
|
import * as DataModels from "../Contracts/DataModels";
|
|
import { AuthorizeAccessComponent } from "../Explorer/Controls/GitHub/AuthorizeAccessComponent";
|
|
import { IGitHubResponse } from "../GitHub/GitHubClient";
|
|
import { IGitHubOAuthToken } from "../GitHub/GitHubOAuthService";
|
|
import { userContext } from "../UserContext";
|
|
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
|
|
|
export interface IJunoResponse<T> {
|
|
status: number;
|
|
data: T;
|
|
}
|
|
|
|
export interface IPinnedRepo {
|
|
owner: string;
|
|
name: string;
|
|
private: boolean;
|
|
branches: IPinnedBranch[];
|
|
}
|
|
|
|
export interface IPinnedBranch {
|
|
name: string;
|
|
}
|
|
|
|
export interface IGalleryItem {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
gitSha: string;
|
|
tags: string[];
|
|
author: string;
|
|
thumbnailUrl: string;
|
|
created: string;
|
|
isSample: boolean;
|
|
downloads: number;
|
|
favorites: number;
|
|
views: number;
|
|
newCellId: string;
|
|
policyViolations: string[];
|
|
pendingScanJobIds: string[];
|
|
}
|
|
|
|
export interface IPublicGalleryData {
|
|
metadata: IPublicGalleryMetaData;
|
|
notebooksData: IGalleryItem[];
|
|
}
|
|
|
|
export interface IPublicGalleryMetaData {
|
|
acceptedCodeOfConduct: boolean;
|
|
}
|
|
|
|
export interface IUserGallery {
|
|
favorites: string[];
|
|
published: string[];
|
|
}
|
|
|
|
// Only exported for unit test
|
|
export interface IPublishNotebookRequest {
|
|
name: string;
|
|
description: string;
|
|
tags: string[];
|
|
thumbnailUrl: string;
|
|
content: unknown;
|
|
addLinkToNotebookViewer: boolean;
|
|
}
|
|
|
|
export class JunoClient {
|
|
private cachedPinnedRepos: ko.Observable<IPinnedRepo[]>;
|
|
|
|
constructor() {
|
|
this.cachedPinnedRepos = ko.observable<IPinnedRepo[]>([]);
|
|
}
|
|
|
|
public subscribeToPinnedRepos(callback: ko.SubscriptionCallback<IPinnedRepo[], void>): ko.Subscription {
|
|
return this.cachedPinnedRepos.subscribe(callback);
|
|
}
|
|
|
|
public async getPinnedRepos(scope: string): Promise<IJunoResponse<IPinnedRepo[]>> {
|
|
const response = await window.fetch(`${this.getNotebooksSubscriptionIdAccountUrl()}/github/pinnedrepos`, {
|
|
headers: JunoClient.getHeaders(),
|
|
});
|
|
|
|
let pinnedRepos: IPinnedRepo[];
|
|
if (response.status === HttpStatusCodes.OK) {
|
|
pinnedRepos = JSON.parse(await response.text());
|
|
|
|
// In case we're restricted to public only scope, we return only public repos
|
|
if (scope === AuthorizeAccessComponent.Scopes.Public.key) {
|
|
pinnedRepos = pinnedRepos.filter((repo) => !repo.private);
|
|
}
|
|
|
|
this.cachedPinnedRepos(pinnedRepos);
|
|
}
|
|
|
|
return {
|
|
status: response.status,
|
|
data: pinnedRepos,
|
|
};
|
|
}
|
|
|
|
public async updatePinnedRepos(repos: IPinnedRepo[]): Promise<IJunoResponse<undefined>> {
|
|
const response = await window.fetch(`${this.getNotebooksSubscriptionIdAccountUrl()}/github/pinnedrepos`, {
|
|
method: "PUT",
|
|
body: JSON.stringify(repos),
|
|
headers: JunoClient.getHeaders(),
|
|
});
|
|
|
|
if (response.status === HttpStatusCodes.OK) {
|
|
this.cachedPinnedRepos(repos);
|
|
}
|
|
|
|
return {
|
|
status: response.status,
|
|
data: undefined,
|
|
};
|
|
}
|
|
|
|
public async deleteGitHubInfo(): Promise<IJunoResponse<undefined>> {
|
|
const response = await window.fetch(`${this.getNotebooksSubscriptionIdAccountUrl()}/github`, {
|
|
method: "DELETE",
|
|
headers: JunoClient.getHeaders(),
|
|
});
|
|
|
|
return {
|
|
status: response.status,
|
|
data: undefined,
|
|
};
|
|
}
|
|
|
|
public async getGitHubToken(code: string): Promise<IGitHubResponse<IGitHubOAuthToken>> {
|
|
const githubParams = JunoClient.getGitHubClientParams();
|
|
githubParams.append("code", code);
|
|
|
|
const response = await window.fetch(
|
|
`${this.getNotebooksSubscriptionIdAccountUrl()}/github/token?${githubParams.toString()}`,
|
|
{
|
|
headers: JunoClient.getHeaders(),
|
|
},
|
|
);
|
|
|
|
let data: IGitHubOAuthToken;
|
|
const body = await response.text();
|
|
if (body) {
|
|
data = JSON.parse(body);
|
|
} else {
|
|
data = {
|
|
error: response.statusText,
|
|
};
|
|
}
|
|
|
|
return {
|
|
status: response.status,
|
|
data,
|
|
};
|
|
}
|
|
|
|
public async deleteAppAuthorization(token: string): Promise<IJunoResponse<string>> {
|
|
const githubParams = JunoClient.getGitHubClientParams();
|
|
githubParams.append("access_token", token);
|
|
|
|
const response = await window.fetch(
|
|
`${this.getNotebooksSubscriptionIdAccountUrl()}/github/token?${githubParams.toString()}`,
|
|
{
|
|
method: "DELETE",
|
|
headers: JunoClient.getHeaders(),
|
|
},
|
|
);
|
|
|
|
return {
|
|
status: response.status,
|
|
data: await response.text(),
|
|
};
|
|
}
|
|
|
|
public async getSampleNotebooks(): Promise<IJunoResponse<IGalleryItem[]>> {
|
|
return this.getNotebooks(`${this.getNotebooksUrl()}/gallery/samples`);
|
|
}
|
|
|
|
public async getPublicNotebooks(): Promise<IJunoResponse<IGalleryItem[]>> {
|
|
return this.getNotebooks(`${this.getNotebooksUrl()}/gallery/public`);
|
|
}
|
|
|
|
public async getPublicGalleryData(): Promise<IJunoResponse<IPublicGalleryData>> {
|
|
const url = `${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/public`;
|
|
const response = await window.fetch(url, { headers: JunoClient.getHeaders() });
|
|
|
|
let data: IPublicGalleryData;
|
|
if (response.status === HttpStatusCodes.OK) {
|
|
data = await response.json();
|
|
}
|
|
|
|
return {
|
|
status: response.status,
|
|
data,
|
|
};
|
|
}
|
|
|
|
public async acceptCodeOfConduct(): Promise<IJunoResponse<boolean>> {
|
|
const url = `${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/acceptCodeOfConduct`;
|
|
const response = await window.fetch(url, {
|
|
method: "PATCH",
|
|
headers: JunoClient.getHeaders(),
|
|
});
|
|
|
|
let data: boolean;
|
|
if (response.status === HttpStatusCodes.OK) {
|
|
data = await response.json();
|
|
}
|
|
|
|
return {
|
|
status: response.status,
|
|
data,
|
|
};
|
|
}
|
|
|
|
public async isCodeOfConductAccepted(): Promise<IJunoResponse<boolean>> {
|
|
const url = `${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/isCodeOfConductAccepted`;
|
|
const response = await window.fetch(url, { headers: JunoClient.getHeaders() });
|
|
|
|
let data: boolean;
|
|
if (response.status === HttpStatusCodes.OK) {
|
|
data = await response.json();
|
|
}
|
|
|
|
return {
|
|
status: response.status,
|
|
data,
|
|
};
|
|
}
|
|
|
|
public async getNotebookInfo(id: string): Promise<IJunoResponse<IGalleryItem>> {
|
|
const response = await window.fetch(this.getNotebookInfoUrl(id));
|
|
|
|
let data: IGalleryItem;
|
|
if (response.status === HttpStatusCodes.OK) {
|
|
data = await response.json();
|
|
}
|
|
|
|
return {
|
|
status: response.status,
|
|
data,
|
|
};
|
|
}
|
|
|
|
public async getNotebookContent(id: string): Promise<IJunoResponse<string>> {
|
|
const response = await window.fetch(this.getNotebookContentUrl(id));
|
|
|
|
let data: string;
|
|
if (response.status === HttpStatusCodes.OK) {
|
|
data = await response.text();
|
|
}
|
|
|
|
return {
|
|
status: response.status,
|
|
data,
|
|
};
|
|
}
|
|
|
|
public async increaseNotebookViews(id: string): Promise<IJunoResponse<IGalleryItem>> {
|
|
const response = await window.fetch(`${this.getNotebooksUrl()}/gallery/${id}/views`, {
|
|
method: "PATCH",
|
|
});
|
|
|
|
let data: IGalleryItem;
|
|
if (response.status === HttpStatusCodes.OK) {
|
|
data = await response.json();
|
|
}
|
|
|
|
return {
|
|
status: response.status,
|
|
data,
|
|
};
|
|
}
|
|
|
|
public async increaseNotebookDownloadCount(id: string): Promise<IJunoResponse<IGalleryItem>> {
|
|
const response = await window.fetch(`${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/${id}/downloads`, {
|
|
method: "PATCH",
|
|
headers: JunoClient.getHeaders(),
|
|
});
|
|
|
|
let data: IGalleryItem;
|
|
if (response.status === HttpStatusCodes.OK) {
|
|
data = await response.json();
|
|
}
|
|
|
|
return {
|
|
status: response.status,
|
|
data,
|
|
};
|
|
}
|
|
|
|
public async favoriteNotebook(id: string): Promise<IJunoResponse<IGalleryItem>> {
|
|
const response = await window.fetch(`${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/${id}/favorite`, {
|
|
method: "PATCH",
|
|
headers: JunoClient.getHeaders(),
|
|
});
|
|
|
|
let data: IGalleryItem;
|
|
if (response.status === HttpStatusCodes.OK) {
|
|
data = await response.json();
|
|
}
|
|
|
|
return {
|
|
status: response.status,
|
|
data,
|
|
};
|
|
}
|
|
|
|
public async unfavoriteNotebook(id: string): Promise<IJunoResponse<IGalleryItem>> {
|
|
const response = await window.fetch(`${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/${id}/unfavorite`, {
|
|
method: "PATCH",
|
|
headers: JunoClient.getHeaders(),
|
|
});
|
|
|
|
let data: IGalleryItem;
|
|
if (response.status === HttpStatusCodes.OK) {
|
|
data = await response.json();
|
|
}
|
|
|
|
return {
|
|
status: response.status,
|
|
data,
|
|
};
|
|
}
|
|
|
|
public async getFavoriteNotebooks(): Promise<IJunoResponse<IGalleryItem[]>> {
|
|
return await this.getNotebooks(`${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/favorites`, {
|
|
headers: JunoClient.getHeaders(),
|
|
});
|
|
}
|
|
|
|
public async getPublishedNotebooks(): Promise<IJunoResponse<IGalleryItem[]>> {
|
|
return await this.getNotebooks(`${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/published`, {
|
|
headers: JunoClient.getHeaders(),
|
|
});
|
|
}
|
|
|
|
public async deleteNotebook(id: string): Promise<IJunoResponse<IGalleryItem>> {
|
|
const response = await window.fetch(`${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/${id}`, {
|
|
method: "DELETE",
|
|
headers: JunoClient.getHeaders(),
|
|
});
|
|
|
|
let data: IGalleryItem;
|
|
if (response.status === HttpStatusCodes.OK) {
|
|
data = await response.json();
|
|
}
|
|
|
|
return {
|
|
status: response.status,
|
|
data,
|
|
};
|
|
}
|
|
|
|
public async publishNotebook(
|
|
name: string,
|
|
description: string,
|
|
tags: string[],
|
|
thumbnailUrl: string,
|
|
content: string,
|
|
): Promise<IJunoResponse<IGalleryItem>> {
|
|
const response = await window.fetch(`${this.getNotebooksSubscriptionIdAccountUrl()}/gallery`, {
|
|
method: "PUT",
|
|
headers: JunoClient.getHeaders(),
|
|
body: JSON.stringify({
|
|
name,
|
|
description,
|
|
tags,
|
|
thumbnailUrl,
|
|
content: JSON.parse(content),
|
|
addLinkToNotebookViewer: true,
|
|
} as IPublishNotebookRequest),
|
|
});
|
|
|
|
let data: IGalleryItem;
|
|
if (response.status === HttpStatusCodes.OK) {
|
|
data = await response.json();
|
|
} else {
|
|
throw new Error(`HTTP status ${response.status} thrown. ${(await response.json()).Message}`);
|
|
}
|
|
|
|
return {
|
|
status: response.status,
|
|
data,
|
|
};
|
|
}
|
|
|
|
public getNotebookContentUrl(id: string): string {
|
|
return `${this.getNotebooksUrl()}/gallery/${id}/content`;
|
|
}
|
|
|
|
public getNotebookInfoUrl(id: string): string {
|
|
return `${this.getNotebooksUrl()}/gallery/${id}`;
|
|
}
|
|
|
|
public async reportAbuse(notebookId: string, abuseCategory: string, notes: string): Promise<IJunoResponse<boolean>> {
|
|
const response = await window.fetch(`${this.getNotebooksUrl()}/gallery/reportAbuse`, {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
notebookId,
|
|
abuseCategory,
|
|
notes,
|
|
}),
|
|
headers: {
|
|
[HttpHeaders.contentType]: "application/json",
|
|
},
|
|
});
|
|
|
|
let data: boolean;
|
|
if (response.status === HttpStatusCodes.OK) {
|
|
data = await response.json();
|
|
}
|
|
|
|
return {
|
|
status: response.status,
|
|
data,
|
|
};
|
|
}
|
|
|
|
public async requestSchema(
|
|
schemaRequest: DataModels.ISchemaRequest,
|
|
): Promise<IJunoResponse<DataModels.ISchemaRequest>> {
|
|
const response = await window.fetch(`${this.getAnalyticsUrl()}/schema/request`, {
|
|
method: "POST",
|
|
body: JSON.stringify(schemaRequest),
|
|
headers: JunoClient.getHeaders(),
|
|
});
|
|
|
|
let data: DataModels.ISchemaRequest;
|
|
if (response.status === HttpStatusCodes.OK) {
|
|
data = await response.json();
|
|
}
|
|
|
|
return {
|
|
status: response.status,
|
|
data,
|
|
};
|
|
}
|
|
|
|
public async getSchema(
|
|
subscriptionId: string,
|
|
resourceGroup: string,
|
|
accountName: string,
|
|
databaseName: string,
|
|
containerName: string,
|
|
): Promise<IJunoResponse<DataModels.ISchema>> {
|
|
const response = await window.fetch(
|
|
`${this.getAnalyticsUrl()}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/databaseAccounts/${accountName}/schema/${databaseName}/${containerName}`,
|
|
{
|
|
method: "GET",
|
|
headers: JunoClient.getHeaders(),
|
|
},
|
|
);
|
|
|
|
let data: DataModels.ISchema;
|
|
|
|
if (response.status === HttpStatusCodes.OK) {
|
|
data = await response.json();
|
|
}
|
|
|
|
return {
|
|
status: response.status,
|
|
data,
|
|
};
|
|
}
|
|
|
|
private async getNotebooks(input: RequestInfo, init?: RequestInit): Promise<IJunoResponse<IGalleryItem[]>> {
|
|
const response = await window.fetch(input, init);
|
|
|
|
let data: IGalleryItem[];
|
|
if (response.status === HttpStatusCodes.OK) {
|
|
data = await response.json();
|
|
}
|
|
|
|
return {
|
|
status: response.status,
|
|
data,
|
|
};
|
|
}
|
|
|
|
// public for tests
|
|
public static getJunoEndpoint(): string {
|
|
const junoEndpoint = userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT;
|
|
if (!validateEndpoint(junoEndpoint, allowedJunoOrigins)) {
|
|
const error = `${junoEndpoint} not allowed as juno endpoint`;
|
|
console.error(error);
|
|
throw new Error(error);
|
|
}
|
|
|
|
return junoEndpoint;
|
|
}
|
|
|
|
private getNotebooksUrl(): string {
|
|
return `${JunoClient.getJunoEndpoint()}/api/notebooks`;
|
|
}
|
|
|
|
private getAccount(): string {
|
|
return userContext?.databaseAccount?.name;
|
|
}
|
|
|
|
private getSubscriptionId(): string {
|
|
return userContext.subscriptionId;
|
|
}
|
|
|
|
private getNotebooksSubscriptionIdAccountUrl(): string {
|
|
return `${this.getNotebooksUrl()}/subscriptions/${this.getSubscriptionId()}/databaseAccounts/${this.getAccount()}`;
|
|
}
|
|
|
|
private getAnalyticsUrl(): string {
|
|
return `${JunoClient.getJunoEndpoint()}/api/analytics`;
|
|
}
|
|
|
|
private static getHeaders(): HeadersInit {
|
|
const authorizationHeader = getAuthorizationHeader();
|
|
return {
|
|
[authorizationHeader.header]: authorizationHeader.token,
|
|
[HttpHeaders.contentType]: "application/json",
|
|
};
|
|
}
|
|
|
|
private static getGitHubClientParams(): URLSearchParams {
|
|
const githubParams = new URLSearchParams({
|
|
client_id: GetGithubClientId(),
|
|
});
|
|
|
|
if (configContext.GITHUB_CLIENT_SECRET) {
|
|
githubParams.append("client_secret", configContext.GITHUB_CLIENT_SECRET);
|
|
}
|
|
|
|
return githubParams;
|
|
}
|
|
}
|