├── .gitignore
├── .rescriptsrc.js
├── .vscode
└── launch.json
├── .webpack.config.js
├── LICENSE
├── README.md
├── package.json
├── public
├── electron.js
├── favicon.ico
├── index.html
└── manifest.json
├── src
├── App.css
├── App.js
├── App.test.js
├── actions
│ ├── index.js
│ └── types.js
├── components
│ ├── AddButton.js
│ ├── Header.js
│ ├── KeyList.js
│ ├── Tooltip.js
│ └── styled.js
├── helpers
│ ├── history.js
│ └── index.js
├── index.css
├── index.js
├── logo.svg
├── reducers
│ ├── index.js
│ └── keys_reducer.js
├── screens
│ ├── AddForm.js
│ └── Browser.js
└── serviceWorker.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 | /dist
14 |
15 | # misc
16 | .DS_Store
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
--------------------------------------------------------------------------------
/.rescriptsrc.js:
--------------------------------------------------------------------------------
1 | module.exports = [require.resolve('./.webpack.config.js')]
2 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "chrome",
9 | "request": "launch",
10 | "name": "Launch Chrome against localhost",
11 | "url": "http://localhost:3000",
12 | "webRoot": "${workspaceFolder}/src"
13 | }
14 | ]
15 | }
--------------------------------------------------------------------------------
/.webpack.config.js:
--------------------------------------------------------------------------------
1 | // define child rescript
2 | module.exports = config => {
3 | config.target = "electron-renderer";
4 | return config;
5 | };
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Vipin Ajayakumar
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # SSH key manager (Mac only)
3 | [](https://opensource.org/licenses/MIT)
4 |
5 | > A Graphical User Interface (GUI) for easily managing ssh keys on your Mac.
6 |
7 | 
8 |
9 | ## Installation
10 |
11 | Just download the latest
12 | release. Easy peasy!
13 |
14 | ## Features
15 |
16 | - List all your existing ssh keys
17 | - Copy public key
18 | - Delete a key
19 | - Add a new key with optional passphrase and comment
20 |
21 | ## Contributing
22 |
23 | I made this app just for the fun of it and to learn a bit about [Electron](https://electronjs.org/) and
24 | [React](https://reactjs.org/). At the moment the functionality is very basic, and there's a lot that can
25 | be done to make it better. If you want to contribute, please get in touch or submit a
26 | pull request.
27 |
28 | ## Support
29 |
30 | Reach out to me at one of the following places.
31 |
32 | - Website at `vipinajayakumar.com`
33 | - Twitter at `@vipinajayakumar`
34 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ssh-key-manager",
3 | "homepage": "./",
4 | "version": "1.0.0",
5 | "private": true,
6 | "dependencies": {
7 | "electron-is-dev": "1.1.0",
8 | "ls": "0.2.1",
9 | "react": "^16.8.6",
10 | "react-dom": "^16.8.6",
11 | "react-redux": "7.0.3",
12 | "react-router": "5.0.0",
13 | "react-router-dom": "5.0.0",
14 | "react-scripts": "3.0.0",
15 | "react-tooltip": "3.10.0",
16 | "redux": "4.0.1",
17 | "redux-form": "8.2.0",
18 | "redux-thunk": "2.3.0",
19 | "styled-components": "4.2.0"
20 | },
21 | "main": "public/electron.js",
22 | "scripts": {
23 | "start": "rescripts start",
24 | "build": "rescripts build",
25 | "test": "rescripts test",
26 | "eject": "react-scripts eject",
27 | "electron-dev": "concurrently \"BROWSER=none yarn start\" \"wait-on http://localhost:3000 && electron .\"",
28 | "postinstall": "electron-builder install-app-deps",
29 | "preelectron-pack": "yarn build",
30 | "electron-pack": "build -mw"
31 | },
32 | "eslintConfig": {
33 | "extends": "react-app"
34 | },
35 | "browserslist": {
36 | "production": [
37 | ">0.2%",
38 | "not dead",
39 | "not op_mini all"
40 | ],
41 | "development": [
42 | "last 1 chrome version",
43 | "last 1 firefox version",
44 | "last 1 safari version"
45 | ]
46 | },
47 | "devDependencies": {
48 | "@rescripts/cli": "0.0.10",
49 | "@rescripts/rescript-env": "0.0.10",
50 | "concurrently": "4.1.0",
51 | "electron": "5.0.1",
52 | "electron-builder": "20.39.0",
53 | "redux-devtools-extension": "2.13.8",
54 | "typescript": "3.4.5",
55 | "wait-on": "3.2.0"
56 | },
57 | "author": {
58 | "name": "Vipin Ajayakumar",
59 | "email": "vipinajayakumar@icloud.com",
60 | "url": "https://www.vipinajayakumar.com"
61 | },
62 | "build": {
63 | "appId": "com.vipinajayakumar.ssh-key-manager",
64 | "productName": "ssh-key-manager",
65 | "copyright": "Copyright © 2019 ${author}",
66 | "mac": {
67 | "category": "public.app-category.utilities"
68 | },
69 | "files": [
70 | "build/**/*",
71 | "node_modules/**/*"
72 | ],
73 | "directories": {
74 | "buildResources": "assets"
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/public/electron.js:
--------------------------------------------------------------------------------
1 | const electron = require("electron");
2 | const os = require("os");
3 |
4 | const { app, BrowserWindow, ipcMain } = electron;
5 |
6 | const path = require("path");
7 | const isDev = require("electron-is-dev");
8 |
9 | const { exec } = require("child_process");
10 |
11 | let mainWindow;
12 |
13 | function createWindow() {
14 | mainWindow = new BrowserWindow({
15 | width: 900,
16 | height: 680,
17 | webPreferences: {
18 | nodeIntegration: true
19 | }
20 | });
21 | mainWindow.loadURL(
22 | isDev
23 | ? "http://localhost:3000"
24 | : `file://${path.join(__dirname, "../build/index.html")}`
25 | );
26 | if (isDev) {
27 | // Open the DevTools.
28 | BrowserWindow.addDevToolsExtension(
29 | path.join(
30 | os.homedir(),
31 | "/Library/Application Support/Google/Chrome/Default/Extensions/fmkadmapgofadopljbjfkapdkoienihi/3.6.0_0"
32 | )
33 | );
34 | BrowserWindow.addDevToolsExtension(
35 | path.join(
36 | os.homedir(),
37 | "/Library/Application Support/Google/Chrome/Default/Extensions/lmhkpmbekcpmknklioeibfkpmmfibljd/2.17.0_0"
38 | )
39 | );
40 | mainWindow.webContents.openDevTools();
41 | }
42 | mainWindow.on("closed", () => (mainWindow = null));
43 | }
44 |
45 | app.on("ready", createWindow);
46 |
47 | app.on("window-all-closed", () => {
48 | if (process.platform !== "darwin") {
49 | app.quit();
50 | }
51 | });
52 |
53 | app.on("activate", () => {
54 | if (mainWindow === null) {
55 | createWindow();
56 | }
57 | });
58 |
59 | ipcMain.on("get:keys", event => {
60 | const homedir = process.env.HOME;
61 | const sshdir = homedir + "/.ssh";
62 | exec("ls -a " + sshdir, (err, stdout, stderr) => {
63 | if (err) {
64 | console.error(`exec error: ${err}`);
65 | return;
66 | }
67 |
68 | const filenames = stdout.split(/\r?\n/).slice(2);
69 | filenames.pop();
70 |
71 | var keys = [];
72 | const filenamesLength = filenames.length;
73 | for (var i = 0; i < filenamesLength; i++) {
74 | const filename = filenames[i];
75 | if (filename.endsWith(".pub")) {
76 | const publicKeyFilename = filename;
77 | const privateKeyFilename = filename.split(".")[0];
78 | keys.push({
79 | privateKeyFilename,
80 | publicKeyFilename,
81 | privateKeyPath: sshdir + "/" + privateKeyFilename,
82 | publicKeyPath: sshdir + "/" + publicKeyFilename
83 | });
84 | }
85 | }
86 |
87 | mainWindow.webContents.send("received:keys", keys);
88 | });
89 | });
90 |
91 | ipcMain.on("remove:key", (event, key) => {
92 | const { privateKeyPath, publicKeyPath } = key;
93 | exec(
94 | "rm " + privateKeyPath + " " + publicKeyPath,
95 | (err, stdout, stderr) => {
96 | if (err) {
97 | console.error(`exec error: ${err}`);
98 | return;
99 | }
100 |
101 | mainWindow.webContents.send("removed:key", key);
102 | }
103 | );
104 | });
105 |
106 | ipcMain.on("add:key", (event, key) => {
107 | const homedir = process.env.HOME;
108 | const sshdir = homedir + "/.ssh";
109 | const { filename, passphrase, comment } = key;
110 | var command =
111 | "ssh-keygen -t rsa -b 4096 -f " +
112 | sshdir +
113 | "/" +
114 | filename +
115 | " -N " +
116 | (passphrase ? passphrase + '""' : '""') +
117 | (comment ? " -C " + '"' + comment + '"' : "");
118 |
119 | exec(command, (err, stdout, stderr) => {
120 | if (err) {
121 | console.error(`exec error: ${err}`);
122 | return;
123 | }
124 |
125 | mainWindow.webContents.send("added:key", key);
126 | });
127 | });
128 |
129 | ipcMain.on("copy:key", (event, key) => {
130 | const { publicKeyPath } = key;
131 | exec(" pbcopy < " + publicKeyPath, (err, stdout, stderr) => {
132 | if (err) {
133 | console.error(`exec error: ${err}`);
134 | return;
135 | }
136 |
137 | mainWindow.webContents.send("copied:key", { publicKey: stdout, key });
138 | });
139 | });
140 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bluprince13/ssh-key-manager/3d31bfdb8441fffd029cd6808a70ed21bb06d423/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
22 |
23 | SSH key manager
24 |
25 |
26 |
27 |
28 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "SSH key manager",
3 | "name": "SSH key manager",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | *,
2 | *::after,
3 | *::before {
4 | margin: 0;
5 | padding: 0;
6 | box-sizing: inherit;
7 |
8 | min-height: 0;
9 | min-width: 0;
10 | }
11 |
12 | html {
13 | font-size: 62.5%;
14 | }
15 |
16 | body {
17 | box-sizing: border-box;
18 | }
19 |
20 | body {
21 | font-family: "Lato", sans-serif;
22 | font-weight: 400;
23 | line-height: 1.7;
24 | color: #777;
25 | padding: 0 3rem;
26 | max-width: 114rem;
27 | margin: auto auto;
28 | }
29 |
30 | .clearfix::after {
31 | content: "";
32 | clear: both;
33 | display: table;
34 | }
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Router, Route, Switch } from "react-router-dom";
3 | import { Provider } from "react-redux";
4 | import history from "./helpers/history";
5 |
6 | import store from "./reducers";
7 |
8 | import Browser from "./screens/Browser";
9 | import AddForm from "./screens/AddForm";
10 | import Header from "./components/Header";
11 |
12 | import { ThemeProvider } from "styled-components";
13 |
14 | const theme = {
15 | lightest: "#e0f7fa",
16 | light: "#b2ebf2",
17 | highlight: "red",
18 | dark: "#4dd0e1",
19 | darkest: "#0097a7",
20 |
21 | small: "0.75rem",
22 | medium: "1rem"
23 | };
24 |
25 | export default function App() {
26 | return (
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/src/actions/index.js:
--------------------------------------------------------------------------------
1 | import { ipcRenderer } from "electron";
2 | import { GET_KEYS, REMOVE_KEY, COPY_KEY } from "./types";
3 | import history from "../helpers/history";
4 | import { copyToClipboard } from "../helpers";
5 |
6 | export const getKeys = () => dispatch => {
7 | ipcRenderer.send("get:keys");
8 |
9 | ipcRenderer.on("received:keys", (event, keys) => {
10 | dispatch({ type: GET_KEYS, payload: { keys } });
11 | });
12 | };
13 |
14 | export const addKey = key => dispatch => {
15 | ipcRenderer.send("add:key", key);
16 |
17 | ipcRenderer.on("added:key", (event, key) => {
18 | history.push("/");
19 | });
20 | };
21 |
22 | export const removeKey = key => dispatch => {
23 | ipcRenderer.send("remove:key", key);
24 |
25 | ipcRenderer.on("removed:key", (event, key) => {
26 | dispatch({ type: REMOVE_KEY, payload: { key } });
27 | });
28 | };
29 |
30 | export const copyKey = key => dispatch => {
31 | ipcRenderer.send("copy:key", key);
32 |
33 | ipcRenderer.on("copied:key", (event, { key, publicKey }) => {
34 | copyToClipboard(publicKey);
35 | dispatch({ type: COPY_KEY, payload: { key, publicKey } });
36 | });
37 | };
38 |
--------------------------------------------------------------------------------
/src/actions/types.js:
--------------------------------------------------------------------------------
1 | export const GET_KEYS = "get_keys";
2 | export const ADD_KEY = "add_key";
3 | export const REMOVE_KEY = "remove_key";
4 | export const COPY_KEY = "copy_key";
5 |
--------------------------------------------------------------------------------
/src/components/AddButton.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { withRouter } from "react-router-dom";
3 |
4 | import { StyledButton, StyledIcon } from "../components/styled";
5 | class AddButton extends Component {
6 | render() {
7 | return (
8 | {
11 | this.props.history.push("/add");
12 | }}
13 | >
14 |
15 |
16 | );
17 | }
18 | }
19 |
20 | export default withRouter(AddButton);
21 |
--------------------------------------------------------------------------------
/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import styled from "styled-components";
3 | import { Link } from "react-router-dom";
4 |
5 | const StyledHeader = styled.div`
6 | position: relative;
7 |
8 | padding: 1rem;
9 | text-align: left;
10 | background: #00bcd4;
11 | font-size: 3rem;
12 |
13 | margin-bottom: 1rem;
14 | `;
15 |
16 | class Header extends Component {
17 | render() {
18 | return (
19 |
20 |
24 | SSH key manager
25 |
26 |
27 | );
28 | }
29 | }
30 |
31 | export default Header;
32 |
--------------------------------------------------------------------------------
/src/components/KeyList.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import styled from "styled-components";
3 |
4 | import ReactTooltip from "react-tooltip";
5 | import Tooltip from "./Tooltip";
6 |
7 | const StyledRow = styled.li`
8 | background: ${props => props.theme.lightest};
9 | font-size: ${props => props.theme.medium};
10 |
11 | position: relative;
12 | display: block;
13 | padding: 0.75rem 1.25rem;
14 | margin-bottom: -1px;
15 | border: 1px solid rgba(0, 0, 0, 0.125);
16 |
17 | &:hover {
18 | background: ${props => props.theme.light};
19 | }
20 |
21 | &:first-child {
22 | border-top-left-radius: 0.25rem;
23 | border-top-right-radius: 0.25rem;
24 | }
25 |
26 | &:last-child {
27 | margin-bottom: 0;
28 | border-bottom-right-radius: 0.25rem;
29 | border-bottom-left-radius: 0.25rem;
30 | }
31 | `;
32 |
33 | const StyledIcon = styled.i`
34 | &: hover {
35 | color: ${props => props.theme.highlight};
36 | }
37 | `;
38 |
39 | const StyledListGroup = styled.ul`
40 | display: -webkit-box;
41 | display: -ms-flexbox;
42 | display: flex;
43 | -webkit-box-orient: vertical;
44 | -webkit-box-direction: normal;
45 | -ms-flex-direction: column;
46 | flex-direction: column;
47 | padding-left: 0;
48 | margin-bottom: 0;
49 | `;
50 |
51 | class KeyList extends Component {
52 | componentDidUpdate() {
53 | ReactTooltip.rebuild();
54 | }
55 |
56 | handleCopy(key) {
57 | this.props.copyKey(key);
58 |
59 | setTimeout(() => {
60 | ReactTooltip.hide();
61 | }, 1500);
62 | }
63 |
64 | handleRemove(key) {
65 | this.props.removeKey(key);
66 | ReactTooltip.hide();
67 | }
68 |
69 | renderKeys() {
70 | const keys = this.props.keys;
71 |
72 | return keys.map(key => {
73 | const { privateKeyFilename } = key;
74 | return (
75 |
76 |
77 | {privateKeyFilename}
78 |
79 | {
81 | this.handleCopy(key);
82 | }}
83 | data-tip="Public key copied"
84 | data-event="click"
85 | data-for="copied"
86 | >
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | {
98 | this.handleRemove(key);
99 | }}
100 | >
101 |
102 |
103 |
104 |
105 |
106 | );
107 | });
108 | }
109 |
110 | render() {
111 | return {this.renderKeys()};
112 | }
113 | }
114 |
115 | export default KeyList;
116 |
--------------------------------------------------------------------------------
/src/components/Tooltip.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import ReactTooltip from "react-tooltip";
3 |
4 | export default class Tooltip extends Component {
5 | render() {
6 | return (
7 |
14 | );
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/styled.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const StyledIcon = styled.i``;
4 |
5 | export const StyledButton = styled.button`
6 | background-color: ${props => props.theme.light};
7 | display: block;
8 |
9 | height: 3rem;
10 | width: 3rem;
11 | border-radius: 50%;
12 |
13 | &: hover {
14 | background-color: ${props => props.theme.dark};
15 |
16 | ${StyledIcon} {
17 | color: ${props => props.theme.highlight};
18 | }
19 | }
20 |
21 | &: focus {
22 | outline: none;
23 | border: none;
24 | }
25 |
26 | &: active {
27 | background-color: ${props => props.theme.darkest};
28 | }
29 | `;
30 |
31 |
--------------------------------------------------------------------------------
/src/helpers/history.js:
--------------------------------------------------------------------------------
1 | import { createBrowserHistory } from 'history'
2 |
3 | export default createBrowserHistory({
4 | /* pass a configuration object here if needed */
5 | })
--------------------------------------------------------------------------------
/src/helpers/index.js:
--------------------------------------------------------------------------------
1 | export const copyToClipboard = text => {
2 | var dummy = document.createElement("input");
3 | document.body.appendChild(dummy);
4 | dummy.setAttribute('value', text);
5 | dummy.select();
6 | document.execCommand("copy");
7 | document.body.removeChild(dummy);
8 | };
9 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
6 | sans-serif;
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | }
10 |
11 | code {
12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
13 | monospace;
14 | }
15 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import * as serviceWorker from './serviceWorker';
6 |
7 | ReactDOM.render(, document.getElementById('root'));
8 |
9 | // If you want your app to work offline and load faster, you can change
10 | // unregister() to register() below. Note this comes with some pitfalls.
11 | // Learn more about service workers: https://bit.ly/CRA-PWA
12 | serviceWorker.unregister();
13 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers, createStore, applyMiddleware } from "redux";
2 | import thunk from "redux-thunk";
3 | import { reducer as formReducer } from "redux-form";
4 |
5 | import { composeWithDevTools } from "redux-devtools-extension";
6 |
7 | import keysReducer from "./keys_reducer";
8 |
9 | const rootReducer = combineReducers({
10 | keys: keysReducer,
11 | form: formReducer
12 | });
13 |
14 | const store = createStore(
15 | rootReducer,
16 | composeWithDevTools(applyMiddleware(thunk))
17 | );
18 |
19 | export default store;
20 |
--------------------------------------------------------------------------------
/src/reducers/keys_reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | GET_KEYS,
3 | REMOVE_KEY,
4 | } from "../actions/types";
5 |
6 | const INITIAL_STATE = [];
7 |
8 | export default (state = INITIAL_STATE, action) => {
9 | switch (action.type) {
10 | case GET_KEYS:
11 | const { keys } = action.payload;
12 | return keys;
13 | case REMOVE_KEY:
14 | const { key: keyToRemove } = action.payload;
15 | const new_keys = state.filter(key => {
16 | return (
17 | key.privateKeyFilename !== keyToRemove.privateKeyFilename
18 | );
19 | });
20 | return new_keys;
21 | default:
22 | return state;
23 | }
24 | };
25 |
--------------------------------------------------------------------------------
/src/screens/AddForm.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { connect } from "react-redux";
3 | import { Field, reduxForm } from "redux-form";
4 | import { withRouter } from "react-router-dom";
5 | import styled, { keyframes, css } from "styled-components";
6 |
7 | import * as actions from "../actions";
8 | import Tooltip from "../components/Tooltip";
9 | import { StyledButton, StyledIcon } from "../components/styled";
10 |
11 | const StyledForm = styled.form`
12 | margin: 1rem;
13 | `;
14 |
15 | const StyledFieldGroup = styled.div`
16 | display: block;
17 | `;
18 |
19 | const StyledField = styled.div`
20 | margin-bottom: 1rem;
21 | position: relative;
22 | `;
23 |
24 | const StyledLabel = styled.label`
25 | font-size: ${props => props.theme.small};
26 | `;
27 |
28 | const StyledInput = styled.input`
29 | display: block;
30 |
31 | margin-top: 0.4rem;
32 |
33 | width: 100%;
34 | -webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
35 | -moz-box-sizing: border-box; /* Firefox, other Gecko */
36 | box-sizing: border-box;
37 | padding: 0.375rem 0.75rem;
38 |
39 | font-size: ${props => props.theme.medium};
40 | line-height: 1.5;
41 | color: #495057;
42 | background-color: ${props => props.theme.lightest}
43 | background-clip: padding-box;
44 | border: 1px solid #ced4da;
45 | border-radius: 0.25rem;
46 | transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
47 |
48 | &::placeholder {
49 | font-size: ${props => props.theme.small};
50 | }
51 |
52 | &:focus {
53 | color: #495057;
54 | background-color: #fff;
55 | border-color: #80bdff;
56 | outline: 0;
57 | box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
58 | }
59 | `;
60 |
61 | const StyledError = styled.span`
62 | color: ${props => props.theme.highlight};
63 | font-size: ${props => props.theme.small};
64 | `;
65 |
66 | const StyledButtonGroup = styled.div`
67 | margin: 1rem;
68 | `;
69 |
70 | const shadowpulse = keyframes`
71 | 0% {
72 | box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.2);
73 | }
74 | 100% {
75 | box-shadow: 0 0 0 1rem rgba(0, 0, 0, 0);
76 | }
77 | `;
78 |
79 | const LoadingButton = styled(StyledButton)`
80 | animation: ${props =>
81 | props.submitted
82 | ? css`
83 | ${shadowpulse} 1s infinite
84 | `
85 | : ""};
86 | `;
87 |
88 | const validate = (values, props) => {
89 | const errors = {};
90 | if (!values.filename) {
91 | errors.filename = "Required";
92 | }
93 |
94 | const { keys } = props;
95 | const isFilenameUsed = keys.filter(
96 | key => key.privateKeyFilename === values.filename
97 | );
98 | if (isFilenameUsed.length !== 0) {
99 | errors.filename = "This filename is already taken";
100 | }
101 |
102 | return errors;
103 | };
104 |
105 | const renderField = ({ input, label, type, meta: { touched, error } }) => (
106 |
107 | {label}
108 |
109 | {touched && error && {error}}
110 |
111 | );
112 |
113 | class AddForm extends Component {
114 | state = {
115 | submitted: false
116 | };
117 |
118 | componentDidMount() {
119 | const { dispatch, change } = this.props;
120 | const { keys } = this.props;
121 | const isDefaultUsed = keys.filter(
122 | key => key.privateKeyFilename === "id_rsa"
123 | );
124 | if (isDefaultUsed.length === 0) {
125 | dispatch(change("filename", "id_rsa"));
126 | }
127 | }
128 |
129 | render() {
130 | const { handleSubmit, pristine, reset, submitting } = this.props;
131 |
132 | const goHome = () => {
133 | this.props.history.push("/");
134 | };
135 |
136 | const submit = values => {
137 | this.submitted = true;
138 | this.props.addKey(values);
139 | };
140 |
141 | return (
142 |
143 |
144 |
150 |
156 |
162 |
163 |
164 |
165 |
172 |
173 |
174 |
175 |
183 |
184 |
185 |
186 |
194 |
195 |
196 |
197 |
198 |
199 | );
200 | }
201 | }
202 |
203 | function mapStateToProps(state) {
204 | return { keys: state.keys };
205 | }
206 |
207 | AddForm = reduxForm({
208 | form: "AddForm", // a unique identifier for this form
209 | validate, // <--- validation function given to redux-form
210 | initialValues: {
211 | passphrase: ""
212 | }
213 | })(AddForm);
214 | AddForm = connect(
215 | mapStateToProps,
216 | actions
217 | )(AddForm);
218 | AddForm = withRouter(AddForm);
219 |
220 | export default AddForm;
221 |
--------------------------------------------------------------------------------
/src/screens/Browser.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { connect } from "react-redux";
3 | import styled from "styled-components";
4 |
5 | import KeyList from "../components/KeyList";
6 | import * as actions from "../actions";
7 | import AddButton from "../components/AddButton";
8 |
9 | const StyledContainer = styled.div`
10 | margin: 1rem;
11 | `;
12 | class Browser extends Component {
13 | componentWillMount() {
14 | this.props.getKeys();
15 | }
16 |
17 | render() {
18 | return (
19 |
20 |
25 |
26 |
27 | );
28 | }
29 | }
30 |
31 | function mapStateToProps(state) {
32 | return { keys: state.keys };
33 | }
34 |
35 | export default connect(
36 | mapStateToProps,
37 | actions
38 | )(Browser);
39 |
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl)
104 | .then(response => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get('content-type');
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf('javascript') === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then(registration => {
113 | registration.unregister().then(() => {
114 | window.location.reload();
115 | });
116 | });
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config);
120 | }
121 | })
122 | .catch(() => {
123 | console.log(
124 | 'No internet connection found. App is running in offline mode.'
125 | );
126 | });
127 | }
128 |
129 | export function unregister() {
130 | if ('serviceWorker' in navigator) {
131 | navigator.serviceWorker.ready.then(registration => {
132 | registration.unregister();
133 | });
134 | }
135 | }
136 |
--------------------------------------------------------------------------------