Stashed / app / electron / main.js
main.js
Raw
const {
  app,
  protocol,
  BrowserWindow,
  session,
  ipcMain,
  Menu,
  dialog,
  net,
} = require("electron");
const {
  default: installExtension,
  REDUX_DEVTOOLS,
  REACT_DEVELOPER_TOOLS,
} = require("electron-devtools-installer");
const SecureElectronLicenseKeys = require("secure-electron-license-keys");
const Protocol = require("./protocol");
const MenuBuilder = require("./menu");
const i18nextBackend = require("i18next-electron-fs-backend");
const i18nextMainBackend = require("../localization/i18n.mainconfig");
const Store = require("secure-electron-store").default;
const ContextMenu = require("secure-electron-context-menu").default;
const path = require("path");
const fs = require("fs");
const crypto = require("crypto");
const isDev = process.env.NODE_ENV === "development";
const port = 40992; // Hardcoded; needs to match webpack.development.js and package.json
const selfHost = `http://localhost:${port}`;

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win;
let menuBuilder;

async function createWindow() {
  // If you'd like to set up auto-updating for your app,
  // I'd recommend looking at https://github.com/iffy/electron-updater-example
  // to use the method most suitable for you.
  // eg. autoUpdater.checkForUpdatesAndNotify();

  if (!isDev) {
    // Needs to happen before creating/loading the browser window;
    // protocol is only used in prod
    protocol.registerBufferProtocol(
      Protocol.scheme,
      Protocol.requestHandler
    ); /* eng-disable PROTOCOL_HANDLER_JS_CHECK */
  }

  const store = new Store({
    path: app.getPath("userData"),
  });

  // Use saved config values for configuring your
  // BrowserWindow, for instance.
  // NOTE - this config is not passcode protected
  // and stores plaintext values
  //let savedConfig = store.mainInitialStore(fs);

  // Create the browser window.
  win = new BrowserWindow({
    width: 1280,
    height: 1024,
    title: "Application is currently initializing...",
    webPreferences: {
      devTools: isDev,
      nodeIntegration: false,
      nodeIntegrationInWorker: false,
      nodeIntegrationInSubFrames: false,
      contextIsolation: true,
      enableRemoteModule: false,
      additionalArguments: [`storePath:${app.getPath("userData")}`],
      preload: path.join(__dirname, "preload.js"),
      /* eng-disable PRELOAD_JS_CHECK */
      disableBlinkFeatures: "Auxclick",
    },
  });

  // Sets up main.js bindings for our i18next backend
  i18nextBackend.mainBindings(ipcMain, win, fs);

  // Sets up main.js bindings for our electron store;
  // callback is optional and allows you to use store in main process
  const callback = function (success, initialStore) {
    console.log(
      `${!success ? "Un-s" : "S"}uccessfully retrieved store in main process.`
    );
    // console.log(initialStore); // {"key1": "value1", ... }
  };

  store.mainBindings(ipcMain, win, fs, callback);

  // Sets up bindings for our custom context menu
  ContextMenu.mainBindings(ipcMain, win, Menu, isDev, {
    loudAlertTemplate: [
      {
        id: "loudAlert",
        label: "AN ALERT!",
      },
    ],
    softAlertTemplate: [
      {
        id: "softAlert",
        label: "Soft alert",
      },
    ],
  });

  // Setup bindings for offline license verification
  SecureElectronLicenseKeys.mainBindings(ipcMain, win, fs, crypto, {
    root: process.cwd(),
    version: app.getVersion(),
  });

  // Load app
  if (isDev) {
    win.loadURL(selfHost);
  } else {
    win.loadURL(`${Protocol.scheme}://rse/index.html`);
  }

  win.webContents.on("did-finish-load", () => {
    win.setTitle(`Stashed App (v${app.getVersion()})`);
  });

  // Only do these things when in development
  if (isDev) {
    // Errors are thrown if the dev tools are opened
    // before the DOM is ready
    win.webContents.once("dom-ready", async () => {
      await installExtension([REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS])
        .then((name) => console.log(`Added Extension:  ${name}`))
        .catch((err) => console.log("An error occurred: ", err))
        .finally(() => {
          require("electron-debug")(); // https://github.com/sindresorhus/electron-debug
          win.webContents.openDevTools();
        });
    });
  }

  // Emitted when the window is closed.
  win.on("closed", () => {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    win = null;
  });

  // https://electronjs.org/docs/tutorial/security#4-handle-session-permission-requests-from-remote-content
  const ses = session;
  const partition = "default";
  ses
    .fromPartition(
      partition
    ) /* eng-disable PERMISSION_REQUEST_HANDLER_JS_CHECK */
    .setPermissionRequestHandler((webContents, permission, permCallback) => {
      const allowedPermissions = []; // Full list here: https://developer.chrome.com/extensions/declare_permissions#manifest

      if (allowedPermissions.includes(permission)) {
        permCallback(true); // Approve permission request
      } else {
        console.error(
          `The application tried to request permission for '${permission}'. This permission was not whitelisted and has been blocked.`
        );

        permCallback(false); // Deny
      }
    });

  session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
    (details.requestHeaders["user-agent"] =
      "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36"),
      (details.requestHeaders["accept"] =
        "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"),
      (details.requestHeaders["accept-language"] = "en-US,en;q=0.9"),
      (details.requestHeaders["sec-ch-ua"] =
        '" Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90"'),
      (details.requestHeaders["sec-ch-ua-mobile"] = "?0"),
      (details.requestHeaders["sec-fetch-dest"] = "document"),
      (details.requestHeaders["sec-fetch-mode"] = "navigate"),
      (details.requestHeaders["sec-fetch-site"] = "none"),
      (details.requestHeaders["sec-fetch-user"] = "?1"),
      (details.requestHeaders["upgrade-insecure-requests"] = "1");
    callback({ cancel: false, requestHeaders: details.requestHeaders });
  });

  // https://electronjs.org/docs/tutorial/security#1-only-load-secure-content;
  // The below code can only run when a scheme and host are defined, I thought
  // we could use this over _all_ urls
  // ses.fromPartition(partition).webRequest.onBeforeRequest({urls:["http://localhost./*"]}, (listener) => {
  //   if (listener.url.indexOf("http://") >= 0) {
  //     listener.callback({
  //       cancel: true
  //     });
  //   }
  // });

  menuBuilder = MenuBuilder(win, app.name);

  // Set up necessary bindings to update the menu items
  // based on the current language selected
  i18nextMainBackend.on("loaded", (loaded) => {
    i18nextMainBackend.changeLanguage("en");
    i18nextMainBackend.off("loaded");
  });

  i18nextMainBackend.on("languageChanged", (lng) => {
    menuBuilder.buildMenu(i18nextMainBackend);
  });
}

// Needs to be called before app is ready;
// gives our scheme access to load relative files,
// as well as local storage, cookies, etc.
// https://electronjs.org/docs/api/protocol#protocolregisterschemesasprivilegedcustomschemes
protocol.registerSchemesAsPrivileged([
  {
    scheme: Protocol.scheme,
    privileges: {
      standard: true,
      secure: true,
    },
  },
]);

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on("ready", () => {
  createWindow();
});

// Quit when all windows are closed.
app.on("window-all-closed", () => {
  // On macOS it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform !== "darwin") {
    app.quit();
  } else {
    i18nextBackend.clearMainBindings(ipcMain);
    ContextMenu.clearMainBindings(ipcMain);
    SecureElectronLicenseKeys.clearMainBindings(ipcMain);
  }
});

app.on("activate", () => {
  // On macOS it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (win === null) {
    createWindow();
  }
});

// https://electronjs.org/docs/tutorial/security#12-disable-or-limit-navigation
app.on("web-contents-created", (event, contents) => {
  contents.on("will-navigate", (contentsEvent, navigationUrl) => {
    /* eng-disable LIMIT_NAVIGATION_JS_CHECK  */
    const parsedUrl = new URL(navigationUrl);
    const validOrigins = [selfHost];

    // Log and prevent the app from navigating to a new page if that page's origin is not whitelisted
    if (!validOrigins.includes(parsedUrl.origin)) {
      console.error(
        `The application tried to redirect to the following address: '${parsedUrl}'. This origin is not whitelisted and the attempt to navigate was blocked.`
      );

      contentsEvent.preventDefault();
    }
  });

  contents.on("will-redirect", (contentsEvent, navigationUrl) => {
    const parsedUrl = new URL(navigationUrl);
    const validOrigins = [];

    // Log and prevent the app from redirecting to a new page
    if (!validOrigins.includes(parsedUrl.origin)) {
      console.error(
        `The application tried to redirect to the following address: '${navigationUrl}'. This attempt was blocked.`
      );

      contentsEvent.preventDefault();
    }
  });

  // https://electronjs.org/docs/tutorial/security#11-verify-webview-options-before-creation
  contents.on(
    "will-attach-webview",
    (contentsEvent, webPreferences, params) => {
      // Strip away preload scripts if unused or verify their location is legitimate
      delete webPreferences.preload;
      delete webPreferences.preloadURL;

      // Disable Node.js integration
      webPreferences.nodeIntegration = false;
    }
  );

  // https://electronjs.org/docs/tutorial/security#13-disable-or-limit-creation-of-new-windows
  // This code replaces the old "new-window" event handling;
  // https://github.com/electron/electron/pull/24517#issue-447670981
  contents.setWindowOpenHandler(({ url }) => {
    const parsedUrl = new URL(url);
    const validOrigins = [];

    // Log and prevent opening up a new window
    if (!validOrigins.includes(parsedUrl.origin)) {
      console.error(
        `The application tried to open a new window at the following address: '${url}'. This attempt was blocked.`
      );

      return {
        action: "deny",
      };
    }

    return {
      action: "allow",
    };
  });
});

// Filter loading any module via remote;
// you shouldn't be using remote at all, though
// https://electronjs.org/docs/tutorial/security#16-filter-the-remote-module
app.on("remote-require", (event, webContents, moduleName) => {
  event.preventDefault();
});

// built-ins are modules such as "app"
app.on("remote-get-builtin", (event, webContents, moduleName) => {
  event.preventDefault();
});

app.on("remote-get-global", (event, webContents, globalName) => {
  event.preventDefault();
});

app.on("remote-get-current-window", (event, webContents) => {
  event.preventDefault();
});

app.on("remote-get-current-web-contents", (event, webContents) => {
  event.preventDefault();
});

ipcMain.on("exportInventoryData", (e, args) => {
  const options = {
    title: "Select a path to export inventory data to",
    filters: [{ name: "JSON", extensions: ["json"] }],
  };
  dialog
    .showSaveDialog(win, options)
    .then((res) => {
      console.log(res.filePath);
      fs.writeFile(res.filePath, JSON.stringify(args, null, 2), (err) => {
        if (err) throw err;
      });
    })
    .catch((err) => {
      throw err;
    });
});

ipcMain.on("importInventoryData", (e, args) => {
  const options = {
    title: "Select a file to import inventory data",
    filters: [{ name: "JSON", extensions: ["json"] }],
  };
  dialog.showOpenDialog(win, options).then((res) => {
    console.log(res.filePaths[0]);
    fs.readFile(res.filePaths[0], "utf8", (err, data) => {
      if (err) throw err;
      win.webContents.send("receiveInventoryData", data);
    });
  });
});

ipcMain.on("getProductData", (e, args) => {
  console.log("getting product data...");
  let data = "";
  const request = net.request({
    method: "GET",
    protocol: "https:",
    url: `https://stockx.com/api/products/${args}?includes=market`,
  });

  request.on("error", (err) => {
    console.log(err);
    console.log("NetError::: " + err.message);
  });

  request.on("response", (_response) => {
    _response.on("end", () => {
      console.log("NetEnd::: END response (no more data).");
      win.webContents.send("receiveProductData", {
        ...JSON.parse(data),
      });
      data = "";
    });
    _response.on("data", (_chunk) => {
      // console.log("NetStatus::: " + _response.statusCode);
      // console.log("NetHeaders::: " + JSON.stringify(_response.headers));
      console.log("NetBody::: " + _chunk);
      data += _chunk;
    });
  });

  request.end();
});

ipcMain.on("getChartData", (e, args) => {
  console.log(args);
  let data = "";
  const request = net.request({
    method: "GET",
    protocol: "https:",
    url: `https://stockx.com/api/products/${args.itemId}/chart?start_date=${args.startDate}&end_date=${args.endDate}&intervals=100&format=highstock&currency=USD&country=US`,
  });

  request.on("error", (err) => {
    console.log(err);
    console.log("NetError::: " + err.message);
  });

  request.on("response", (_response) => {
    _response.on("end", () => {
      console.log("NetEnd::: END response (no more data).");

      console.log(data, "logged!");
      win.webContents.send("receiveChartData", {
        ...JSON.parse(data),
        ...args,
      });

      data = "";
    });
    _response.on("data", (_chunk) => {
      // console.log("NetStatus::: " + _response.statusCode);
      // console.log("NetHeaders::: " + JSON.stringify(_response.headers));
      // console.log("NetBody::: " + _chunk);
      data += _chunk;
    });
  });

  request.end();
});

ipcMain.on("getSaleData", (e, args) => {
  let data = "";
  const request = net.request({
    method: "GET",
    protocol: "https:",
    url: `https://stockx.com/api/products/${args}/activity?state=480&currency=USD&limit=10&sort=createdAt&order=DESC&country=US`,
  });
  request.on("error", (err) => {
    console.log(err);
    console.log("NetError::: " + err.message);
  });

  request.on("response", (_response) => {
    _response.on("end", () => {
      console.log("NetEnd::: END response (no more data).");
      win.webContents.send("receiveSaleData", data);
      data = "";
    });
    _response.on("data", (_chunk) => {
      // console.log("NetStatus::: " + _response.statusCode);
      // console.log("NetHeaders::: " + JSON.stringify(_response.headers));
      // console.log("NetBody::: " + _chunk);
      data += _chunk;
    });
  });
  request.end();
});