Repository: codecentric/gopass-ui Branch: master Commit: 6f4bcd3f7609 Files: 93 Total size: 136.7 KB Directory structure: gitextract_1wdh2ba1/ ├── .github/ │ └── workflows/ │ └── on-push.yml ├── .gitignore ├── .huskyrc ├── .nvmrc ├── .prettierrc ├── .vscode/ │ └── launch.json ├── LICENSE ├── README.md ├── docs/ │ ├── development.md │ ├── platforms-and-packages.md │ └── releasing.md ├── mocks/ │ ├── fileMock.js │ └── styleMock.js ├── package.json ├── src/ │ ├── main/ │ │ ├── AppUtilities.ts │ │ ├── AppWindows.ts │ │ ├── GopassExecutor.ts │ │ └── index.ts │ ├── renderer/ │ │ ├── common/ │ │ │ ├── Settings.ts │ │ │ └── notifications/ │ │ │ ├── Notification.tsx │ │ │ └── NotificationProvider.tsx │ │ ├── components/ │ │ │ ├── ExternalLink.tsx │ │ │ ├── GoBackNavigationButton.tsx │ │ │ ├── PaginatedTable.tsx │ │ │ ├── RoundActionButton.tsx │ │ │ ├── loading-screen/ │ │ │ │ ├── LoadingScreen.css │ │ │ │ └── LoadingScreen.tsx │ │ │ └── tree/ │ │ │ ├── TreeComponent.tsx │ │ │ ├── TreeHeader.tsx │ │ │ └── TreeStyle.ts │ │ ├── explorer-app/ │ │ │ ├── ExplorerApplication.css │ │ │ ├── ExplorerApplication.tsx │ │ │ ├── GithubService.ts │ │ │ ├── MainContent.tsx │ │ │ ├── SecretsProvider.tsx │ │ │ ├── components/ │ │ │ │ ├── EnvironmentTest.tsx │ │ │ │ ├── LastVersionInfo.tsx │ │ │ │ ├── MainNavigation.tsx │ │ │ │ └── PasswordStrengthInfo.tsx │ │ │ ├── pages/ │ │ │ │ ├── AddMountPage.tsx │ │ │ │ ├── AddSecretPage.css │ │ │ │ ├── AddSecretPage.tsx │ │ │ │ ├── HomePage.tsx │ │ │ │ ├── MountsPage.tsx │ │ │ │ ├── PasswordHealthPage.tsx │ │ │ │ ├── SettingsPage.tsx │ │ │ │ └── details/ │ │ │ │ ├── HistoryTable.tsx │ │ │ │ └── SecretDetailsPage.tsx │ │ │ ├── password-health/ │ │ │ │ ├── PasswordHealthIndicator.tsx │ │ │ │ ├── PasswordHealthRules.ts │ │ │ │ ├── PasswordRater.ts │ │ │ │ ├── PasswordRatingComponent.css │ │ │ │ ├── PasswordRatingComponent.tsx │ │ │ │ └── PasswordRule.d.ts │ │ │ └── side-navigation/ │ │ │ ├── SecretExplorer.tsx │ │ │ ├── SecretTree.tsx │ │ │ ├── SecretsDirectoryService.ts │ │ │ └── SecretsFilterService.ts │ │ ├── explorer-app.tsx │ │ ├── search-app/ │ │ │ ├── CollectionItems.tsx │ │ │ ├── SearchApplication.css │ │ │ ├── SearchApplication.tsx │ │ │ ├── SearchResults.tsx │ │ │ ├── SearchResultsView.tsx │ │ │ └── SecretText.tsx │ │ ├── search-app.tsx │ │ ├── secrets/ │ │ │ ├── AsyncPasswordHealthCollector.ts │ │ │ ├── Gopass.ts │ │ │ ├── deriveIconFromSecretName.ts │ │ │ └── useCopySecretToClipboard.ts │ │ └── types/ │ │ ├── electron-is-accelerator.d.ts │ │ ├── fallback.d.ts │ │ ├── promise-timeout.d.ts │ │ └── string-replace-to-array.d.ts │ └── shared/ │ └── settings.ts ├── test/ │ ├── Gopass.test.ts │ ├── PasswordHealthIndicator.test.ts │ ├── SecretsDirectoryService.test.ts │ ├── SecretsFilterService.test.ts │ ├── __snapshots__/ │ │ └── SecretsDirectoryService.test.ts.snap │ ├── deriveIconFromSecretName.test.ts │ └── mock/ │ └── electron-mock.ts ├── tsconfig.json ├── tslint.json ├── webpack.base.config.js ├── webpack.main.config.js ├── webpack.main.prod.config.js ├── webpack.renderer.explorer.config.js ├── webpack.renderer.explorer.dev.config.js ├── webpack.renderer.explorer.prod.config.js ├── webpack.renderer.search.config.js ├── webpack.renderer.search.dev.config.js └── webpack.renderer.search.prod.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/on-push.yml ================================================ name: On push (tests, build) on: push jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 with: node-version: '14.16' registry-url: 'https://registry.npmjs.org' - run: npm i - run: npm test build: runs-on: ${{ matrix.os }} strategy: matrix: os: [macos-latest, ubuntu-latest, windows-latest] steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 with: node-version: '14.16' registry-url: 'https://registry.npmjs.org' - name: Build/release Electron app uses: samuelmeuli/action-electron-builder@v1 with: # GitHub token, automatically provided to the action (no need to define this secret in the repo settings) github_token: ${{ secrets.github_token }} # this action will not release release: false ================================================ FILE: .gitignore ================================================ node_modules/ .idea/ *.iml dist/ release/ ================================================ FILE: .huskyrc ================================================ { "hooks": { "pre-commit": "npm run lint && npm test" } } ================================================ FILE: .nvmrc ================================================ 14.16 ================================================ FILE: .prettierrc ================================================ { "singleQuote": true, "jsxSingleQuote": true, "arrowParens": "avoid", "printWidth": 160, "tabWidth": 4, "semi": false, "trailingComma": "none", "bracketSpacing": true } ================================================ FILE: .vscode/launch.json ================================================ { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Launch Program", "program": "${workspaceFolder}/dist/main.js" } ] } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 codecentric AG Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Gopass UI [![Latest release](https://img.shields.io/github/release/codecentric/gopass-ui.svg)](https://github.com/codecentric/gopass-ui/releases/latest) Gopass UI logo ## What is Gopass and Gopass UI? > [Gopass](https://github.com/gopasspw/gopass) is a rewrite of the pass password manager in Go with the aim of making it cross-platform and adding additional features – (Taken from Github) `Gopass UI` is wrapping `gopass` from your command line. It makes your life easier by providing a graphical user interface to search and manage your secrets. It looks like this: GIF demonstrating core features of Gopass UI In addition there is a search window that can be opened with `(cmd || ctrl) + shift + p`. ## How can I use it? For most platforms just [download the latest packaged application](https://github.com/codecentric/gopass-ui/releases/latest) from our releases and install it. We provide binaries for MacOS, Windows and Linux (deb, RPM, snap, AppImage, Gentoo). For more details see [supported platforms and packages](docs/platforms-and-packages.md). Of course, you need to have [Gopass](https://github.com/gopasspw/gopass) up and running. We also recommend to use a dialog-based PIN-entry-tool for typing in GPG passphrases like [pinentry-mac for MacOS](https://formulae.brew.sh/formula/pinentry-mac) instead of using the default terminal-based. ### Platform notice We'll only test the MacOS builds and **are not able to offer support for Linux and Windows releases**. We are happy to review your pull requests addressing any of such issues. ## Issues and Contribution Feel free to report any usage issue. We are very keen about your feedback and appreciate any help. You'd like to help us developing Gopass UI? Awesome! We are looking forward to your pull requests, issues and participation in discussion. ## Development See how to get started in our [development documentation](docs/development.md). ================================================ FILE: docs/development.md ================================================ ## Development ### Clone and install dependencies First, clone the repository and navigate inside: ```bash git clone https://github.com/codecentric/gopass-ui.git && cd gopass-ui/ ``` Then, install the dependencies: ```bash nvm use # make sure that nvm is installed on your machine and it installs the requested Node version npm install ``` ### Development The app is divided into one main process and two renderer processes. One renderer process is for the global search window, the other one for the main explorer window. All processes have to be started **simultaneously** in different console tabs: ```bash # don't forget nvm use && npm install from the previous section ;-) # run this in a pane for powering the main process (the "backend") npm run start-main-dev # run this in a pane for the renderer of the main/explorer window npm run start-renderer-explorer-dev # run this in a pane for the renderer of the search window npm run start-renderer-search-dev ``` This will start the application with hot-reloading so you can instantly start developing and see the changes in the open application. ### Testing We use Jest for tests. Currently the project contains (too less) unit and integration tests. Unit tests should have no dependency to the local machine except the Node environment we're setting up. Integration tests can also involve system binaries like Gopass, GPG and so on – you got the point ;-) Run them with `npm test` and `npm run test:integration`. ### Linting This project contains `prettier` and `tslint`. **TLDR:** Prettier assists during development. Tslint is ran in a Husky pre-commit hook together with unit tests and in Github actions (see `.github/workflows/`). **Prettier** is more aggressive because it is designed opinionated. It will find and correct more . The only options we decide on are to be found in `.prettierrc`. We use Prettier to enforce and apply code style during development process. Right after saving an edited file it will correct code style mistakes! In VSCode this comes with the Prettier extension already. In JetBrains IntelliJ IDEA/Webstorm [this can be easily configured](https://prettier.io/docs/en/webstorm.html). On the CLI, feel free to use `npm run prettier:check` and `npm run prettier:write`. **Tslint** is used to check the code style baseline before commiting staged code and while running CI. Feel free to use the scripts `npm run lint` for linting and `npm run lint:fix` for fixing simple issues automatically to make code comply to the baseline style. ### Production packaging We use [Electron builder](https://www.electron.build/) to build and package the application. See how we use it to package all targeted platforms and formats in [our release docs](./releasing.md). Packaging will create all results in `releases` folder. ================================================ FILE: docs/platforms-and-packages.md ================================================ ## Supported Platforms Gopass-ui is available for the following platforms: * MacOS (.dmg) * Windows (.exe) * Linux, see next section ### Linux Packages For Linux the following packages are provided: * .deb (download [here](https://github.com/codecentric/gopass-ui/releases/latest)) * .rpm (download [here](https://github.com/codecentric/gopass-ui/releases/latest)) * .snap (download [here](https://github.com/codecentric/gopass-ui/releases/latest)) * .pacman (download [here](https://github.com/codecentric/gopass-ui/releases/latest)) * .AppImage (download [here](https://github.com/codecentric/gopass-ui/releases/latest)) * Gentoo: `emerge app-admin/gopass-ui` ([gentoo overlay](https://gitlab.awesome-it.de/overlays/awesome), thanks [@danielcb](https://github.com/danielcb)) ================================================ FILE: docs/releasing.md ================================================ ## Releasing and Publishing Gopass UI This documents the steps it needs to release and publish a new version of Gopass UI to Github. ### In the codebase 1. Let's check if code style and tests are okay: `npm run release:check`. If there are issues, fix them first. 2. Increment the version number in `package.json` and do `npm i` to reflect it within `package-lock.json`. 3. Build the releases for your local platform to verify everything is working: `npm run release`. This takes a while. If successful, the binaries were built in `release/` 4. Build the releases for [all supported platforms and packages](./platforms-and-packages.md): `npm run release:full` 5. As we know that everything worked, commit and push the version change ### Draft and public release on Github 1. [Draft a new release](https://github.com/codecentric/gopass-ui/releases/new) 2. Choose the created Git tag. 3. Write a precise but catchy release title. Maybe something about the core topics of this release etc. 4. Describe this release in detail. What features were added or changed? Were bugs fixed? New platforms supported? 5. Attach all binaries for this release from the `release/` directory. 6. Publish and spread the word! 🎉🎉🎉 ================================================ FILE: mocks/fileMock.js ================================================ module.exports = 'test-file-stub' ================================================ FILE: mocks/styleMock.js ================================================ module.exports = {} ================================================ FILE: package.json ================================================ { "name": "gopass-ui", "version": "0.8.0", "description": "Awesome UI for the gopass CLI – a password manager for your daily business", "main": "./dist/main.js", "scripts": { "build-main": "cross-env NODE_ENV=production webpack --config webpack.main.prod.config.js", "build-renderer-search": "cross-env NODE_ENV=production webpack --config webpack.renderer.search.prod.config.js", "build-renderer-explorer": "cross-env NODE_ENV=production webpack --config webpack.renderer.explorer.prod.config.js", "build": "npm run build-main && npm run build-renderer-explorer && npm run build-renderer-search", "start-renderer-search-dev": "NODE_OPTIONS=\"--max-old-space-size=2048\" webpack-dev-server --config webpack.renderer.search.dev.config.js", "start-renderer-explorer-dev": "NODE_OPTIONS=\"--max-old-space-size=2048\" webpack-dev-server --config webpack.renderer.explorer.dev.config.js", "start-main-dev": "webpack --config webpack.main.config.js && electron ./dist/main.js", "prestart": "npm run build", "start": "electron .", "prettier:check": "prettier --check '{src,test,mocks}/**/*.{ts,tsx,js,jsx}'", "prettier:write": "prettier --write '{src,test,mocks}/**/*.{ts,tsx,js,jsx}'", "lint": "tslint '{src,test,mocks}/**/*.{ts,tsx,js,jsx}' --project ./tsconfig.json", "lint:fix": "tslint '{src,test,mocks}/**/*.{ts,tsx}' --project ./tsconfig.json --fix", "test": "npm run test:unit", "test:unit": "jest --testRegex '\\.test\\.tsx?$'", "test:unit:watch": "jest --testRegex '\\.test\\.tsx?$' --watch", "test:integration": "jest --testRegex '\\.itest\\.ts$'", "release:check": "npm run lint && npm test", "release": "npm run release:check && npm run build && electron-builder --publish onTag", "release:full": "npm run release:check && npm run build && electron-builder --mac dmg --win --linux deb rpm snap AppImage pacman", "postinstall": "electron-builder install-app-deps" }, "jest": { "transform": { "^.+\\.tsx?$": "ts-jest" }, "testRegex": "\\.?test\\.tsx?$", "moduleFileExtensions": [ "ts", "tsx", "js", "json", "node" ], "moduleNameMapper": { "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/mocks/fileMock.js", "\\.(s?css|sass)$": "/mocks/styleMock.js", "^electron$": "/test/mock/electron-mock.ts" } }, "build": { "productName": "Gopass UI", "appId": "de.codecentric.gopassui", "directories": { "output": "release" }, "files": [ "dist/", "node_modules/", "package.json" ], "mac": { "category": "public.app-category.productivity" }, "publish": [ "github" ] }, "repository": { "type": "git", "url": "git+ssh://git@github.com:codecentric/gopass-ui.git" }, "author": { "name": "codecentric AG", "email": "info@codecentric.de", "url": "https://www.codecentric.de" }, "contributors": [ { "name": "Matthias Rütten", "email": "matthias.ruetten@codecentric.de" }, { "name": "Jonas Verhoelen", "email": "jonas.verhoelen@codecentric.de" } ], "license": "SEE LICENSE", "bugs": { "url": "https://github.com/codecentric/gopass-ui/issues" }, "homepage": "https://github.com/codecentric/gopass-ui", "devDependencies": { "@types/dateformat": "^3.0.1", "@types/electron-devtools-installer": "2.2.0", "@types/electron-settings": "3.1.2", "@types/history": "^4.7.2", "@types/jest": "^26.0.23", "@types/lodash": "^4.14.170", "@types/node": "^14.17.4", "@types/react": "^16.8.13", "@types/react-dom": "^16.8.3", "@types/react-hot-loader": "^4.1.0", "@types/react-router": "^4.4.5", "@types/react-router-dom": "^4.2.0", "@types/react-test-renderer": "^16.0.0", "@types/webdriverio": "^5.0.0", "@types/webpack-env": "^1.16.0", "awesome-typescript-loader": "^5.2.1", "copy-webpack-plugin": "^5.1.1", "cross-env": "^7.0.3", "css-loader": "^2.1.0", "dateformat": "^4.5.1", "electron": "13.6.6", "electron-builder": "^22.14.3", "electron-devtools-installer": "3.2.0", "electron-mock-ipc": "0.3.9", "file-loader": "^3.0.1", "html-webpack-plugin": "^3.2.0", "husky": "^1.3.1", "image-webpack-loader": "^4.6.0", "jest": "^27.0.5", "node-sass": "^7.0.0", "pagination-calculator": "^1.0.0", "prettier": "^2.0.5", "react-hot-loader": "^4.6.3", "react-test-renderer": "^16.2.0", "sass-loader": "^7.3.1", "source-map-loader": "^0.2.4", "spectron": "^15.0.0", "style-loader": "^0.23.1", "ts-jest": "^27.0.3", "tslint": "^5.15.0", "tslint-config-airbnb": "^5.4.2", "tslint-config-prettier": "^1.18.0", "tslint-react": "^4.0.0", "typescript": "^3.8.3", "webpack": "^4.29.0", "webpack-cli": "^3.3.11", "webpack-dev-server": "^3.11.0", "webpack-merge": "^4.2.1" }, "dependencies": { "@electron/remote": "^1.2.0", "animate.css": "^3.7.2", "electron-is-accelerator": "^0.2.0", "electron-log": "^4.3.5", "electron-settings": "^3.2.0", "fix-path": "^3.0.0", "history": "^4.10.1", "lodash": "^4.17.21", "material-design-icons": "^3.0.1", "materialize-css": "^1.0.0", "promise-timeout": "^1.3.0", "react": "^16.13.1", "react-animated-css": "^1.2.1", "react-dom": "^16.13.1", "react-keyboard-event-handler": "^1.5.4", "react-materialize": "^2.6.0", "react-router": "^5.1.2", "react-router-dom": "^5.1.2", "react-treebeard": "^3.2.4", "string-replace-to-array": "^1.0.3" } } ================================================ FILE: src/main/AppUtilities.ts ================================================ import * as electronSettings from 'electron-settings' import { DEFAULT_SYSTEM_SETTINGS, DEFAULT_USER_SETTINGS, SystemSettings, UserSettings } from '../shared/settings' export const installExtensions = async () => { const installer = require('electron-devtools-installer') const forceDownload = !!process.env.UPGRADE_EXTENSIONS const extensions = ['REACT_DEVELOPER_TOOLS'] return Promise.all(extensions.map(name => installer.default(installer[name], forceDownload))).catch(console.info) } export const getSystemSettings = (): SystemSettings => { return (electronSettings.get('system_settings') as any) || DEFAULT_SYSTEM_SETTINGS } export const getUserSettings = (): UserSettings => { return (electronSettings.get('user_settings') as any) || DEFAULT_USER_SETTINGS } ================================================ FILE: src/main/AppWindows.ts ================================================ import { BrowserWindow, Menu, app, nativeTheme } from 'electron' import * as url from 'url' import * as path from 'path' export const createMainWindow = (): BrowserWindow => { nativeTheme.themeSource = 'light' const mainWindow = new BrowserWindow({ width: 1000, height: 600, center: true, title: 'Gopass UI', icon: path.join(__dirname, 'assets', 'icon.png'), webPreferences: { enableRemoteModule: true, nodeIntegration: true, contextIsolation: false, worldSafeExecuteJavaScript: false } }) if (process.env.NODE_ENV !== 'production') { mainWindow.loadURL('http://localhost:2003') mainWindow.webContents.openDevTools() } else { mainWindow.loadURL( url.format({ pathname: path.join(__dirname, 'explorer', 'index.html'), protocol: 'file:', slashes: true }) ) } return mainWindow } export const createSearchWindow = (show: boolean): BrowserWindow => { const searchWindow = new BrowserWindow({ show, width: process.env.NODE_ENV !== 'production' ? 1200 : 600, height: 600, frame: false, center: true, skipTaskbar: true, title: 'Gopass UI Search Window', resizable: false, webPreferences: { enableRemoteModule: true, nodeIntegration: true, contextIsolation: false, worldSafeExecuteJavaScript: false } }) searchWindow.setMenu(null) if (process.env.NODE_ENV !== 'production') { searchWindow.loadURL('http://localhost:2004') searchWindow.webContents.openDevTools() } else { searchWindow.loadURL( url.format({ pathname: path.join(__dirname, 'search', 'index.html'), protocol: 'file:', slashes: true }) ) } return searchWindow } export const hideMainWindow = (mainWindow: BrowserWindow | null) => { if (mainWindow) { if (app.hide) { // Linux and MacOS app.hide() } else { // for Windows mainWindow.blur() mainWindow.hide() } } } export const buildContextMenu = (mainWindow: BrowserWindow | null, searchWindow: BrowserWindow | null) => Menu.buildFromTemplate([ { label: 'Explorer', click: () => { if (mainWindow) { mainWindow.show() } else { mainWindow = createMainWindow() } } }, { label: 'Search', click: () => { if (searchWindow) { searchWindow.show() } else { searchWindow = createSearchWindow(true) } } }, { type: 'separator' }, { label: 'Quit', click: () => { app.quit() } } ]) ================================================ FILE: src/main/GopassExecutor.ts ================================================ import { exec } from 'child_process' import { IpcMainEvent } from 'electron' export interface GopassOptions { executionId: string command: string pipeTextInto?: string args?: string[] } export default class GopassExecutor { public static async handleEvent(event: IpcMainEvent, options: GopassOptions) { const argsString = options.args ? ` ${options.args.join(' ')}` : '' const pipeText = options.pipeTextInto ? `echo "${options.pipeTextInto}" | ` : '' const command = `${pipeText}gopass ${options.command}${argsString}` exec(command, (err: Error | null, stdout: string, stderr: string) => { event.sender.send(`gopass-answer-${options.executionId}`, { status: err ? 'ERROR' : 'OK', executionId: options.executionId, payload: err ? stderr : stdout }) }) } } ================================================ FILE: src/main/index.ts ================================================ import { app, BrowserWindow, Event, globalShortcut, ipcMain, IpcMainEvent, Tray, session, shell, Accelerator } from 'electron' import { URL } from 'url' import * as path from 'path' import * as fixPath from 'fix-path' import * as electronSettings from 'electron-settings' import { SystemSettings, UserSettings } from '../shared/settings' import GopassExecutor from './GopassExecutor' import { buildContextMenu, createMainWindow, createSearchWindow } from './AppWindows' import { getSystemSettings, getUserSettings, installExtensions } from './AppUtilities' const isDevMode = process.env.NODE_ENV !== 'production' fixPath() import * as remoteMain from '@electron/remote/main' remoteMain.initialize() let mainWindow: BrowserWindow | null let searchWindow: BrowserWindow | null let tray: Tray const setGlobalSearchWindowShortcut = (shortcut: Accelerator, previousShortcut?: Accelerator) => { // unregister previously used shortcut if Electron recognises it as valid if (previousShortcut) { try { globalShortcut.unregister(previousShortcut) } catch (e) { // previous shortcut was not considered as valid } } // unregister shortcut from other usages within the application // in case an error is thrown, Electron does not recognise it as valid and the method returns try { globalShortcut.unregister(shortcut) } catch (e) { return } // register shortcut once sure it is a valid and usage-free Accelerator for Electron globalShortcut.register(shortcut, () => { if (searchWindow) { if (searchWindow.isFocused()) { searchWindow.hide() } else { searchWindow.show() } } else { searchWindow = createSearchWindow(true) } }) } const setTray = (showTray: boolean) => { if (showTray) { if (!tray || tray.isDestroyed()) { if (process.platform === 'darwin') { tray = new Tray(path.join(__dirname, 'assets', 'icon-mac@2x.png')) } else if (process.platform === 'linux') { tray = new Tray(path.join(__dirname, 'assets', 'icon@2x.png')) } else { tray = new Tray(path.join(__dirname, 'assets', 'icon.png')) } tray.setToolTip('Gopass UI') tray.setContextMenu(buildContextMenu(mainWindow, searchWindow)) } } else { if (tray && !tray.isDestroyed()) { tray.destroy() } } } const listenToIpcEvents = () => { ipcMain.on('gopass', GopassExecutor.handleEvent) ipcMain.on('getUserSettings', (event: IpcMainEvent) => { event.returnValue = getUserSettings() }) ipcMain.on('hideSearchWindow', () => { if (searchWindow) { searchWindow.hide() } }) ipcMain.on('getSystemSettings', (event: IpcMainEvent) => { event.returnValue = getSystemSettings() }) ipcMain.on('updateUserSettings', (_: Event, update: Partial) => { const current = getUserSettings() const all = { ...current, ...update } // modify aspects of application where updated settings need take effect if (update.searchShortcut && update.searchShortcut !== current.searchShortcut) { setGlobalSearchWindowShortcut(update.searchShortcut, current.searchShortcut) } if (update.showTray && update.showTray !== current.showTray) { setTray(update.showTray) } if (update.startOnLogin && update.startOnLogin !== current.startOnLogin) { configureStartOnLogin(update.startOnLogin) } electronSettings.set('user_settings', all as any) }) ipcMain.on('updateSystemSettings', (_: Event, update: Partial) => { electronSettings.set('system_settings', { ...getSystemSettings(), ...update } as any) }) } const configureStartOnLogin = (startOnLogin: boolean) => { app.setLoginItemSettings({ openAtLogin: startOnLogin, openAsHidden: true }) } const setup = async () => { /** * Adds a restrictive default CSP for all fetch directives to all HTTP responses of the web server. * About default-src: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/default-src * Electron reference: https://electronjs.org/docs/tutorial/security#6-define-a-content-security-policy */ if (session.defaultSession && !isDevMode) { session.defaultSession.webRequest.onHeadersReceived((details, callback) => { callback({ responseHeaders: { ...details.responseHeaders, 'Content-Security-Policy': [`default-src 'self' 'unsafe-inline'; connect-src https://api.github.com`] } }) }) } if (isDevMode) { await installExtensions() } const settings = getUserSettings() mainWindow = createMainWindow() mainWindow.on('closed', () => { app.quit() }) searchWindow = createSearchWindow(false) setGlobalSearchWindowShortcut(settings.searchShortcut, undefined) setTray(settings.showTray) configureStartOnLogin(settings.startOnLogin) listenToIpcEvents() } app.on('ready', setup) app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit() } }) app.on('activate', () => { if (mainWindow === null) { mainWindow = createMainWindow() } if (searchWindow === null) { searchWindow = createSearchWindow(false) } }) /** * Prevent navigation to every target that lays outside of the Electron application (localhost:2003 and localhost:2004) * Reference: https://electronjs.org/docs/tutorial/security#12-disable-or-limit-navigation */ app.on('web-contents-created', (event, contents) => { contents.on('will-navigate', (navigationEvent, navigationUrl) => { const parsedUrl = new URL(navigationUrl) if (!parsedUrl.origin.startsWith('http://localhost:200')) { navigationEvent.preventDefault() } }) }) /** * Prevents unwanted modules from 'remote' from being used. * Reference: https://electronjs.org/docs/tutorial/security#16-filter-the-remote-module */ const allowedRemoteModules = new Set(['app']) app.on('remote-get-builtin', (event, webContents, moduleName) => { if (!allowedRemoteModules.has(moduleName)) { event.preventDefault() console.warn(`Blocked module "${moduleName}"`) } }) const allowedModules = new Set() const proxiedModules = new Map() app.on('remote-require', (event, webContents, moduleName) => { if (proxiedModules.has(moduleName)) { const proxiedModule = proxiedModules.get(moduleName) event.returnValue = proxiedModule console.warn(`Proxied remote-require of module "${moduleName}" to "${proxiedModule}"`) } if (!allowedModules.has(moduleName)) { event.preventDefault() console.warn(`Blocked remote-require of module "${moduleName}"`) } }) const allowedGlobals = new Set() app.on('remote-get-global', (event, webContents, globalName) => { if (!allowedGlobals.has(globalName)) { event.preventDefault() } }) app.on('remote-get-current-window', event => { event.preventDefault() }) app.on('remote-get-current-web-contents', event => { event.preventDefault() }) ================================================ FILE: src/renderer/common/Settings.ts ================================================ import { ipcRenderer } from 'electron' import { SystemSettings, UserSettings } from '../../shared/settings' import set = Reflect.set export class Settings { public static getUserSettings(): UserSettings { return ipcRenderer.sendSync('getUserSettings') } public static updateUserSettings(settings: Partial) { ipcRenderer.send('updateUserSettings', settings) } public static getSystemSettings(): SystemSettings { return ipcRenderer.sendSync('getSystemSettings') } public static updateSystemSettings(settings: Partial) { ipcRenderer.send('updateSystemSettings', settings) } } ================================================ FILE: src/renderer/common/notifications/Notification.tsx ================================================ import * as React from 'react' import * as m from 'react-materialize' import { Animated } from 'react-animated-css' import { useNotificationContext } from './NotificationProvider' const DEFAULT_TIMEOUT = 3000 export default function NotificationView(props: { dismissTimeout?: number }) { const { notification, hide, isHidden } = useNotificationContext() const { dismissTimeout = DEFAULT_TIMEOUT } = props React.useEffect(() => { let timeoutId = 0 if (notification && dismissTimeout !== 0) { timeoutId = window.setTimeout(() => hide(), dismissTimeout) } return () => { if (timeoutId !== 0) { clearTimeout(timeoutId) } } }, [notification, dismissTimeout]) return ( {notification && ( {notification ? notification.message : ''} hide()}> close )} ) } ================================================ FILE: src/renderer/common/notifications/NotificationProvider.tsx ================================================ import * as React from 'react' export interface Notification { status: 'OK' | 'ERROR' message: string } export interface NotificationContext { notification?: Notification isHidden: boolean show: (notification: Notification) => void hide: () => void } const Context = React.createContext(null) export function useNotificationContext() { const context = React.useContext(Context) if (!context) { throw Error('NO Context!') } return context } export default function NotificationProvider({ children }: any) { const [notification, setNotification] = React.useState() const [isHidden, setIsHidden] = React.useState(false) return ( { setNotification(newNotification) setIsHidden(false) }, hide: () => { setIsHidden(true) setTimeout(() => { setNotification(undefined) }, 1000) } }} > {children} ) } ================================================ FILE: src/renderer/components/ExternalLink.tsx ================================================ import * as React from 'react' import { shell } from 'electron' export function ExternalLink(props: { url: string; children: any }) { const { url, children } = props return ( shell.openExternal(url)}> {children} ) } ================================================ FILE: src/renderer/components/GoBackNavigationButton.tsx ================================================ import * as React from 'react' import { withRouter } from 'react-router' import { History } from 'history' import { RoundActionButton } from './RoundActionButton' const GoBackNavigationButton = (props: { history: History }) => (
props.history.replace('/')} />
) export default withRouter(GoBackNavigationButton) ================================================ FILE: src/renderer/components/PaginatedTable.tsx ================================================ import * as React from 'react' import * as m from 'react-materialize' import { paginationCalculator } from 'pagination-calculator' import { PageInformation } from 'pagination-calculator/dist/paginationCalculator' export interface TableColumn { fieldName: string label: string } export interface TableRow { id: string [fieldName: string]: string | React.ReactNode } export interface PaginatedTableProps { columns: TableColumn[] rows: TableRow[] } export interface PaginatedTableState { page: number pageSize: number } export default class PaginatedTable extends React.Component { constructor(props: PaginatedTableProps) { super(props) this.state = { page: 1, pageSize: 7 // TODO: make configurable through settings page } } public render() { const { columns, rows } = this.props const pagination = paginationCalculator({ total: rows.length, current: this.state.page, pageSize: this.state.pageSize, pageLimit: Math.ceil(rows.length / this.state.pageSize) }) const pageRows = rows.slice(pagination.showingStart - 1, pagination.showingEnd) return ( <> {columns.map(column => ( {column.label} ))} {pageRows.map(row => ( {columns.map(column => ( {row[column.fieldName]} ))} ))} {this.renderPagination(pagination)} ) } private renderPagination(pagination: PageInformation) { return ( ) } private changeToPage = (maxPageNumber: number) => { return (page: number) => { if (page <= maxPageNumber && page >= 1) { this.setState({ page }) } } } } ================================================ FILE: src/renderer/components/RoundActionButton.tsx ================================================ import * as React from 'react' import * as m from 'react-materialize' export interface RoundActionBtnProps { icon: string onClick?: () => void } export const RoundActionButton = ({ icon, onClick }: RoundActionBtnProps) => ( ) ================================================ FILE: src/renderer/components/loading-screen/LoadingScreen.css ================================================ .loading-screen-wrapper { display: flex; align-items: center; justify-content: center; height: 500px; font-size: 24px; } .loading-screen-message { width: 40%; min-width: 300px; text-align: center; } ================================================ FILE: src/renderer/components/loading-screen/LoadingScreen.tsx ================================================ import * as React from 'react' import * as m from 'react-materialize' import './LoadingScreen.css' const WAITING_TEXTS = [ 'Loading, please wait...', 'Still doing something...', 'It seems to takes some time...', 'Doop di doop di douuu...', "I'm sorry that it takes longer.", 'Maybe there is a persistent problem. Sorry for that!' ] export function LoadingScreen() { const [waitingTextIndex, setWaitingTextIndex] = React.useState(0) React.useEffect(() => { const timeout = setInterval(() => { setWaitingTextIndex(waitingTextIndex + 1) }, 2000) return () => clearInterval(timeout) }, []) return (

{WAITING_TEXTS[waitingTextIndex % WAITING_TEXTS.length]}

) } ================================================ FILE: src/renderer/components/tree/TreeComponent.tsx ================================================ import * as React from 'react' import * as t from 'react-treebeard' import { globalStyle } from './TreeStyle' import { TreeHeader as Header } from './TreeHeader' export interface Tree { name: string toggled?: boolean loading?: boolean children?: Tree[] path: string } export interface TreeComponentProps { tree: Tree onLeafClick: (leafId: string) => void } interface TreeComponentState { selectedNode?: any } export default class TreeComponent extends React.Component { public state: TreeComponentState = {} public render() { return } private onToggle = (node: any, toggled: boolean) => { // if no children (thus being a leaf and thereby an entry), trigger the handler if (!node.children || node.children.length === 0) { this.props.onLeafClick(node.path) } // previously selected node is no more active if (this.state.selectedNode) { this.state.selectedNode.active = false } // newly selected node shall be active node.active = true // ...and toggled if having children if (node.children) { node.toggled = toggled } this.setState({ selectedNode: node }) if (node.children && node.children.length === 1) { this.onToggle(node.children[0], true) } } } ================================================ FILE: src/renderer/components/tree/TreeHeader.tsx ================================================ import * as React from 'react' import * as m from 'react-materialize' import { deriveIconFromSecretName } from '../../secrets/deriveIconFromSecretName' export const TreeHeader = ({ style, node }: any) => { let iconType = 'folder' const isLeaf = !node.children && node.path if (!node.children && node.path) { iconType = deriveIconFromSecretName(node.name) } return (
{node.children && (
chevron_right
)} {iconType} {node.name}
) } ================================================ FILE: src/renderer/components/tree/TreeStyle.ts ================================================ const white = '#FFFFFF' const none = 'none' export const globalStyle = { tree: { base: { listStyle: none, backgroundColor: white, margin: 0, padding: 0, color: '#000000', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;', fontSize: '18px' }, node: { base: { position: 'relative', borderLeft: '1px solid #ececec', padding: '4px 12px 4px', marginLeft: '8px' }, link: { cursor: 'pointer', position: 'relative', display: 'block' }, activeLink: { background: white }, toggle: { base: { display: none }, wrapper: { position: 'absolute', top: '50%', left: '50%', margin: '-7px 0 0 -7px', height: '14px' }, height: 0, width: 0, arrow: { fill: white, strokeWidth: 0 } }, header: { base: { display: 'inline-block', verticalAlign: 'top', color: '#555', cursor: 'pointer' } }, subtree: { listStyle: none, paddingLeft: '60px' }, loading: { color: '#E2C089' } } } } ================================================ FILE: src/renderer/explorer-app/ExplorerApplication.css ================================================ .secret-explorer { position: fixed; z-index: 10; top: 0; left: 0; bottom: 0; width: 450px; overflow: auto; min-width: 300px; border-right: 1px solid rgba(0, 0, 0, 0.14); } .secret-explorer .search-bar { background: #f9f9f9; border-bottom: 1px solid rgba(0, 0, 0, 0.14); position: sticky; top: 0; z-index: 10; } .secret-explorer .search-bar * { margin: 0 !important; border: none !important; } .secret-explorer .search-bar > div { padding: 0 !important; } .secret-explorer .search-bar input { box-sizing: border-box !important; padding: 24px !important; } .secret-explorer > ul > li { border: 0; } .secret-explorer .chevron { display: inline-block; user-select: none; } .secret-explorer .chevron.toggled .material-icons { transform: rotate(90deg); } .secret-explorer .icon-wrapper > .material-icons:only-child { margin-left: 30px; } .secret-explorer .chevron .material-icons { margin: 0; } .secret-explorer i.material-icons { position: relative; top: 7px; margin-right: 8px; } .main-content { resize: both; padding-left: 450px; } .m-top { margin-top: 55px; } .panel-headline { margin-top: 0; } .link { cursor: pointer; } span.code { font-family: Consolas, monospace; } .card-panel pre { margin-bottom: 0px; } .password-strength-sum { height: 100px; width: 100px; align-items: center; justify-content: center; text-align: center; border-radius: 50%; display: flex; font-size: 42px; font-weight: 200; } .password-strength-sum.red { color: white; } ================================================ FILE: src/renderer/explorer-app/ExplorerApplication.tsx ================================================ import * as React from 'react' import SecretExplorer from './side-navigation/SecretExplorer' import MainContent from './MainContent' import './ExplorerApplication.css' const ExplorerApplication = () => { return ( <> ) } export default ExplorerApplication ================================================ FILE: src/renderer/explorer-app/GithubService.ts ================================================ export interface GithubTag { url: string ref: string } export default class GithubService { public static getTagsOfRepository(owner: string, repositoryName: string): Promise { return new Promise((resolve, reject) => { const httpRequest = new XMLHttpRequest() const url = `https://api.github.com/repos/${owner}/${repositoryName}/git/refs/tags` httpRequest.open('GET', url) httpRequest.onload = e => { if (httpRequest.status >= 200 && httpRequest.status < 300) { resolve(JSON.parse(httpRequest.response) as GithubTag[]) } else { reject({ status: httpRequest.status, statusText: httpRequest.statusText }) } } httpRequest.send() }) } } ================================================ FILE: src/renderer/explorer-app/MainContent.tsx ================================================ import * as React from 'react' import * as m from 'react-materialize' import { match, Route } from 'react-router-dom' import SettingsPage from './pages/SettingsPage' import HomePage from './pages/HomePage' import MainNavigation from './components/MainNavigation' import GoBackNavigation from '../components/GoBackNavigationButton' import Notification from '../common/notifications/Notification' import NotificationProvider from '../common/notifications/NotificationProvider' import PasswordHealthOverview from './pages/PasswordHealthPage' import AddSecretPage from './pages/AddSecretPage' import SecretDetailsPage from './pages/details/SecretDetailsPage' import MountsPage from './pages/MountsPage' import AddMountPage from './pages/AddMountPage' const Routes = () => ( <> ( <> )} /> ; location: { search?: string } }) => { const secretName = atob(props.match.params.encodedSecretName) const isAdded = props.location.search ? props.location.search === '?added' : false return ( <> ) }} /> ( <> )} /> } /> ( <> )} /> } /> ( <> )} /> ) const MainContent = () => (
) export default MainContent ================================================ FILE: src/renderer/explorer-app/SecretsProvider.tsx ================================================ import * as React from 'react' import { Tree } from '../components/tree/TreeComponent' import SecretsFilterService from './side-navigation/SecretsFilterService' import SecretsDirectoryService from './side-navigation/SecretsDirectoryService' import Gopass from '../secrets/Gopass' export interface SecretsContext { tree: Tree searchValue: string applySearchToTree: (searchValue: string) => void reloadSecretNames: () => Promise } const Context = React.createContext(undefined) export const useSecretsContext = () => { const context = React.useContext(Context) if (!context) { throw Error('no secrets context!') } return context } export const SecretsProvider = ({ children }: { children: React.ReactNode }) => { const [tree, setTree] = React.useState({ name: 'Stores', toggled: true, children: [], path: '' }) const [searchValue, setSearchValue] = React.useState('') const [secretNames, setSecretNames] = React.useState([]) const applySearchToTree = (newSearchValue?: string, updatedSecretNames?: string[]) => { let searchValueToUse = searchValue if (newSearchValue !== undefined) { setSearchValue(newSearchValue) searchValueToUse = newSearchValue } const filteredSecretNames = SecretsFilterService.filterBySearch(updatedSecretNames || secretNames, searchValueToUse) const newTree: Tree = SecretsDirectoryService.secretPathsToTree(filteredSecretNames, tree, filteredSecretNames.length <= 15) setTree(newTree) } const loadSecretsAndBuildTree = async (newSearchValue: string | undefined = undefined) => { const allSecretNames = await Gopass.getAllSecretNames() setSecretNames(allSecretNames) applySearchToTree(newSearchValue !== undefined ? newSearchValue : searchValue, allSecretNames) } const providerValue: SecretsContext = { tree, searchValue, reloadSecretNames: () => loadSecretsAndBuildTree(), applySearchToTree } return {children} } ================================================ FILE: src/renderer/explorer-app/components/EnvironmentTest.tsx ================================================ import * as React from 'react' import * as m from 'react-materialize' import { timeout } from 'promise-timeout' import { shell } from 'electron' import Gopass from '../../secrets/Gopass' import { ExternalLink } from '../../components/ExternalLink' import { Settings } from '../../common/Settings' type ErrorDetails = 'GOPASS_CONNECTION' | 'DECRYPTION' | undefined export function EnvironmentTest() { const [environmentTestStatus, setEnvironmentTestStatus] = React.useState<'PENDING' | 'RUNNING' | 'OK' | 'ERROR'>('PENDING') const [errorDetails, setErrorDetails] = React.useState() function reset() { setEnvironmentTestStatus('PENDING') setErrorDetails(undefined) } function executeTest() { setEnvironmentTestStatus('RUNNING') timeout(Gopass.getAllSecretNames(), 1500) .then(([firstEntry]) => { if (firstEntry) { timeout(Gopass.show(firstEntry), 10000) .then(() => { setEnvironmentTestStatus('OK') Settings.updateSystemSettings({ environmentTestSuccessful: true }) }) .catch(() => { setEnvironmentTestStatus('ERROR') Settings.updateSystemSettings({ environmentTestSuccessful: false }) setErrorDetails('DECRYPTION') }) } }) .catch(() => { setEnvironmentTestStatus('ERROR') Settings.updateSystemSettings({ environmentTestSuccessful: false }) setErrorDetails('GOPASS_CONNECTION') }) } switch (environmentTestStatus) { case 'PENDING': return case 'RUNNING': return case 'OK': return default: case 'ERROR': return } } function PendingContent(props: { executeTest: () => void }) { return ( <> Your system has to meet the following requirements for Gopass UI to work properly:
  1. Gopass needs to be installed and configured to be up and running 🙂
  2. MacOS: you should use pinentry-mac as a GPG passphrase dialog tool (available{' '} as Brew formulae)

During the environment test you might be asked for your GPG passphrase. Please unlock your GPG keypair by entering it.

Test your environment ) } function RunningContent() { return (


Tests are running...
) } function ErrorContent(props: { errorDetails: ErrorDetails; reset: () => void }) { return (
error

Oops, something went wrong.

{props.errorDetails && ( <> {props.errorDetails === 'DECRYPTION' && <>It wasn't possible to decrypt your secrets.} {props.errorDetails === 'GOPASS_CONNECTION' && <>It wasn't possible to access the gopass CLI.}

)} Do you need help getting started?{' '} shell.openExternal('https://github.com/codecentric/gopass-ui/issues')}>Please create an issue.

restart
) } function OkContent() { return (
done
Everything looks fine
) } ================================================ FILE: src/renderer/explorer-app/components/LastVersionInfo.tsx ================================================ import * as React from 'react' import { app } from '@electron/remote' import GithubService, { GithubTag } from '../GithubService' import { ExternalLink } from '../../components/ExternalLink' import { Settings } from '../../common/Settings' const ONE_HOUR_IN_MILLIS = 3600000 const VERSION_CHECK_INTERVAL = ONE_HOUR_IN_MILLIS export const LatestVersionInfo = () => { const { releaseCheck } = Settings.getSystemSettings() const [tags, setTags] = React.useState([]) React.useEffect(() => { const millisNow = new Date().getTime() const shouldFetchTags = !releaseCheck || !releaseCheck.lastCheckTimestamp || millisNow - VERSION_CHECK_INTERVAL > releaseCheck.lastCheckTimestamp if (shouldFetchTags) { GithubService.getTagsOfRepository('codecentric', 'gopass-ui').then(newTags => { setTags(newTags) Settings.updateSystemSettings({ releaseCheck: { lastCheckTimestamp: millisNow, results: newTags } }) }) } else { setTags(releaseCheck.results) } }, []) const lastTag = tags[tags.length - 1] const lastTagName = lastTag ? lastTag.ref.slice(10, lastTag.ref.length) : '' const appVersion = app.getVersion() if (lastTagName) { return lastTagName.includes(appVersion) ? ( <> You have the latest version {appVersion} of Gopass UI installed 🎉 ) : ( <> Your Gopass UI version ({appVersion}) is out of date 😕 Make sure you got the latest release of Gopass UI:  {`${lastTagName} on Github`} ) } return null } ================================================ FILE: src/renderer/explorer-app/components/MainNavigation.tsx ================================================ import * as React from 'react' import { History } from 'history' import { withRouter } from 'react-router' import { RoundActionButton } from '../../components/RoundActionButton' import { useNotificationContext } from '../../common/notifications/NotificationProvider' import Gopass from '../../secrets/Gopass' import { useSecretsContext } from '../SecretsProvider' interface MainNavigationViewProps { history: History } function MainNavigationComponent({ history }: MainNavigationViewProps) { const notificationContext = useNotificationContext() const secretsContext = useSecretsContext() const refreshGopassStores = async () => { try { await Gopass.sync() await secretsContext.reloadSecretNames() notificationContext.show({ status: 'OK', message: 'Your stores have been synchronised successfully.' }) } catch (err) { notificationContext.show({ status: 'ERROR', message: `Oops, something went wrong: ${JSON.stringify(err)}` }) } } return (
history.replace('/')} /> history.replace('/add-secret')} /> history.replace('/settings')} /> history.replace('/mounts')} /> history.replace('/password-health')} />
) } export default withRouter(MainNavigationComponent) ================================================ FILE: src/renderer/explorer-app/components/PasswordStrengthInfo.tsx ================================================ import * as React from 'react' import * as m from 'react-materialize' interface PasswordStrengthInfoProps { strength: number labelContent: any } export const PasswordStrengthInfo = ({ strength, labelContent }: PasswordStrengthInfoProps) => ( <> ) ================================================ FILE: src/renderer/explorer-app/pages/AddMountPage.tsx ================================================ import * as React from 'react' import * as m from 'react-materialize' import { RouteComponentProps, withRouter } from 'react-router' import Gopass, { Mount } from '../../secrets/Gopass' import { RoundActionButton } from '../../components/RoundActionButton' import { useSecretsContext } from '../SecretsProvider' import { useNotificationContext } from '../../common/notifications/NotificationProvider' function AddMountPage({ history }: RouteComponentProps) { const notificationContext = useNotificationContext() const secretsContext = useSecretsContext() const [mount, setMount] = React.useState({ name: '', path: '' }) const addMount = async () => { if (mount.path && mount.name) { try { await Gopass.addMount(mount) await secretsContext.reloadSecretNames() history.replace('/mounts') } catch (err) { if (typeof err === 'string') { if (err === 'duplicate-name') { notificationContext.show({ status: 'ERROR', message: `A mount named "${mount.name}" does already exist` }) } if (err.includes('Doubly mounted path')) { notificationContext.show({ status: 'ERROR', message: `The path "${mount.path}" is already in use by another mount` }) } } else { notificationContext.show({ status: 'ERROR', message: `Unexpected error while adding mount: ${JSON.stringify(err)}` }) } } } } return ( <>
history.replace('/mounts')} />

New Mount

Create a new mount that shall be managed by Gopass as a password store from now on. setMount({ ...mount, name: value })} label='Name' /> setMount({ ...mount, path: value })} label='Directory path' /> Save ) } export default withRouter(AddMountPage) ================================================ FILE: src/renderer/explorer-app/pages/AddSecretPage.css ================================================ .secret-value-textarea { height: 64px; font-size: 16px; resize: vertical; border: 1px solid #9e9e9e; margin-top: 5px; } .secret-value-field-row { margin: 0 0.75rem; width: calc(100% - 1.5rem); } .secret-value-label { fontSize: 0.8rem; color: #9e9e9e; } ================================================ FILE: src/renderer/explorer-app/pages/AddSecretPage.tsx ================================================ import * as React from 'react' import * as m from 'react-materialize' import { RouteComponentProps, withRouter } from 'react-router' import Gopass from '../../secrets/Gopass' import { passwordSecretRegex } from '../../secrets/deriveIconFromSecretName' import { PasswordStrengthInfo } from '../components/PasswordStrengthInfo' import { PasswordRater } from '../password-health/PasswordRater' import './AddSecretPage.css' import { Settings } from '../../common/Settings' import { useSecretsContext } from '../SecretsProvider' interface AddSecretPageState { name?: string value?: string } class AddSecretPage extends React.Component { constructor(props: any) { super(props) this.state = { name: undefined, value: this.generateRandomValue(Settings.getUserSettings().secretValueLength) } } public render() { const { name, value } = this.state const nameIndicatesPassword = name ? passwordSecretRegex.test(name) : false const entity = nameIndicatesPassword ? 'Password' : 'Secret' const nameLabel = `Secret name (${nameIndicatesPassword ? 'detected password' : 'e.g. store/my/new/secret/name'})` const valueLabel = `${entity} value` const shuffleButtonLabel = `Shuffle ${nameIndicatesPassword ? 'password' : 'value'}` const currentPasswordValueRating = PasswordRater.ratePassword(value || '') return ( <>

New {entity}

Adds new secrets to your Gopass stores. After clicking the Add-button, your new secret will be pushed to remote directly.