diff --git a/package-lock.json b/package-lock.json index f0aa5ae..79d003e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "redis": "^3.1.2", "tar-fs": "^2.1.1", "textextensions": "^5.12.0", + "ts-custom-error": "^3.2.0", "xml-flow": "^1.0.4" }, "devDependencies": { @@ -4463,6 +4464,14 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/ts-custom-error": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.2.0.tgz", + "integrity": "sha512-cBvC2QjtvJ9JfWLvstVnI45Y46Y5dMxIaG1TDMGAD/R87hpvqFL+7LhvUDhnRCfOnx/xitollFWWvUKKKhbN0A==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/ts-node": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.2.0.tgz", @@ -8496,6 +8505,11 @@ "nopt": "~1.0.10" } }, + "ts-custom-error": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.2.0.tgz", + "integrity": "sha512-cBvC2QjtvJ9JfWLvstVnI45Y46Y5dMxIaG1TDMGAD/R87hpvqFL+7LhvUDhnRCfOnx/xitollFWWvUKKKhbN0A==" + }, "ts-node": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.2.0.tgz", diff --git a/package.json b/package.json index 95ae12b..6c6c882 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "redis": "^3.1.2", "tar-fs": "^2.1.1", "textextensions": "^5.12.0", + "ts-custom-error": "^3.2.0", "xml-flow": "^1.0.4" }, "devDependencies": { diff --git a/src/AnonymizedFile.ts b/src/AnonymizedFile.ts index 4bb3dd6..fb4a267 100644 --- a/src/AnonymizedFile.ts +++ b/src/AnonymizedFile.ts @@ -6,6 +6,7 @@ import { Tree, TreeElement, TreeFile } from "./types"; import storage from "./storage"; import config from "../config"; import { anonymizePath, anonymizeStream } from "./anonymize-utils"; +import AnonymousError from "./AnonymousError"; function tree2sha( tree: any, @@ -39,7 +40,8 @@ export default class AnonymizedFile { constructor(data: { repository: Repository; anonymizedPath: string }) { this.repository = data.repository; - if (!this.repository.options.terms) throw new Error("terms_not_specified"); + if (!this.repository.options.terms) + throw new AnonymousError("terms_not_specified"); this.anonymizedPath = data.anonymizedPath; } @@ -49,9 +51,8 @@ export default class AnonymizedFile { * @returns the origin relative path of the file */ async originalPath(): Promise { - // console.log(new Error().stack); if (this._originalPath) return this._originalPath; - if (!this.anonymizedPath) throw new Error("path_not_specified"); + if (!this.anonymizedPath) throw new AnonymousError("path_not_specified"); const paths = this.anonymizedPath.trim().split("/"); @@ -67,7 +68,7 @@ export default class AnonymizedFile { continue; } if (!currentAnonymized[fileName]) { - throw new Error("file_not_found"); + throw new AnonymousError("file_not_found", this); } currentAnonymized = currentAnonymized[fileName]; @@ -100,7 +101,7 @@ export default class AnonymizedFile { currentAnonymized.sha === undefined || currentAnonymized.size === undefined ) { - throw new Error("folder_not_supported"); + throw new AnonymousError("folder_not_supported", this); } const file: TreeFile = currentAnonymized as TreeFile; @@ -111,7 +112,7 @@ export default class AnonymizedFile { // it should never happen const shaTree = tree2sha(currentOriginal); if (!currentAnonymized.sha || !shaTree[file.sha]) { - throw new Error("file_not_found"); + throw new AnonymousError("file_not_found", this); } this._originalPath = path.join(currentOriginalPath, shaTree[file.sha]); @@ -144,7 +145,7 @@ export default class AnonymizedFile { async content(): Promise { if (this.fileSize && this.fileSize > config.MAX_FILE_SIZE) { - throw new Error("file_too_big"); + throw new AnonymousError("file_too_big", this); } if (await storage.exists(this.originalCachePath)) { return storage.read(this.originalCachePath); @@ -160,7 +161,7 @@ export default class AnonymizedFile { } get originalCachePath() { - if (!this.originalPath) throw new Error("path_not_defined"); + if (!this.originalPath) throw new AnonymousError("path_not_defined"); return path.join(this.repository.originalCachePath, this._originalPath); } diff --git a/src/AnonymousError.ts b/src/AnonymousError.ts new file mode 100644 index 0000000..51bba96 --- /dev/null +++ b/src/AnonymousError.ts @@ -0,0 +1,18 @@ +import { CustomError } from 'ts-custom-error' + +/** + * Custom error message + */ +export default class AnonymousError extends CustomError { + + value: any; + + constructor(message: string, value?: any) { + super(message); + this.value = value; + } + + toString(): string { + return this.message; + } +} diff --git a/src/Repository.ts b/src/Repository.ts index 5f0263d..c139820 100644 --- a/src/Repository.ts +++ b/src/Repository.ts @@ -13,6 +13,7 @@ import { anonymizeStream } from "./anonymize-utils"; import GitHubBase from "./source/GitHubBase"; import Conference from "./Conference"; import ConferenceModel from "./database/conference/conferences.model"; +import AnonymousError from "./AnonymousError"; export default class Repository { private _model: IAnonymizedRepositoryDocument; @@ -32,7 +33,7 @@ export default class Repository { this.source = new Zip(data.source, this); break; default: - throw new Error("unsupported_source"); + throw new AnonymousError("unsupported_source", data.source.type); } this.owner = new User(new UserModel({ _id: data.owner })); } @@ -103,13 +104,13 @@ export default class Repository { } } if (this._model.status == "expired") { - throw new Error("repository_expired"); + throw new AnonymousError("repository_expired", this); } if (this._model.status == "removed") { - throw new Error("repository_expired"); + throw new AnonymousError("repository_expired", this); } if (this._model.status != "ready") { - throw new Error("repository_not_ready"); + throw new AnonymousError("repository_not_ready", this); } } @@ -137,7 +138,7 @@ export default class Repository { if (this._model.options.update && this._model.lastView < yesterday) { if (this._model.status != "ready") { - throw new Error("repo_not_ready"); + throw new AnonymousError("repository_not_ready", this); } // Only GitHubBase can be update for the moment @@ -162,7 +163,7 @@ export default class Repository { console.error( `${branch.name} for ${this.source.githubRepository.fullName} is not found` ); - throw new Error("branch_not_found"); + throw new AnonymousError("branch_not_found", this); } this._model.anonymizeDate = new Date(); console.log( diff --git a/src/database/database.ts b/src/database/database.ts index 23ee159..fa5da49 100644 --- a/src/database/database.ts +++ b/src/database/database.ts @@ -2,6 +2,7 @@ import * as mongoose from "mongoose"; import Repository from "../Repository"; import config from "../../config"; import AnonymizedRepositoryModel from "./anonymizedRepositories/anonymizedRepositories.model"; +import AnonymousError from "../AnonymousError"; const MONGO_URL = `mongodb://${config.DB_USERNAME}:${config.DB_PASSWORD}@${config.DB_HOSTNAME}:27017/`; @@ -23,6 +24,6 @@ export async function connect() { export async function getRepository(repoId: string) { const data = await AnonymizedRepositoryModel.findOne({ repoId }); - if (!data) throw new Error("repo_not_found"); + if (!data) throw new AnonymousError("repo_not_found", repoId); return new Repository(data); } diff --git a/src/routes/conference.ts b/src/routes/conference.ts index eaeea7a..3b5fc16 100644 --- a/src/routes/conference.ts +++ b/src/routes/conference.ts @@ -1,9 +1,7 @@ import * as express from "express"; -import config from "../../config"; +import AnonymousError from "../AnonymousError"; import Conference from "../Conference"; -import AnonymizedRepositoryModel from "../database/anonymizedRepositories/anonymizedRepositories.model"; import ConferenceModel from "../database/conference/conferences.model"; -import Repository from "../Repository"; import { ensureAuthenticated } from "./connection"; import { handleError, getUser } from "./route-utils"; @@ -69,26 +67,26 @@ router.get("/", async (req: express.Request, res: express.Response) => { }); function validateConferenceForm(conf) { - if (!conf.name) throw new Error("conf_name_missing"); - if (!conf.conferenceID) throw new Error("conf_id_missing"); - if (!conf.startDate) throw new Error("conf_start_date_missing"); - if (!conf.endDate) throw new Error("conf_end_date_missing"); + if (!conf.name) throw new AnonymousError("conf_name_missing"); + if (!conf.conferenceID) throw new AnonymousError("conf_id_missing"); + if (!conf.startDate) throw new AnonymousError("conf_start_date_missing"); + if (!conf.endDate) throw new AnonymousError("conf_end_date_missing"); if (new Date(conf.startDate) > new Date(conf.endDate)) - throw new Error("conf_start_date_invalid"); + throw new AnonymousError("conf_start_date_invalid"); if (new Date() > new Date(conf.endDate)) - throw new Error("conf_end_date_invalid"); + throw new AnonymousError("conf_end_date_invalid"); if (plans.filter((p) => p.id == conf.plan.planID).length != 1) - throw new Error("invalid_plan"); + throw new AnonymousError("invalid_plan"); const plan = plans.filter((p) => p.id == conf.plan.planID)[0]; if (plan.pricePerRepo > 0) { const billing = conf.billing; - if (!billing) throw new Error("billing_missing"); - if (!billing.name) throw new Error("billing_name_missing"); - if (!billing.email) throw new Error("billing_email_missing"); - if (!billing.address) throw new Error("billing_address_missing"); - if (!billing.city) throw new Error("billing_city_missing"); - if (!billing.zip) throw new Error("billing_zip_missing"); - if (!billing.country) throw new Error("billing_country_missing"); + if (!billing) throw new AnonymousError("billing_missing"); + if (!billing.name) throw new AnonymousError("billing_name_missing"); + if (!billing.email) throw new AnonymousError("billing_email_missing"); + if (!billing.address) throw new AnonymousError("billing_address_missing"); + if (!billing.city) throw new AnonymousError("billing_city_missing"); + if (!billing.zip) throw new AnonymousError("billing_zip_missing"); + if (!billing.country) throw new AnonymousError("billing_country_missing"); } } @@ -103,7 +101,7 @@ router.post( conferenceID: req.params.conferenceID, }); if (model.owners.indexOf(user.model.id) == -1) - throw new Error("not_authorized"); + throw new AnonymousError("not_authorized"); } validateConferenceForm(req.body); model.name = req.body.name; @@ -148,7 +146,7 @@ router.post( res.send("ok"); } catch (error) { if (error.message?.indexOf(" duplicate key") > -1) { - return handleError(new Error("conf_id_used"), res); + return handleError(new AnonymousError("conf_id_used"), res); } handleError(error, res); } @@ -163,10 +161,10 @@ router.get( const data = await ConferenceModel.findOne({ conferenceID: req.params.conferenceID, }); - if (!data) throw new Error("conf_not_found"); + if (!data) throw new AnonymousError("conf_not_found"); const conference = new Conference(data); if (conference.ownerIDs.indexOf(user.model.id) == -1) - throw new Error("not_authorized"); + throw new AnonymousError("not_authorized"); const o: any = conference.toJSON(); o.repositories = (await conference.repositories()).map((r) => r.toJSON()); res.json(o); @@ -184,10 +182,10 @@ router.delete( const data = await ConferenceModel.findOne({ conferenceID: req.params.conferenceID, }); - if (!data) throw new Error("conf_not_found"); + if (!data) throw new AnonymousError("conf_not_found"); const conference = new Conference(data); if (conference.ownerIDs.indexOf(user.model.id) == -1) - throw new Error("not_authorized"); + throw new AnonymousError("not_authorized"); await conference.remove(); res.send("ok"); } catch (error) { diff --git a/src/routes/repository-private.ts b/src/routes/repository-private.ts index 32a5b41..2df5a57 100644 --- a/src/routes/repository-private.ts +++ b/src/routes/repository-private.ts @@ -11,6 +11,7 @@ import config from "../../config"; import { IAnonymizedRepositoryDocument } from "../database/anonymizedRepositories/anonymizedRepositories.types"; import Repository from "../Repository"; import ConferenceModel from "../database/conference/conferences.model"; +import AnonymousError from "../AnonymousError"; const router = express.Router(); @@ -182,22 +183,22 @@ function validateNewRepo(repoUpdate) { !repoUpdate.repoId.match(validCharacters) || repoUpdate.repoId.length < 3 ) { - throw new Error("invalid_repoId"); + throw new AnonymousError("invalid_repoId"); } if (!repoUpdate.source.branch) { - throw new Error("branch_not_specified"); + throw new AnonymousError("branch_not_specified"); } if (!repoUpdate.source.commit) { - throw new Error("commit_not_specified"); + throw new AnonymousError("commit_not_specified"); } if (!repoUpdate.options) { - throw new Error("options_not_provided"); + throw new AnonymousError("options_not_provided"); } if (!Array.isArray(repoUpdate.terms)) { - throw new Error("invalid_terms_format"); + throw new AnonymousError("invalid_terms_format"); } if (!/^[a-f0-9]+$/.test(repoUpdate.source.commit)) { - throw new Error("invalid_commit_format"); + throw new AnonymousError("invalid_commit_format"); } } @@ -283,7 +284,7 @@ router.post( new Date() > conf.endDate || conf.status !== "ready" ) { - throw new Error("conf_not_activated"); + throw new AnonymousError("conf_not_activated"); } const f = conf.repositories.filter((r) => r.id == repo.model.id); if (f.length) { @@ -358,7 +359,7 @@ router.post("/", async (req: express.Request, res: express.Response) => { conf.status !== "ready" ) { await repo.remove(); - throw new Error("conf_not_activated"); + throw new AnonymousError("conf_not_activated"); } conf.repositories.push({ id: repo.id, @@ -371,6 +372,9 @@ router.post("/", async (req: express.Request, res: express.Response) => { res.send("ok"); new Repository(repo).anonymize(); } catch (error) { + if (error.message?.indexOf(" duplicate key") > -1) { + return handleError(new AnonymousError("repoId_already_used", repoUpdate.repoId), res); + } return handleError(error, res); } }); diff --git a/src/routes/repository-public.ts b/src/routes/repository-public.ts index 9f59f4e..44d5431 100644 --- a/src/routes/repository-public.ts +++ b/src/routes/repository-public.ts @@ -58,9 +58,9 @@ router.get( redirectURL = repo.source.url; } else { repo.check(); + await repo.updateIfNeeded(); } - await repo.updateIfNeeded(); let download = false; const conference = await repo.conference(); diff --git a/src/routes/route-utils.ts b/src/routes/route-utils.ts index 41d356e..a72afaa 100644 --- a/src/routes/route-utils.ts +++ b/src/routes/route-utils.ts @@ -1,6 +1,12 @@ import * as express from "express"; +import AnonymizedFile from "../AnonymizedFile"; +import AnonymousError from "../AnonymousError"; import * as db from "../database/database"; import UserModel from "../database/users/users.model"; +import Repository from "../Repository"; +import GitHubBase from "../source/GitHubBase"; +import GitHubDownload from "../source/GitHubDownload"; +import { GitHubRepository } from "../source/GitHubRepository"; import User from "../User"; export async function getRepo( @@ -31,8 +37,35 @@ export async function getRepo( } } +function printError(error: any) { + if (error instanceof AnonymousError) { + let detail = error.value?.toString(); + if (error.value instanceof Repository) { + detail = error.value.repoId; + } else if (error.value instanceof AnonymizedFile) { + detail = `/r/${error.value.repository.repoId}/${error.value.anonymizedPath}`; + } else if (error.value instanceof GitHubRepository) { + detail = `${error.value.fullName}`; + } else if (error.value instanceof User) { + detail = `${error.value.username}`; + } else if (error.value instanceof GitHubBase) { + detail = `${error.value.repository.repoId}`; + } + console.error( + "[ERROR]", + error.message, + `'${detail}'`, + error.stack.split("\n")[1].trim() + ); + } else if (error instanceof Error) { + console.error(error); + } else { + console.error(error); + } +} + export function handleError(error: any, res: express.Response) { - console.log(error); + printError(error); let message = error; if (error instanceof Error) { message = error.message; @@ -52,12 +85,12 @@ export async function getUser(req: express.Request) { const user = (req.user as any).user; if (!user) { req.logout(); - throw new Error("not_connected"); + throw new AnonymousError("not_connected"); } const model = await UserModel.findById(user._id); if (!model) { req.logout(); - throw new Error("not_connected"); + throw new AnonymousError("not_connected"); } return new User(model); } diff --git a/src/routes/webview.ts b/src/routes/webview.ts index 0f3d37a..025da86 100644 --- a/src/routes/webview.ts +++ b/src/routes/webview.ts @@ -3,6 +3,7 @@ import { getRepo, handleError } from "./route-utils"; import * as path from "path"; import AnonymizedFile from "../AnonymizedFile"; import GitHubDownload from "../source/GitHubDownload"; +import AnonymousError from "../AnonymousError"; const router = express.Router(); @@ -10,18 +11,15 @@ async function webView(req: express.Request, res: express.Response) { const repo = await getRepo(req, res); if (!repo) return; try { - if (!repo.options.page) { - throw new Error("page_not_activated"); - } - if (!repo.options.pageSource) { - throw new Error("page_not_activated"); + if (!repo.options.page || !repo.options.pageSource) { + throw new AnonymousError("page_not_activated"); } if ( repo.options.pageSource?.branch != (repo.source as GitHubDownload).branch.name ) { - throw new Error("page_not_supported_on_different_branch"); + throw new AnonymousError("page_not_supported_on_different_branch"); } let requestPath = path.join( diff --git a/src/source/GitHubBase.ts b/src/source/GitHubBase.ts index e2da5dc..ae4bac1 100644 --- a/src/source/GitHubBase.ts +++ b/src/source/GitHubBase.ts @@ -6,6 +6,7 @@ import { OAuthApp } from "@octokit/oauth-app"; import Repository from "../Repository"; import * as stream from "stream"; import UserModel from "../database/users/users.model"; +import AnonymousError from "../AnonymousError"; export default abstract class GitHubBase { type: "GitHubDownload" | "GitHubStream" | "Zip"; @@ -37,17 +38,18 @@ export default abstract class GitHubBase { } async getFileContent(file: AnonymizedFile): Promise { - throw new Error("Method not implemented."); + throw new AnonymousError("Method not implemented."); } + getFiles(): Promise { - throw new Error("Method not implemented."); + throw new AnonymousError("Method not implemented."); } async getToken(owner?: string) { if (owner) { const user = await UserModel.findOne({ username: owner }); - if (user && user.accessToken) { - return user.accessToken as string; + if (user && user.accessTokens.github) { + return user.accessTokens.github as string; } } if (this.accessToken) { diff --git a/src/source/GitHubDownload.ts b/src/source/GitHubDownload.ts index e7f419b..bc3347a 100644 --- a/src/source/GitHubDownload.ts +++ b/src/source/GitHubDownload.ts @@ -9,6 +9,7 @@ import { SourceBase } from "../types"; import got from "got"; import * as stream from "stream"; import { OctokitResponse } from "@octokit/types"; +import AnonymousError from "../AnonymousError"; export default class GitHubDownload extends GitHubBase implements SourceBase { constructor( @@ -39,7 +40,7 @@ export default class GitHubDownload extends GitHubBase implements SourceBase { async download() { if (this.repository.status == "download") - throw new Error("repo_in_download"); + throw new AnonymousError("repo_in_download", this.repository); let response: OctokitResponse; try { response = await this._getZipUrl(await this.getToken()); @@ -48,10 +49,10 @@ export default class GitHubDownload extends GitHubBase implements SourceBase { try { response = await this._getZipUrl(config.GITHUB_TOKEN); } catch (error) { - throw new Error("repo_not_accessible"); + throw new AnonymousError("repo_not_accessible", this.repository); } } else { - throw new Error("repo_not_accessible"); + throw new AnonymousError("repo_not_accessible", this.repository); } } await this.repository.updateStatus("download"); diff --git a/src/source/GitHubRepository.ts b/src/source/GitHubRepository.ts index 0876e46..d7b7b26 100644 --- a/src/source/GitHubRepository.ts +++ b/src/source/GitHubRepository.ts @@ -3,6 +3,7 @@ import * as gh from "parse-github-url"; import { IRepositoryDocument } from "../database/repositories/repositories.types"; import { Octokit } from "@octokit/rest"; import RepositoryModel from "../database/repositories/repositories.model"; +import AnonymousError from "../AnonymousError"; export class GitHubRepository { private _data: Partial< @@ -116,7 +117,7 @@ export class GitHubRepository { selected.readme = readme; await model.save(); } catch (error) { - throw new Error("readme_not_available"); + throw new AnonymousError("readme_not_available", this); } } @@ -126,7 +127,7 @@ export class GitHubRepository { public get owner(): string { const repo = gh(this.fullName); if (!repo) { - throw "invalid_repo"; + throw new AnonymousError("invalid_repo", this); } return repo.owner || this.fullName; } @@ -134,7 +135,7 @@ export class GitHubRepository { public get repo(): string { const repo = gh(this.fullName); if (!repo) { - throw "invalid_repo"; + throw new AnonymousError("invalid_repo", this); } return repo.name || this.fullName; } @@ -152,7 +153,7 @@ export async function getRepositoryFromGitHub(opt: { repo: opt.repo, }) ).data; - if (!r) throw new Error("repo_not_found"); + if (!r) throw new AnonymousError("repo_not_found", this); let model = await RepositoryModel.findOne({ externalId: "gh_" + r.id }); if (!model) { model = new RepositoryModel({ externalId: "gh_" + r.id }); diff --git a/src/source/GitHubStream.ts b/src/source/GitHubStream.ts index 32ffdee..32801ee 100644 --- a/src/source/GitHubStream.ts +++ b/src/source/GitHubStream.ts @@ -7,6 +7,7 @@ import { SourceBase, Tree } from "../types"; import * as path from "path"; import * as stream from "stream"; +import AnonymousError from "../AnonymousError"; export default class GitHubStream extends GitHubBase implements SourceBase { constructor( @@ -24,7 +25,7 @@ export default class GitHubStream extends GitHubBase implements SourceBase { } async getFileContent(file: AnonymizedFile): Promise { - if (!file.sha) throw new Error("file_sha_not_provided"); + if (!file.sha) throw new AnonymousError("file_sha_not_provided", file); const octokit = new Octokit({ auth: await this.getToken(), }); @@ -36,7 +37,7 @@ export default class GitHubStream extends GitHubBase implements SourceBase { file_sha: file.sha, }); if (!ghRes.data.content && ghRes.data.size != 0) { - throw new Error("file_not_accessible"); + throw new AnonymousError("file_not_accessible", file); } // empty file let content: Buffer; @@ -52,11 +53,11 @@ export default class GitHubStream extends GitHubBase implements SourceBase { return stream.Readable.from(content.toString()); } catch (error) { if (error.status == 403) { - throw new Error("file_too_big"); + throw new AnonymousError("file_too_big", file); } console.error(error); } - throw new Error("file_not_accessible"); + throw new AnonymousError("file_not_accessible"); } async getFiles() { diff --git a/src/storage/S3.ts b/src/storage/S3.ts index d68df5d..7258ddf 100644 --- a/src/storage/S3.ts +++ b/src/storage/S3.ts @@ -9,6 +9,7 @@ import * as flow from "xml-flow"; import * as archiver from "archiver"; import * as path from "path"; import * as gunzip from "gunzip-maybe"; +import AnonymousError from "../AnonymousError"; const originalArchiveStreamToS3Entry: Function = (ArchiveStreamToS3 as any) .prototype.onEntry; @@ -18,7 +19,7 @@ export default class S3Storage implements StorageBase { client: S3; constructor() { - if (!config.S3_BUCKET) throw new Error("s3_config_not_provided"); + if (!config.S3_BUCKET) throw new AnonymousError("s3_config_not_provided"); this.client = new S3({ region: config.S3_REGION, endpoint: config.S3_ENDPOINT,