├── .babelrc ├── .dev.sample.json ├── .github ├── CODEOWNERS ├── FUNDING.yml ├── changelog_configuration.json └── workflows │ ├── lint.yaml │ └── release.yaml ├── .gitignore ├── .jshintrc ├── .nvmrc ├── CHANGELOG ├── DEVELOPMENT.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── reviewers ├── firefox-beta.md └── firefox.md ├── scripts ├── build-zip.js └── generateBuildConfig.js ├── src ├── background │ ├── context-menu.js │ ├── create-alias.js │ ├── index.js │ └── onboarding.js ├── content_script │ ├── input_tools.css │ └── input_tools.js ├── icons │ ├── icon_128.png │ ├── icon_16.png │ ├── icon_32.png │ ├── icon_48.png │ ├── icon_96.png │ ├── icon_beta_128.png │ └── icon_beta_48.png ├── images │ ├── arrow-up.png │ ├── back-button.svg │ ├── chrome-permission-screenshot.png │ ├── firefox-permission-screenshot.png │ ├── horizontal-logo.svg │ ├── icon-copy.svg │ ├── icon-dropdown.svg │ ├── icon-more.svg │ ├── icon-puzzle.png │ ├── icon-settings.svg │ ├── icon-simplelogin.png │ ├── icon-trash.svg │ ├── loading-three-dots.svg │ ├── loading.svg │ ├── proton.svg │ └── sl-button-demo.png ├── manifest.json └── popup │ ├── APIService.js │ ├── App-color.scss │ ├── App-scrollbar.scss │ ├── App.scss │ ├── App.vue │ ├── EventManager.js │ ├── Navigation.js │ ├── SLStorage.js │ ├── Theme.js │ ├── Theme.scss │ ├── Utils.js │ ├── buildConfig.json │ ├── components │ ├── AliasMoreOptions.vue │ ├── ApiKeySetting.vue │ ├── AppSettings.vue │ ├── ExpandTransition.vue │ ├── Header.vue │ ├── Login.vue │ ├── Main.vue │ ├── NewAliasResult.vue │ ├── ReverseAlias.vue │ ├── SelfHostSetting.vue │ ├── SplashScreen.vue │ └── TextareaAutosize.vue │ ├── popup.html │ └── popup.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@babel/plugin-proposal-optional-chaining" 4 | ], 5 | "presets": [ 6 | ["@babel/preset-env", { 7 | "useBuiltIns": "usage", 8 | "corejs": 3, 9 | "targets": { 10 | // https://jamie.build/last-2-versions 11 | "browsers": ["> 0.25%", "not ie 11", "not op_mini all"] 12 | } 13 | }] 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.dev.sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "DEFAULT_API_URL": "https://app.simplelogin.io", 3 | "EXTRA_ALLOWED_DOMAINS": [], 4 | "permissions": [] 5 | } -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | ## code changes will send PR to following users 2 | * @acasajus @cquintana92 @nguyenkims -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: simplelogin 2 | open_collective: simplelogin 3 | custom: ["https://www.paypal.me/RealSimpleLogin"] 4 | -------------------------------------------------------------------------------- /.github/changelog_configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "${{CHANGELOG}}", 3 | "pr_template": "- ${{TITLE}} #${{NUMBER}}", 4 | "empty_template": "- no changes", 5 | "categories": [ 6 | { 7 | "title": "## 🚀 Features", 8 | "labels": ["feature"] 9 | }, 10 | { 11 | "title": "## 🐛 Fixes", 12 | "labels": ["fix", "bug"] 13 | }, 14 | { 15 | "title": "## 🔧 Enhancements", 16 | "labels": ["enhancement"] 17 | } 18 | ], 19 | "ignore_labels": ["ignore"], 20 | "tag_resolver": { 21 | "method": "sort" 22 | } 23 | } -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | lint: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout repository 9 | uses: actions/checkout@v2 10 | 11 | - name: Install NodeJS 12 | uses: actions/setup-node@v3 13 | with: 14 | node-version: 16 15 | 16 | - name: Perform linting 17 | run: | 18 | npm install 19 | npm run prettier:check -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Build the extension 2 | on: 3 | push: 4 | tags: 5 | - '[0-9]+.[0-9]+.[0-9]+' 6 | 7 | jobs: 8 | create-release: 9 | name: create-release 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 1 16 | 17 | - name: Install dependencies and check tag format 18 | run: | 19 | sudo apt update && sudo apt install -y jq 20 | PACKAGE_JSON_VERSION=$(jq -Mr '.version' package.json) 21 | EXTENSION_VERSION=${GITHUB_REF#refs/tags/} 22 | if [[ "${PACKAGE_JSON_VERSION}" != "${EXTENSION_VERSION}" ]]; then 23 | echo "Tag name does not match the version in package.json" 24 | echo "package.json: [${PACKAGE_JSON_VERSION}] | tag: [${EXTENSION_VERSION}]" 25 | exit 1 26 | fi 27 | echo "EXTENSION_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV 28 | 29 | - name: Create artifacts directory 30 | run: mkdir artifacts 31 | 32 | - name: Build Changelog 33 | id: build_changelog 34 | uses: mikepenz/release-changelog-builder-action@v3 35 | with: 36 | configuration: ".github/changelog_configuration.json" 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | 40 | - name: Prepare Slack notification contents 41 | run: | 42 | changelog=$(cat << EOH 43 | ${{ steps.build_changelog.outputs.changelog }} 44 | EOH 45 | ) 46 | messageWithoutNewlines=$(echo "${changelog}" | awk '{printf "%s\\n", $0}') 47 | messageWithoutDoubleQuotes=$(echo "${messageWithoutNewlines}" | sed "s/\"/'/g") 48 | echo "${messageWithoutDoubleQuotes}" 49 | 50 | echo "${messageWithoutDoubleQuotes}" > artifacts/slack-changelog 51 | 52 | - name: Create GitHub release 53 | id: release 54 | uses: actions/create-release@v1 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | with: 58 | tag_name: ${{ env.EXTENSION_VERSION }} 59 | release_name: ${{ env.EXTENSION_VERSION }} 60 | body: ${{ steps.build_changelog.outputs.changelog }} 61 | 62 | - name: Save release upload URL to artifact 63 | run: echo "${{ steps.release.outputs.upload_url }}" > artifacts/release-upload-url 64 | 65 | - name: Save version number to artifact 66 | run: echo "${{ env.EXTENSION_VERSION }}" > artifacts/release-version 67 | 68 | - name: Upload artifacts 69 | uses: actions/upload-artifact@v4 70 | with: 71 | name: artifacts 72 | path: artifacts 73 | 74 | build-release: 75 | name: build-release 76 | needs: ['create-release'] 77 | runs-on: ubuntu-latest 78 | strategy: 79 | max-parallel: 4 80 | matrix: 81 | variant: ['full', 'lite', 'mac', 'beta', 'beta-firefox'] 82 | 83 | steps: 84 | - name: Build info 85 | run: echo "Starting build process for variant = ${{ matrix.variant }}" 86 | 87 | - name: Checkout repository 88 | uses: actions/checkout@v4 89 | with: 90 | fetch-depth: 1 91 | 92 | - name: Install NodeJS 93 | uses: actions/setup-node@v3 94 | with: 95 | node-version: 16 96 | 97 | - name: Get release download URL 98 | uses: actions/download-artifact@v4 99 | with: 100 | name: artifacts 101 | path: artifacts 102 | 103 | - name: Read artifacts 104 | shell: bash 105 | run: | 106 | # Set the release_upload_url 107 | release_upload_url="$(cat artifacts/release-upload-url)" 108 | echo "RELEASE_UPLOAD_URL=$release_upload_url" >> $GITHUB_ENV 109 | echo "release upload url: $RELEASE_UPLOAD_URL" 110 | 111 | # Set the release_version 112 | release_version="$(cat artifacts/release-version)" 113 | echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV 114 | echo "release version: $RELEASE_VERSION" 115 | 116 | - name: Generate buildConfig 117 | shell: bash 118 | env: 119 | ENABLE_LOGIN_WITH_PROTON: true 120 | run: | 121 | npm run generate:buildconfig 122 | 123 | - name: Build lite version 124 | if: matrix.variant == 'lite' 125 | shell: bash 126 | run: | 127 | npm install 128 | npm run build:lite 129 | SUFFIX=lite npm run build-zip 130 | 131 | - name: Build beta version for Chromium 132 | if: matrix.variant == 'beta' 133 | shell: bash 134 | run: | 135 | npm install 136 | npm run build:beta 137 | npm run build-zip 138 | 139 | - name: Build beta version for Firefox 140 | if: matrix.variant == 'beta-firefox' 141 | shell: bash 142 | run: | 143 | npm install 144 | npm run build:firefox:beta 145 | 146 | # dist-zip/simplelogin-extension-beta-firefox-v... can be submitted to firefox 147 | SUFFIX=firefox npm run build-zip 148 | 149 | - name: Build production version for Firefox 150 | if: matrix.variant == 'beta-firefox' 151 | shell: bash 152 | run: | 153 | npm install 154 | npm run build:firefox 155 | 156 | # dist-zip/simplelogin-extension-beta-firefox-v... can be submitted to firefox 157 | SUFFIX=firefox npm run build-zip 158 | 159 | - name: Build production version for Chromium 160 | if: matrix.variant == 'full' 161 | shell: bash 162 | run: | 163 | npm install 164 | npm run build 165 | npm run build-zip 166 | 167 | - name: Build Mac version 168 | if: matrix.variant == 'mac' 169 | shell: bash 170 | run: | 171 | npm install 172 | npm run build:mac 173 | SUFFIX=mac npm run build-zip 174 | 175 | - name: Package extension 176 | run: | 177 | ZIP_NAME=$(find dist-zip -type f -name '*.zip' | head -n 1) 178 | ASSET_NAME=$(basename "${ZIP_NAME}") 179 | 180 | echo "ASSET_PATH=${ZIP_NAME}" >> $GITHUB_ENV 181 | echo "ASSET_NAME=${ASSET_NAME}" >> $GITHUB_ENV 182 | 183 | - name: Upload release archive 184 | uses: actions/upload-release-asset@v1.0.1 185 | env: 186 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 187 | with: 188 | upload_url: ${{ env.RELEASE_UPLOAD_URL }} 189 | asset_path: ${{ env.ASSET_PATH }} 190 | asset_name: ${{ env.ASSET_NAME }} 191 | asset_content_type: application/octet-stream 192 | 193 | notify: 194 | name: notify 195 | needs: ['create-release', 'build-release'] 196 | runs-on: ubuntu-latest 197 | steps: 198 | - name: Get artifacts 199 | uses: actions/download-artifact@v4 200 | with: 201 | name: artifacts 202 | path: artifacts 203 | 204 | - name: Read artifacts 205 | shell: bash 206 | run: | 207 | # Set the slack-changelog 208 | slack_changelog=$(cat artifacts/slack-changelog) 209 | echo "SLACK_CHANGELOG=$slack_changelog" >> $GITHUB_ENV 210 | echo "slack changelog: $SLACK_CHANGELOG" 211 | 212 | - name: Post notification to Slack 213 | uses: slackapi/slack-github-action@v1.19.0 214 | with: 215 | channel-id: ${{ secrets.SLACK_CHANNEL_ID }} 216 | payload: | 217 | { 218 | "blocks": [ 219 | { 220 | "type": "header", 221 | "text": { 222 | "type": "plain_text", 223 | "text": "New tag created on browser-extension", 224 | "emoji": true 225 | } 226 | }, 227 | { 228 | "type": "section", 229 | "text": { 230 | "type": "mrkdwn", 231 | "text": "*Tag: ${{ github.ref_name }}* (${{ github.sha }})" 232 | } 233 | }, 234 | { 235 | "type": "section", 236 | "text": { 237 | "type": "mrkdwn", 238 | "text": "*Changelog:*\n${{ env.SLACK_CHANGELOG }}" 239 | } 240 | } 241 | ] 242 | } 243 | env: 244 | SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /*.log 3 | /dist 4 | /dist-zip 5 | .DS_Store 6 | .idea/ 7 | .dev.json 8 | code-for-reviewer/ -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 9 3 | } -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.20.2 2 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to SimpleLogin Chrome/Firefox extension will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [3.0.7] - 2025-03-20 11 | 12 | - Do not try to rerun extension setup if already logged in 13 | 14 | ## [3.0.6] - 2024-09-04 15 | 16 | - Fix onboarding on Safari 17 | 18 | ## [3.0.1-3.0.4] - 2024-04-11 19 | 20 | - Automate firefox review assets 21 | 22 | ## [3.0.0] - 2024-04-11 23 | 24 | - Use Manifest v3 25 | 26 | ## [2.11.1] - 2024-02-22 27 | - Fix alias list not displaying on Mac 28 | 29 | ## [2.11.0] - 2024-02-15 30 | - Fix upgrade button not working 31 | - Prepare the Safari extension 32 | - Enforce the reverse alias limit 33 | 34 | ## [2.10.2] - 2023-09-18 35 | 36 | - Use internal Sentry 37 | 38 | ## [2.10.0] - 2023-07-05 39 | 40 | - Fix sl-button class conflict 41 | 42 | ## [2.9.2] - 2022-11-10 43 | - Updated gecko ID for Firefox 44 | 45 | ## [2.9.1] - 2022-11-10 46 | 47 | - Support dark and OS theme 48 | - Support communication with future Mac app 49 | 50 | ## [2.7.0] - 2022-08-02 51 | 52 | - Add "Login with Proton" 53 | - Show user info on app settings page 54 | - Remove tabs permission 55 | - Fix the "create totally random alias" button includes hostname in alias 56 | - Upgrade dependencies 57 | 58 | 59 | ## [2.6.3] - 2022-05-20 60 | 61 | - Remove cookies permission 62 | 63 | ## [2.6.2] - 2022-05-20 64 | 65 | - Fixed CI 66 | 67 | ## [2.6.1] - 2022-05-20 68 | 69 | - Add version to settings page 70 | - Improve the CI 71 | 72 | ## [2.6.0] - 2022-05-20 73 | 74 | - Require previously optional permissions on install 75 | - Improve the onboarding 76 | - Update outdated packages 77 | 78 | ## [2.5.2] - 2021-10-28 79 | 80 | Fix icon not displayed 81 | 82 | ## [2.5.1] - 2021-10-28 83 | 84 | Provide additional size icons 85 | 86 | ## [2.5.0] - 2021-10-05 87 | - Support create reverse-alias 88 | - Improve UI 89 | - Update dependencies 90 | 91 | ## [2.4.3] - 2021-08-03 92 | - Support dot sign (.) in alias prefix 93 | - Update some wordings, improve UI 94 | 95 | 96 | ## [2.4.2] - 2020-09-26 97 | - Fix onboarding screen showing up on browser restart 98 | 99 | ## [2.4.1] - 2020-09-26 100 | - Reduce display glitch on Firefox 101 | 102 | ## [2.4.0] - 2020-09-26 103 | - Ctrl+Shift+S (or Cmd+Shift+S on MacOS) to open SimpleLogin pop-up 104 | - Ctrl+Shift+X (or Cmd+Shift+X on MacOS) to create a random alias 105 | - Fix window not displayed when a new alias is created 106 | 107 | ## [2.3.1] - 2020-09-07 108 | - Fix the hostname issue 109 | 110 | ## [2.3.0] - 2020-09-05 111 | - Better onboarding process 112 | 113 | ## [2.2.0] - 2020-09-03 114 | - Fix Firefox overflow menu 115 | - Better onboarding process 116 | - Do not require tabs permission. 117 | 118 | ## [2.1.0] - 2020-08-21 119 | - Improve error alert 120 | 121 | ## [2.1.0.0] - 2020-08-13 (Beta version) 122 | - Right click to create alias 123 | - Able to change alias from name, toggle PGP 124 | - Able to choose alias's mailbox(es) 125 | - UI improved 126 | 127 | ## [2.0.0.0] - 2020-07-30 (Beta version) 128 | - Create alias via the SimpleLogin button displayed in the email field 129 | 130 | ## [1.10.0] - 2020-07-16 131 | - Able to add and edit alias note 132 | - Lots of UI touches 🎨 133 | 134 | ## [1.9.1] - 2020-07-12 135 | - Take into account the case alias creation time is expired 136 | 137 | ## [1.9.0] - 2020-07-11 138 | - Able to enable/disable an alias 139 | - Able to delete an alias 140 | 141 | ## [1.8.0] - 2020-07-08 142 | - Big refactoring to make the code more modulable 143 | - Add some UI touches & fixes: log out button on top, navigation button, etc. 144 | 145 | ## [1.7.0] - 2020-07-04 146 | You can now login with email/password! 147 | 148 | ## [1.6.0] - 2020-06-24 149 | Handle 429 error. 150 | Fix Firefox scroll bar -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # DEVELOPMENT 2 | 3 | This document contains an overview on how the extension is organized, which parts does it have and how does it work. 4 | 5 | ## General overview 6 | 7 | The extension consists of 2 main screens: 8 | 9 | - main screen: displays email alias recommendation, alias creation and existing alias. 10 | - new alias screen: when a new alias is created, user is redirected to this screen so they can copy it. 11 | 12 | 13 | ## How to change the domain where the extension is connecting to 14 | 15 | In order to change the backend URL, you will need to: 16 | 17 | 1. Copy the `.dev.sample.json` file into a `.dev.json` file. 18 | 2. Edit the `DEFAULT_API_URL` parameter and enter the URL you want to use. 19 | 3. You may need to run `npm start` again in order for the changes to take effect. 20 | 21 | ## How does the extension setup work 22 | 23 | The extension setup process works like the following: 24 | 25 | 1. The webpage sends a message (the code can be found [here](https://github.com/simple-login/app/blob/0e3be23acc7978f6e2b1127ed78dc2147cf43515/templates/onboarding/index.html#L41-L42) and [here](https://github.com/simple-login/app/blob/0e3be23acc7978f6e2b1127ed78dc2147cf43515/templates/onboarding/setup_done.html#L31-L32)) 26 | 2. The extension has a listener for events on the page, and detects it [like this](https://github.com/simple-login/browser-extension/blob/55629849838b716dabcb008898c97c4ee1118da1/src/content_script/input_tools.js#L257) 27 | 3. Once the event has been detected, the extension sends it to the background context [with this call](https://github.com/simple-login/browser-extension/blob/55629849838b716dabcb008898c97c4ee1118da1/src/content_script/input_tools.js#L260) 28 | 4. The background context [detects the event](https://github.com/simple-login/browser-extension/blob/master/src/background/index.js#L119-L120) and performs the setup. This message can only come from one of the authorized domains (see the "Add custom allowed domains" section to see how this works). 29 | 5. The setup consists on a HTTP request that will use the cookies for the SimpleLogin domain, and it will receive an API Key in the response. This API Key will be stored on the `SLStorage` and be used from then on. 30 | 6. Once the setup has been done, the user will be redirected to a page where they will be able to test the extension. 31 | 32 | Here you have a full definition of the flow: 33 | 34 | 1. Once the extension is installed, the user will be prompted with a webpage (`/onboarding`) where two things can happen: 35 | 1. If the user is already logged in, the webpage will send the message for performing the extension setup. 36 | 1. Once the setup is done, they will be redirected to the `/onboarding/final` page. 37 | 2. If the user is not logged in, they will be prompted to log in. 38 | 1. After they log in, they will be redirected to the `/onboarding/setup_done` page. 39 | 2. The page will send the message for performing the extension setup. 40 | 3. Once the setup is done, they will be redirected to the `/onboarding/final` page. 41 | 2. Once the user reaches the `/onboarding/final` page, the extension will be correctly set up, the user will be able to use it, and the page will contain the extension version at the bottom of the page 42 | 43 | 44 | ## Add custom allowed domains 45 | 46 | The messages for both performing the extension setup and for checking if it's installed are only allowed if they come from a [predefined set of origins](https://github.com/simple-login/browser-extension/blob/55629849838b716dabcb008898c97c4ee1118da1/src/background/index.js#L72-L77). 47 | 48 | However, for testing purposes there is a parameter that can be added to your dev config. You can find it in your `.dev.json`, under the name `EXTRA_ALLOWED_DOMAINS`. 49 | 50 | Keep in mind that the domains you write here will be converted to regex, so if you want to allow `*.local` you may need to write it as `.*.local`. Also take into account that only the hostname portion will be used (that means, if your page is `someserver.com:1234` only the `someserver.com` portion will be evaluated). 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 SimpleLogin 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SimpleLogin Chrome/Firefox extension 2 | --- 3 |

4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |

17 | 18 | SimpleLogin is the **open-source** privacy-first email alias and Single Sign-On (SSO) Identity Provider. 19 | 20 | More info on our website at https://simplelogin.io 21 | 22 | The extension uses VueJS with https://github.com/Kocal/vue-web-extension boilerplate. 23 | 24 | ## How to get the extension 25 | 26 | You can directly install the extension by visiting the store page for your browser: 27 | 28 | - [Google Chrome / Brave / Opera / Chromium-based](https://chrome.google.com/webstore/detail/simpleloginreceive-send-e/dphilobhebphkdjbpfohgikllaljmgbn) 29 | - [Mozilla Firefox](https://addons.mozilla.org/firefox/addon/simplelogin/) 30 | - [Microsoft Edge](https://microsoftedge.microsoft.com/addons/detail/simpleloginreceive-sen/diacfpipniklenphgljfkmhinphjlfff) 31 | 32 | ## Development information 33 | 34 | You can find more information about how the extension works and which parts it has in [DEVELOPMENT.md](./DEVELOPMENT.md) 35 | 36 | ## Contributing Guide 37 | 38 | All work on SimpleLogin Chrome/Firefox extension happens directly on GitHub. 39 | 40 | This project has been tested with Node v20.2.0 and NPM 9.6.6 41 | 42 | 43 | To run the extension locally, please first install all dependencies with `npm install`. 44 | 45 | ## Chrome 46 | 47 | Run `npm start` to generate the `/dist` folder that can be installed into Chrome. 48 | 49 | In case of `Error: error:0308010C:digital envelope routines::unsupported` error, the workaround is to accept OPEN SSL by running this command before running `npm start` 50 | 51 | ```bash 52 | export NODE_OPTIONS=--openssl-legacy-provider 53 | ```` 54 | 55 | ## Firefox 56 | 57 | Run `npm run start:firefox` to generate the `/dist` folder which can then be installed on Firefox, more info on https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Your_first_WebExtension#installing 58 | 59 | ## Code formatting 60 | 61 | The code is formatted using `prettier`, make sure to run it before creating the commit, otherwise the GitHub lint workflow will mark the check as not passing: 62 | 63 | ```bash 64 | npm run prettier:write 65 | ``` 66 | 67 | ## How to generate a release 68 | 69 | 1. Increment the version in `package.json`. 70 | 2. Update CHANGELOG with the changes. 71 | 3. Create a tag and push it to the repository. The tag name must match the version set in `package.json`. 72 | 4. Wait until the CI process generates the extension ZIP and uploads it to GitHub. You will be able to find the generated zip as an artifact attached to the [GitHub release](https://github.com/simple-login/browser-extension/releases). 73 | 5. Upload the extension to the Chrome, Firefox and Edge stores. 74 | 75 | ### Firefox 76 | 77 | For Firefox, the code source must be submitted too. To faciliate the review process, the code source can be generated using the following script 78 | 79 | For beta version: 80 | 81 | ```bash 82 | # create the code source for firefox reviewer 83 | rm -rf code-for-reviewer && mkdir code-for-reviewer 84 | 85 | # copy the minimum files 86 | cp -r src LICENSE CHANGELOG scripts package.json package-lock.json webpack.config.js .dev.sample.json .babelrc code-for-reviewer 87 | 88 | # override the readme 89 | cp reviewers/firefox-beta.md code-for-reviewer/README.md 90 | ``` 91 | 92 | For prod version 93 | 94 | ```bash 95 | # create the code source for firefox reviewer 96 | rm -rf code-for-reviewer && mkdir code-for-reviewer 97 | 98 | # copy the minimum files 99 | cp -r src LICENSE CHANGELOG scripts package.json package-lock.json webpack.config.js .dev.sample.json .babelrc code-for-reviewer 100 | 101 | # override the readme 102 | cp reviewers/firefox.md code-for-reviewer/README.md 103 | ``` 104 | 105 | 106 | ## How to build the extension locally 107 | 108 | In order to build the extension yourself, please follow these steps: 109 | 110 | - Make sure you have the dependencies installed and up-to-date with `npm install`. 111 | - Run the build process with `npm run build`. 112 | - Create the zip package with `npm run build-zip`. You will find the extension in the `dist-zip/` directory. 113 | - If you want to use it on Firefox you will need to enter the `dist/` directory and run `web-ext build`. You will find the extension in the `dist/web-ext-artifacts/` directory. 114 | 115 | - (Optional, only useful for beta build) Build beta version: change `betaRev` in `package.json`, then generate zip file using 116 | 117 | ## How to build a version for Mac 118 | 119 | For the development, you can run `npm run start:mac` for the Mac app. 120 | 121 | For the production release, `npm run build:mac` 122 | 123 | ```bash 124 | npm run build:beta && npm run build-zip 125 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simplelogin-extension", 3 | "version": "3.0.7", 4 | "betaRev": "0", 5 | "description": "SimpleLogin Browser Extension", 6 | "author": "extension@simplelogin.io", 7 | "license": "MIT", 8 | "scripts": { 9 | "prettier": "prettier \"src/**/*.{js,vue}\"", 10 | "prettier:write": "npm run prettier -- --write", 11 | "prettier:check": "prettier --check \"src/**/*.{js,vue}\"", 12 | "build": "cross-env NODE_ENV=production webpack", 13 | "build:dev": "cross-env NODE_ENV=development webpack", 14 | "build:firefox": "cross-env NODE_ENV=production FIREFOX=1 webpack", 15 | "build:firefox:beta": "cross-env NODE_ENV=production BETA=1 FIREFOX=1 webpack", 16 | "build:lite": "cross-env NODE_ENV=production LITE=1 webpack", 17 | "build:beta": "cross-env NODE_ENV=production BETA=1 webpack", 18 | "build:mac": "cross-env NODE_ENV=production MAC=1 webpack", 19 | "build-zip": "node scripts/build-zip.js", 20 | "generate:buildconfig": "node scripts/generateBuildConfig.js", 21 | "start": "cross-env HMR=true NODE_ENV=development BETA=1 webpack -- --watch", 22 | "start:mac": "cross-env HMR=true NODE_ENV=development BETA=1 MAC=1 webpack -- --watch", 23 | "start:firefox": "cross-env HMR=true NODE_ENV=development BETA=1 FIREFOX=1 webpack -- --watch" 24 | }, 25 | "dependencies": { 26 | "@fortawesome/fontawesome-svg-core": "^1.2.36", 27 | "@fortawesome/free-solid-svg-icons": "^5.15.4", 28 | "@fortawesome/vue-fontawesome": "^0.1.10", 29 | "@sentry/browser": "^5.25.0", 30 | "@sentry/integrations": "^5.30.0", 31 | "bootstrap": "^4.6.1", 32 | "bootstrap-vue": "^2.21.2", 33 | "browser": "^0.2.6", 34 | "tippy.js": "^6.3.7", 35 | "v-clipboard": "^2.2.3", 36 | "vue": "^2.6.14", 37 | "vue-js-modal": "^1.3.35", 38 | "vue-js-toggle-button": "^1.3.3", 39 | "vue-router": "^3.5.2", 40 | "vue-toasted": "^1.1.28", 41 | "webextension-polyfill": "^0.9.0" 42 | }, 43 | "devDependencies": { 44 | "@babel/core": "^7.1.2", 45 | "@babel/plugin-proposal-class-properties": "^7.10.4", 46 | "@babel/plugin-proposal-optional-chaining": "^7.0.0", 47 | "@babel/preset-env": "^7.1.0", 48 | "@babel/runtime-corejs3": "^7.4.0", 49 | "archiver": "^3.0.0", 50 | "babel-loader": "^8.0.2", 51 | "copy-webpack-plugin": "^6.4.1", 52 | "core-js": "^3.22.7", 53 | "cross-env": "^5.2.0", 54 | "css-loader": "^2.1.1", 55 | "ejs": "^3.1.8", 56 | "file-loader": "^6.2.0", 57 | "mini-css-extract-plugin": "^0.4.4", 58 | "prettier": "^2.6.2", 59 | "sass": "^1.52.1", 60 | "sass-loader": "^10.1.1", 61 | "vue-loader": "^15.4.2", 62 | "vue-template-compiler": "^2.6.14", 63 | "web-ext-types": "^2.1.0", 64 | "webpack": "^4.46.0", 65 | "webpack-cli": "^4.9.2" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /reviewers/firefox-beta.md: -------------------------------------------------------------------------------- 1 | SimpleLogin Chrome/Firefox extension 2 | --- 3 | 4 | Please find below the instructions for building the SimpleLogin extension from source. 5 | 6 | This project has been tested with Node v20.2.0 and NPM 9.6.6. 7 | 8 | Please run the following commands to install dependencies and generate a build 9 | 10 | ```bash 11 | export NODE_OPTIONS=--openssl-legacy-provider 12 | npm install 13 | npm run build:firefox:beta 14 | npm run build-zip 15 | ``` 16 | 17 | After that the build should be available in `/dist` folder. Its zip file can be found in `dist-zip` folder. -------------------------------------------------------------------------------- /reviewers/firefox.md: -------------------------------------------------------------------------------- 1 | SimpleLogin Chrome/Firefox extension 2 | --- 3 | 4 | Please find below the instructions for building the SimpleLogin extension from source. 5 | 6 | This project has been tested with Node v20.2.0 and NPM 9.6.6. 7 | 8 | Please run the following commands to install dependencies and generate a build 9 | 10 | ```bash 11 | export NODE_OPTIONS=--openssl-legacy-provider 12 | npm install 13 | npm run build:firefox 14 | npm run build-zip 15 | ``` 16 | 17 | After that the build should be available in `/dist` folder. Its zip file can be found in `dist-zip` folder. -------------------------------------------------------------------------------- /scripts/build-zip.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const process = require('process'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const archiver = require('archiver'); 7 | 8 | const DEST_DIR = path.join(__dirname, '../dist'); 9 | const DEST_ZIP_DIR = path.join(__dirname, '../dist-zip'); 10 | 11 | const extractExtensionData = () => { 12 | const extPackageJson = require('../package.json'); 13 | const distManifestJson = require('../dist/manifest.json'); 14 | const isBeta = distManifestJson.name.match(/beta/i); 15 | const betaRev = extPackageJson.betaRev; 16 | 17 | return { 18 | name: extPackageJson.name + (isBeta ? '-beta' : '-release'), 19 | version: extPackageJson.version + (isBeta ? ('.' + betaRev) : ''), 20 | }; 21 | }; 22 | 23 | const makeDestZipDirIfNotExists = () => { 24 | if(!fs.existsSync(DEST_ZIP_DIR)) { 25 | fs.mkdirSync(DEST_ZIP_DIR); 26 | } 27 | }; 28 | 29 | const buildZip = (src, dist, zipFilename) => { 30 | console.info(`Building ${zipFilename}...`); 31 | 32 | const archive = archiver('zip', { zlib: { level: 9 }}); 33 | const stream = fs.createWriteStream(path.join(dist, zipFilename)); 34 | 35 | return new Promise((resolve, reject) => { 36 | archive 37 | .directory(src, false) 38 | .on('error', err => reject(err)) 39 | .pipe(stream); 40 | 41 | stream.on('close', () => resolve()); 42 | archive.finalize(); 43 | }); 44 | }; 45 | 46 | const extractSuffix = () => { 47 | if (process.env.SUFFIX) { 48 | return `-${process.env.SUFFIX}`; 49 | } 50 | return ''; 51 | }; 52 | 53 | const main = () => { 54 | const {name, version} = extractExtensionData(); 55 | const suffix = extractSuffix(); 56 | const zipFilename = `${name}${suffix}-v${version}.zip`; 57 | 58 | makeDestZipDirIfNotExists(); 59 | 60 | buildZip(DEST_DIR, DEST_ZIP_DIR, zipFilename) 61 | .then(() => console.info('OK')) 62 | .catch(console.err); 63 | }; 64 | 65 | main(); 66 | -------------------------------------------------------------------------------- /scripts/generateBuildConfig.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const PATH = path.join(__dirname, '../src', 'popup', 'buildConfig.json'); 5 | 6 | const isLoginWithProtonEnabled = () => { 7 | const enableLoginWithProton = process.env.ENABLE_LOGIN_WITH_PROTON; 8 | if (enableLoginWithProton == undefined || enableLoginWithProton === 'true') { 9 | return true; 10 | } 11 | return false; 12 | }; 13 | 14 | const config = { 15 | features: { 16 | loginWithProtonEnabled: isLoginWithProtonEnabled() 17 | }, 18 | buildTime: new Date().getTime() 19 | }; 20 | 21 | fs.writeFileSync(PATH, JSON.stringify(config, null, 2)); 22 | -------------------------------------------------------------------------------- /src/background/context-menu.js: -------------------------------------------------------------------------------- 1 | import { handleNewRandomAlias } from "./create-alias"; 2 | import browser from "webextension-polyfill"; 3 | 4 | function displayAndCopy(alias, error) { 5 | function copyTextToClipboard(text) { 6 | if (!text) return; 7 | var textArea = document.createElement("textarea"); 8 | textArea.value = text; 9 | 10 | textArea.style.top = "0"; 11 | textArea.style.left = "0"; 12 | textArea.style.position = "fixed"; 13 | 14 | document.body.appendChild(textArea); 15 | textArea.focus(); 16 | textArea.select(); 17 | 18 | try { 19 | document.execCommand("copy"); 20 | } catch (err) {} 21 | 22 | document.body.removeChild(textArea); 23 | } 24 | 25 | function showSLDialog(message) { 26 | let slDialog = document.createElement("div"); 27 | slDialog.style.position = "fixed"; 28 | slDialog.style.bottom = "0"; 29 | slDialog.style.right = "0"; 30 | slDialog.style.margin = "0.7em"; 31 | slDialog.style.padding = "0.7em"; 32 | slDialog.style.fontFamily = "Verdana, Arial, Helvetica, sans-serif"; 33 | slDialog.style.fontSize = "1em"; 34 | slDialog.style.pointerEvents = "none"; 35 | slDialog.style.zIndex = "999999"; 36 | slDialog.style.background = "white"; 37 | slDialog.style.border = "2px solid #777"; 38 | slDialog.style.borderRadius = "5px"; 39 | slDialog.innerText = JSON.stringify(message); 40 | 41 | document.body.appendChild(slDialog); 42 | 43 | setTimeout(function () { 44 | document.body.removeChild(slDialog); 45 | }, 3000); 46 | } 47 | 48 | showSLDialog(alias ? alias + " copied to clipboard" : "ERROR: " + error); 49 | copyTextToClipboard(alias); 50 | } 51 | 52 | function generateAliasHandlerJS(tab, res) { 53 | chrome.scripting 54 | .executeScript({ 55 | target: { tabId: tab.id }, 56 | func: displayAndCopy, 57 | args: [res.alias || null, res.error || null], 58 | }) 59 | .then(() => console.log("injected a function")); 60 | } 61 | 62 | async function handleOnClickContextMenu(info, tab) { 63 | const res = await handleNewRandomAlias(info.pageUrl); 64 | generateAliasHandlerJS(tab, res); 65 | } 66 | 67 | export { handleOnClickContextMenu, generateAliasHandlerJS }; 68 | -------------------------------------------------------------------------------- /src/background/create-alias.js: -------------------------------------------------------------------------------- 1 | import { 2 | callAPI, 3 | API_ROUTE, 4 | API_ON_ERR, 5 | reloadSettings, 6 | } from "../popup/APIService"; 7 | import Utils from "../popup/Utils"; 8 | 9 | /** 10 | * Create random alias 11 | */ 12 | async function handleNewRandomAlias(currentUrl) { 13 | await reloadSettings(); 14 | const hostname = await Utils.getHostName(currentUrl); 15 | try { 16 | const res = await callAPI( 17 | API_ROUTE.NEW_RANDOM_ALIAS, 18 | { 19 | hostname, 20 | }, 21 | { 22 | note: await Utils.getDefaultNote(), 23 | }, 24 | API_ON_ERR.THROW 25 | ); 26 | 27 | return res.data; 28 | } catch (err) { 29 | // rate limit reached 30 | if (err.response.status === 429) { 31 | return { 32 | error: 33 | "Rate limit exceeded - please wait 60s before creating new alias", 34 | }; 35 | } else if (err.response.data.error) { 36 | return { 37 | error: err.response.data.error, 38 | }; 39 | } else { 40 | return { 41 | error: "Unknown error", 42 | }; 43 | } 44 | } 45 | } 46 | 47 | export { handleNewRandomAlias }; 48 | -------------------------------------------------------------------------------- /src/background/index.js: -------------------------------------------------------------------------------- 1 | import browser from "webextension-polyfill"; 2 | import APIService, { API_ROUTE } from "../popup/APIService"; 3 | import SLStorage from "../popup/SLStorage"; 4 | import Onboarding from "./onboarding"; 5 | 6 | import { handleNewRandomAlias } from "./create-alias"; 7 | import { 8 | handleOnClickContextMenu, 9 | generateAliasHandlerJS, 10 | } from "./context-menu"; 11 | import Utils from "../popup/Utils"; 12 | 13 | /** 14 | * Get app settings 15 | */ 16 | async function handleGetAppSettings() { 17 | const apiKey = await SLStorage.get(SLStorage.SETTINGS.API_KEY); 18 | return { 19 | showSLButton: 20 | apiKey !== "" && (await SLStorage.get(SLStorage.SETTINGS.SHOW_SL_BUTTON)), 21 | isLoggedIn: apiKey !== "", 22 | url: await SLStorage.get(SLStorage.SETTINGS.API_URL), 23 | SLButtonPosition: await SLStorage.get( 24 | SLStorage.SETTINGS.SL_BUTTON_POSITION 25 | ), 26 | }; 27 | } 28 | 29 | async function finalizeExtensionSetup(apiKey) { 30 | if (!apiKey) { 31 | return; 32 | } 33 | 34 | await SLStorage.set(SLStorage.SETTINGS.API_KEY, apiKey); 35 | 36 | const currentTab = await browser.tabs.query({ 37 | active: true, 38 | currentWindow: true, 39 | }); 40 | 41 | const apiUrl = await SLStorage.get(SLStorage.SETTINGS.API_URL); 42 | const url = `${apiUrl}/onboarding/final`; 43 | await browser.tabs.update(currentTab[0].id, { 44 | url, 45 | }); 46 | } 47 | 48 | async function handleExtensionSetup() { 49 | const apiUrl = await SLStorage.get(SLStorage.SETTINGS.API_URL); 50 | 51 | const url = apiUrl + API_ROUTE.GET_API_KEY_FROM_COOKIE.path; 52 | const res = await fetch(url, { 53 | method: "POST", 54 | headers: { 55 | "Content-Type": "application/json", 56 | "X-Sl-Allowcookies": true, 57 | }, 58 | body: JSON.stringify({ 59 | device: Utils.getDeviceName(), 60 | }), 61 | }); 62 | 63 | if (res.ok) { 64 | const apiRes = await res.json(); 65 | const apiKey = apiRes.api_key; 66 | finalizeExtensionSetup(apiKey); 67 | } else { 68 | console.error("api error"); 69 | } 70 | } 71 | 72 | /** 73 | * Check if a message comes from an authorized source 74 | * @param {string} url 75 | * @returns Promise 76 | */ 77 | const isMessageAllowed = async (url) => { 78 | const requestUrl = new URL(url); 79 | const apiUrlValue = await SLStorage.get(SLStorage.SETTINGS.API_URL); 80 | const apiUrl = new URL(apiUrlValue); 81 | 82 | let allowedSources = [ 83 | new RegExp(apiUrl.hostname), 84 | new RegExp("^app\\.simplelogin\\.io$"), 85 | new RegExp("^.*\\.protonmail\\.ch$"), 86 | new RegExp("^.*\\.protonmail\\.com$"), 87 | ]; 88 | 89 | const extraAllowedDomains = 90 | SLStorage.DEFAULT_SETTINGS[SLStorage.SETTINGS.EXTRA_ALLOWED_DOMAINS]; 91 | for (const extra of extraAllowedDomains) { 92 | allowedSources.push(new RegExp(extra)); 93 | } 94 | 95 | for (const source of allowedSources) { 96 | if (source.test(requestUrl.host)) return true; 97 | } 98 | return false; 99 | }; 100 | 101 | /** 102 | * Handle the event of a page querying if the SL extension is installed 103 | * @return {{data: {version: string}, tag: string}} 104 | */ 105 | const handleExtensionInstalledQuery = () => { 106 | const manifest = browser.runtime.getManifest(); 107 | return { 108 | tag: "EXTENSION_INSTALLED_RESPONSE", 109 | data: { 110 | version: manifest.version, 111 | }, 112 | }; 113 | }; 114 | 115 | /** 116 | * Register onMessage listener 117 | */ 118 | browser.runtime.onMessage.addListener(async function (request, sender) { 119 | // Check messages allowed from everywhere 120 | if (request.tag === "NEW_RANDOM_ALIAS") { 121 | return await handleNewRandomAlias(request.currentUrl); 122 | } else if (request.tag === "GET_APP_SETTINGS") { 123 | return await handleGetAppSettings(); 124 | } 125 | 126 | // Check messages allowed only from authorized sources 127 | const messageAllowed = await isMessageAllowed(sender.url); 128 | if (!messageAllowed) return; 129 | 130 | if (request.tag === "EXTENSION_SETUP") { 131 | // On Safari the background script won't set cookies properly in API calls (see https://bugs.webkit.org/show_bug.cgi?id=260676), 132 | // so we will return the API URL to the content script which will make the API call with cookies properly set 133 | return process.env.MAC 134 | ? await SLStorage.get(SLStorage.SETTINGS.API_URL) 135 | : await handleExtensionSetup(); 136 | } else if (request.tag === "EXTENSION_INSTALLED_QUERY") { 137 | return handleExtensionInstalledQuery(); 138 | } else if (request.tag === "SAFARI_FINALIZE_EXTENSION_SETUP") { 139 | return await finalizeExtensionSetup(request.data); 140 | } 141 | }); 142 | 143 | /** 144 | * Register context menu 145 | */ 146 | browser.contextMenus.create({ 147 | id: "sl-random", 148 | title: "Create random email alias (copied)", 149 | contexts: ["all"], 150 | }); 151 | 152 | browser.contextMenus.onClicked.addListener(handleOnClickContextMenu); 153 | 154 | /** 155 | * Shortcuts and hotkeys listener 156 | */ 157 | browser.commands.onCommand.addListener(async (command) => { 158 | if (command === "generate-random-alias") { 159 | const currentTab = ( 160 | await browser.tabs.query({ active: true, currentWindow: true }) 161 | )[0]; 162 | const res = await handleNewRandomAlias(currentTab.url); 163 | generateAliasHandlerJS(currentTab, res); 164 | } 165 | }); 166 | 167 | APIService.initService(); 168 | Onboarding.initService(); 169 | -------------------------------------------------------------------------------- /src/background/onboarding.js: -------------------------------------------------------------------------------- 1 | import browser from "webextension-polyfill"; 2 | import SLStorage from "../popup/SLStorage"; 3 | 4 | function initService() { 5 | browser.runtime.onInstalled.addListener(async function ({ reason }) { 6 | if (reason === "install") { 7 | await SLStorage.clear(); 8 | 9 | const apiUrl = await SLStorage.get(SLStorage.SETTINGS.API_URL); 10 | await browser.tabs.create({ 11 | url: `${apiUrl}/onboarding`, 12 | }); 13 | } 14 | }); 15 | } 16 | 17 | export default { initService }; 18 | -------------------------------------------------------------------------------- /src/content_script/input_tools.css: -------------------------------------------------------------------------------- 1 | .simplelogin-extension--button-wrapper { 2 | position: absolute; 3 | z-index: 999999; 4 | } 5 | 6 | .simplelogin-extension--button { 7 | cursor: pointer; 8 | box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.3); 9 | max-height: 30px; 10 | max-width: 30px; 11 | position: absolute; 12 | top: 50%; 13 | left: 50%; 14 | transform: translate(-50%, -50%); 15 | border-radius: 100px; 16 | background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3C!-- Generator: Adobe Illustrator 23.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --%3E%3Csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='-30 -68.5 345 286.5' style='enable-background:new 0 0 265 156.86; background-color: white;' xml:space='preserve'%3E%3Cstyle type='text/css'%3E .st0%7Bfill:%23BE1E2D;%7D .st1%7Bfill:url(%23SVGID_1_);%7D%0A%3C/style%3E%3Cg%3E%3ClinearGradient id='SVGID_1_' gradientUnits='userSpaceOnUse' x1='0.1062' y1='77.7856' x2='696.6286' y2='77.7856'%3E%3Cstop offset='0' style='stop-color:%23EE307C'/%3E%3Cstop offset='1' style='stop-color:%23AA2990'/%3E%3C/linearGradient%3E%3Cpath class='st1' d='M66.82-0.04L66.77,0h0.08L66.82-0.04z M257.49,0H66.77l-2.42,2.5l-0.42,0.46c-1.83,1.92-3.75,3.75-5.66,5.5 C43.66,21.9,26.09,32.06,7.02,37.06c-0.54,0.17-1.08,0.29-1.62,0.46L2.98,38.1l-0.54,2.42c-0.21,0.87-0.37,1.75-0.54,2.67 c-1.17,6.16-1.79,12.49-1.79,18.78c0,20.57,6.2,40.31,17.95,57.09c10.58,15.12,24.9,26.9,41.64,34.23 c0.42,0.21,0.87,0.42,1.33,0.58c1,0.46,2,0.83,3,1.25l1.21,0.46l0.17,0.04l0.08-0.04c0.08,0.04,0.21,0.04,0.29,0.04h191.72 c4.16,0,7.5-3.37,7.5-7.5V7.45C264.99,3.33,261.65,0,257.49,0z M244.54,11.95l-79.83,69.58c-1.67,1.42-4.08,1.42-5.7-0.04 l-26.03-23.07c-0.12-5.58-0.71-11.12-1.79-16.53c-0.08-0.46-0.17-0.92-0.25-1.42l-0.54-2.42l-2.42-0.62 c-0.58-0.12-1.17-0.29-1.75-0.42c-13.24-3.54-25.78-9.58-37.06-17.45l-8.62-7.62H244.54z M137.11,77.62l-6.54,5.79 c0.75-3.29,1.33-6.62,1.75-10.04L137.11,77.62z M65.52,147.12c-2.46-1-4.87-2.12-7.25-3.33C27.92,128.25,8.1,96.52,8.1,61.96 c0-5.83,0.54-11.66,1.67-17.32c17.78-4.79,34.27-13.24,48.51-24.9c2.96-2.37,5.79-4.91,8.54-7.58 c13.49,13.12,29.52,23.11,47.1,29.36c3.16,1.17,6.37,2.17,9.66,3.04c0.37,2.08,0.71,4.21,0.92,6.33c0.37,3.37,0.58,6.83,0.58,10.29 c0,1.87-0.04,3.75-0.21,5.62c-0.54,9.24-2.5,18.24-5.7,26.69C110,117.76,90.68,137.67,65.52,147.12z M88.8,144.04 c14.12-9.49,25.53-22.36,33.19-37.31l23.94-21.15l12.99,11.62c1.62,1.46,4.12,1.46,5.75,0l13.24-11.74l66.46,58.51L88.8,144.04z M186.95,77.58l66.5-59.09l0.13,117.84L186.95,77.58z M83.47,124.92c3.16-2.33,6.16-4.96,8.83-7.79L83.47,124.92z M113.12,56.42 c-0.21-1.67-0.46-3.33-0.75-5c-2.62-0.71-5.21-1.54-7.75-2.5c0,0-0.04,0-0.04-0.04c-0.67-0.21-1.29-0.5-1.96-0.75 c-1.46-0.58-2.91-1.21-4.37-1.92c-1-0.46-2.04-0.92-3.04-1.46c-0.71-0.33-1.46-0.71-2.17-1.12c-2.29-1.21-4.54-2.54-6.75-3.96 c-1.37-0.83-2.71-1.75-4-2.67c-0.13-0.04-0.21-0.13-0.29-0.21c-1.42-0.96-2.79-2-4.16-3.04c-2.71-2.08-5.33-4.29-7.87-6.62 c-1.08-0.96-2.17-1.96-3.21-3c-2.71,2.62-5.54,5.16-8.49,7.58C47.2,40.73,34.54,47.85,20.93,51.51c-0.87,4.58-1.33,9.24-1.33,13.95 c0,5.62,0.67,11.16,1.87,16.53c4.75,20.65,18.2,38.98,36.81,49.18c2.42,1.33,4.87,2.5,7.45,3.54c1.5-0.58,3-1.21,4.46-1.87 c0.04,0,0.08-0.04,0.08-0.04c1.21-0.54,2.42-1.12,3.58-1.75c1.08-0.54,2.12-1.12,3.16-1.79c0.33-0.17,0.67-0.37,1-0.58 c0.54-0.33,1.08-0.67,1.58-1c0.13-0.08,0.25-0.12,0.33-0.25c0.96-0.58,1.83-1.21,2.71-1.87c0.29-0.17,0.58-0.37,0.83-0.62 l8.83-7.79c0.08,0,0.08,0,0.08-0.04c8.74-9.08,15.2-20.36,18.53-32.65c1.75-6.33,2.67-12.95,2.67-19.65 C113.58,62,113.41,59.21,113.12,56.42z M70.1,99.23l-6.79,6.79h-0.04L35.17,77.91l8.62-8.7l19.53,19.53l6.79-6.79L93.8,58.21 l8.66,8.66L70.1,99.23z'/%3E%3C/g%3E%3C/svg%3E"); 17 | transition: all 0.2s ease; 18 | pointer-events: all; 19 | background-repeat: no-repeat; 20 | background-position: center center; 21 | background-color: white; 22 | background-size: contain; 23 | } 24 | 25 | .simplelogin-extension--button:hover { 26 | transform: translate(-50%, -50%) scale(1.2); 27 | } 28 | 29 | .simplelogin-extension--button.loading { 30 | background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' style='margin: auto; background: none; display: block; shape-rendering: auto;' width='200px' height='200px' viewBox='0 0 100 100' preserveAspectRatio='xMidYMid'%3E%3Ccircle cx='50' cy='50' fill='none' stroke='%23aa2990' stroke-width='10' r='35' stroke-dasharray='164.93361431346415 56.97787143782138' transform='rotate(178.465 50 50)'%3E%3CanimateTransform attributeName='transform' type='rotate' repeatCount='indefinite' dur='1s' values='0 50 50;360 50 50' keyTimes='0;1'%3E%3C/animateTransform%3E%3C/circle%3E%3C/svg%3E"); 31 | } 32 | 33 | .simplelogin-extension--button.loading:hover { 34 | transform: translate(-50%, -50%); 35 | } 36 | -------------------------------------------------------------------------------- /src/content_script/input_tools.js: -------------------------------------------------------------------------------- 1 | if (!window._hasExecutedSlExtension) { 2 | window._hasExecutedSlExtension = true; 3 | 4 | /** 5 | * Send message to background.js and resolve with the response 6 | * @param {string} tag 7 | * @param {object} data 8 | */ 9 | const sendMessageToBackground = (tag, data = null) => { 10 | const _browser = window.chrome || browser; 11 | return new Promise((resolve) => { 12 | try { 13 | _browser.runtime.sendMessage( 14 | { 15 | tag, 16 | data, 17 | }, 18 | function (response) { 19 | resolve(response); 20 | } 21 | ); 22 | } catch (e) { 23 | // Extension context invalidated. 24 | console.error(e); 25 | } 26 | }); 27 | }; 28 | 29 | const slButtonLogic = async () => { 30 | if (!window.hasSLButton) { 31 | window.hasSLButton = true; 32 | 33 | const InputTools = { 34 | isLoading: false, 35 | 36 | // store tracked input elements 37 | trackedElements: [], 38 | 39 | init(target) { 40 | InputTools.queryEmailInputAndApply(target, (element) => { 41 | if (!InputTools.isValidEmailInput(element)) { 42 | return; 43 | } 44 | 45 | // ignore if this elements has already been tracked 46 | const i = InputTools.trackedElements.indexOf(element); 47 | if (i === -1) { 48 | InputTools.trackedElements.push(element); 49 | InputTools.addSLButtonToInput(element); 50 | } 51 | }); 52 | }, 53 | 54 | destroy(target) { 55 | InputTools.queryEmailInputAndApply(target, (element) => { 56 | // remove element from tracking list 57 | const i = InputTools.trackedElements.indexOf(element); 58 | if (i !== -1) { 59 | InputTools.trackedElements.splice(i, 1); 60 | } 61 | }); 62 | }, 63 | 64 | queryEmailInputAndApply(target, actionFunction) { 65 | if (!target.querySelectorAll) return; 66 | const elements = target.querySelectorAll( 67 | "input[type='email'],input[name*='email'],input[id*='email']" 68 | ); 69 | for (const element of elements) { 70 | actionFunction(element); 71 | } 72 | }, 73 | 74 | isValidEmailInput(element) { 75 | const style = getComputedStyle(element); 76 | return ( 77 | // check if element is visible 78 | style.visibility !== "hidden" && 79 | style.display !== "none" && 80 | style.opacity !== "0" && 81 | style.pointerEvents === "auto" && 82 | // check if element is not disabled 83 | !element.disabled && 84 | // for example, we must filter out a checkbox with name*=email 85 | // check if element is text or email input 86 | (element.type === "text" || element.type === "email") 87 | ); 88 | }, 89 | 90 | addSLButtonToInput(inputElem) { 91 | // create wrapper for SL button 92 | const btnWrapper = InputTools.newDiv( 93 | "simplelogin-extension--button-wrapper" 94 | ); 95 | const inputSumHeight = 96 | inputElem.getBoundingClientRect().height + "px"; 97 | btnWrapper.style.height = inputSumHeight; 98 | btnWrapper.style.width = inputSumHeight; 99 | document.body.appendChild(btnWrapper); 100 | 101 | // create the SL button 102 | const slButton = InputTools.newDiv("simplelogin-extension--button"); 103 | slButton.addEventListener("click", function () { 104 | InputTools.handleOnClickSLButton(inputElem, slButton); 105 | }); 106 | slButton.style.height = inputSumHeight; 107 | slButton.style.width = inputSumHeight; 108 | btnWrapper.appendChild(slButton); 109 | 110 | InputTools.placeBtnToTheRightOfElement(inputElem, btnWrapper); 111 | }, 112 | 113 | newDiv(...className) { 114 | const div = document.createElement("div"); 115 | div.classList.add(...className); 116 | return div; 117 | }, 118 | 119 | placeBtnToTheRightOfElement(inputElem, btnWrapper) { 120 | let intervalId = 0; 121 | 122 | function updatePosition() { 123 | // check is element is removed from trackedElements 124 | const i = InputTools.trackedElements.indexOf(inputElem); 125 | if (i === -1) { 126 | btnWrapper.parentNode.removeChild(btnWrapper); 127 | clearInterval(intervalId); 128 | } 129 | 130 | // get dimension & position of input 131 | const inputCoords = inputElem.getBoundingClientRect(); 132 | const inputStyle = getComputedStyle(inputElem); 133 | const elemWidth = InputTools.dimensionToInt(btnWrapper.style.width); 134 | const pageXOffset = window.pageXOffset; 135 | const pageYOffset = window.pageYOffset; 136 | const buttonXOffset = 137 | SLSettings.SLButtonPosition === "right-inside" 138 | ? -elemWidth * 1.02 139 | : elemWidth * 0.02; 140 | 141 | // calculate elem position 142 | const left = 143 | InputTools.sumPixel([ 144 | inputCoords.left, 145 | pageXOffset, 146 | inputElem.offsetWidth, 147 | buttonXOffset, 148 | -inputStyle.paddingRight, 149 | ]) + "px"; 150 | 151 | const top = 152 | InputTools.sumPixel([inputCoords.top, pageYOffset]) + "px"; 153 | 154 | if (btnWrapper.style.left !== left) { 155 | btnWrapper.style.left = left; 156 | } 157 | 158 | if (btnWrapper.style.top !== top) { 159 | btnWrapper.style.top = top; 160 | } 161 | } 162 | 163 | intervalId = setInterval(updatePosition, 200); 164 | updatePosition(); 165 | }, 166 | 167 | async handleOnClickSLButton(inputElem, slButton) { 168 | if (InputTools.isLoading) { 169 | return; 170 | } 171 | InputTools.isLoading = true; 172 | slButton.classList.add("loading"); 173 | 174 | let res = await sendMessageToBackground("NEW_RANDOM_ALIAS", { 175 | currentUrl: window.location.href, 176 | }); 177 | if (res.error) { 178 | alert("SimpleLogin Error: " + res.error); 179 | res = { alias: "" }; 180 | } 181 | 182 | InputTools.isLoading = false; 183 | slButton.classList.remove("loading"); 184 | 185 | inputElem.value = res.alias; 186 | }, 187 | 188 | sumPixel(dimensions) { 189 | let sum = 0; 190 | for (const dim of dimensions) { 191 | sum += !isNaN(dim) ? dim : InputTools.dimensionToInt(dim); 192 | } 193 | return sum; 194 | }, 195 | 196 | dimensionToInt(dim) { 197 | try { 198 | const pixel = parseFloat(dim.replace(/[^0-9.]+/g, "")); 199 | return isNaN(pixel) ? 0 : pixel; 200 | } catch (e) { 201 | return 0; 202 | } 203 | }, 204 | }; 205 | 206 | const MutationObserver = 207 | window.MutationObserver || 208 | window.WebKitMutationObserver || 209 | window.MozMutationObserver; 210 | 211 | /** 212 | * Add DOM mutations listener 213 | */ 214 | const addMutationObserver = () => { 215 | const mutationObserver = new MutationObserver(function (mutations) { 216 | mutations.forEach(function (mutation) { 217 | for (const addedNode of mutation.addedNodes) { 218 | // add SLButton for newly added nodes 219 | InputTools.init(addedNode); 220 | } 221 | 222 | for (const removedNode of mutation.removedNodes) { 223 | // destroy SLButton for removed nodes 224 | InputTools.destroy(removedNode); 225 | } 226 | }); 227 | }); 228 | 229 | const target = document.body; 230 | if (!target) return; 231 | 232 | mutationObserver.observe(target, { 233 | childList: true, 234 | subtree: true, 235 | }); 236 | }; 237 | 238 | const SLSettings = await sendMessageToBackground("GET_APP_SETTINGS"); 239 | if (SLSettings.showSLButton) { 240 | InputTools.init(document); 241 | addMutationObserver(); 242 | } 243 | } 244 | }; 245 | 246 | const slRegisterListener = () => { 247 | if (!window.hasSlListenerRegistered) { 248 | window.hasSlListenerRegistered = true; 249 | 250 | let hasProcessedSetup = false; 251 | 252 | /** 253 | * Callback called for every event 254 | * @param {MessageEvent} event 255 | */ 256 | const onEvent = async (event) => { 257 | if (event.source !== window) return; 258 | if (!event.data) return; 259 | if (!event.data.tag) return; 260 | if (event.data.tag === "PERFORM_EXTENSION_SETUP") { 261 | const SLSettings = await sendMessageToBackground("GET_APP_SETTINGS"); 262 | if (SLSettings.isLoggedIn) { 263 | console.log( 264 | "Received PERFORM_EXTENSION_SETUP but extension is already logged in. Redirecting to dashboard" 265 | ); 266 | window.location.href = `${SLSettings.url}/dashboard/`; 267 | return; 268 | } 269 | 270 | if (!hasProcessedSetup) { 271 | hasProcessedSetup = true; 272 | const apiUrl = await sendMessageToBackground("EXTENSION_SETUP"); 273 | // if apiUrl is undefined then the Chromium/Firefox extension has already finished setup 274 | if (!apiUrl) { 275 | return; 276 | } 277 | // else if apiUrl is defined, we are in Safari and need to setup the Safari extension 278 | const url = apiUrl + "/api/api_key"; 279 | const res = await fetch(url, { 280 | method: "POST", 281 | headers: { 282 | "Content-Type": "application/json", 283 | "X-Sl-Allowcookies": true, 284 | }, 285 | body: JSON.stringify({ 286 | device: "Safari extension", 287 | }), 288 | }); 289 | 290 | if (res.ok) { 291 | const apiRes = await res.json(); 292 | const apiKey = apiRes.api_key; 293 | await sendMessageToBackground( 294 | "SAFARI_FINALIZE_EXTENSION_SETUP", 295 | apiKey 296 | ); 297 | } 298 | } 299 | } else if (event.data.tag === "EXTENSION_INSTALLED_QUERY") { 300 | const res = await sendMessageToBackground( 301 | "EXTENSION_INSTALLED_QUERY" 302 | ); 303 | window.postMessage(res); 304 | } 305 | }; 306 | 307 | window.addEventListener("message", onEvent); 308 | } 309 | }; 310 | 311 | slButtonLogic(); 312 | slRegisterListener(); 313 | } 314 | -------------------------------------------------------------------------------- /src/icons/icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/icons/icon_128.png -------------------------------------------------------------------------------- /src/icons/icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/icons/icon_16.png -------------------------------------------------------------------------------- /src/icons/icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/icons/icon_32.png -------------------------------------------------------------------------------- /src/icons/icon_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/icons/icon_48.png -------------------------------------------------------------------------------- /src/icons/icon_96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/icons/icon_96.png -------------------------------------------------------------------------------- /src/icons/icon_beta_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/icons/icon_beta_128.png -------------------------------------------------------------------------------- /src/icons/icon_beta_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/icons/icon_beta_48.png -------------------------------------------------------------------------------- /src/images/arrow-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/images/arrow-up.png -------------------------------------------------------------------------------- /src/images/back-button.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/chrome-permission-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/images/chrome-permission-screenshot.png -------------------------------------------------------------------------------- /src/images/firefox-permission-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/images/firefox-permission-screenshot.png -------------------------------------------------------------------------------- /src/images/horizontal-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 16 | 22 | 25 | 28 | 29 | 30 | 31 | 32 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /src/images/icon-copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/images/icon-dropdown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/images/icon-more.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/icon-puzzle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/images/icon-puzzle.png -------------------------------------------------------------------------------- /src/images/icon-settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/images/icon-simplelogin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/images/icon-simplelogin.png -------------------------------------------------------------------------------- /src/images/icon-trash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/images/loading-three-dots.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 12 | 13 | 14 | 18 | 22 | 23 | 24 | 28 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/images/loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/images/proton.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/images/sl-button-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/images/sl-button-demo.png -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SimpleLogin by Proton: Secure Email Aliases", 3 | "short_name": "SimpleLogin", 4 | "description": "Easily create a different email for each website to hide your real email. Protect your inbox against spams, phishing, data breaches", 5 | "version": null, 6 | "manifest_version": 3, 7 | "icons": { 8 | "16": "icons/icon_16.png", 9 | "32": "icons/icon_32.png", 10 | "48": "icons/icon_48.png", 11 | "96": "icons/icon_96.png", 12 | "128": "icons/icon_128.png" 13 | }, 14 | "permissions": [ 15 | "activeTab", 16 | "storage", 17 | "contextMenus", 18 | "scripting", 19 | "tabs" 20 | ], 21 | "host_permissions": [ 22 | "https://*.simplelogin.io/*", 23 | "http://*/*", 24 | "https://*/*" 25 | ], 26 | "action": { 27 | "default_title": "SimpleLogin", 28 | "default_popup": "popup/popup.html", 29 | "default_icon": { 30 | "16": "icons/icon_16.png", 31 | "32": "icons/icon_32.png", 32 | "48": "icons/icon_48.png", 33 | "96": "icons/icon_96.png", 34 | "128": "icons/icon_128.png" 35 | } 36 | }, 37 | 38 | "browser_specific_settings": { 39 | "gecko": { 40 | "id": "addon@simplelogin", 41 | "strict_min_version": "109.0" 42 | } 43 | }, 44 | "commands": { 45 | "generate-random-alias": { 46 | "suggested_key": { 47 | "default": "Ctrl+Shift+X" 48 | }, 49 | "description": "Generate a random email alias" 50 | }, 51 | "_execute_browser_action": { 52 | "suggested_key": { 53 | "default": "Ctrl+Shift+S" 54 | }, 55 | "description": "Open the extension action menu" 56 | } 57 | }, 58 | "content_scripts": [ 59 | { 60 | "js": ["content_script/input_tools.js"], 61 | "css": ["content_script/input_tools.css"], 62 | "matches": ["http://*/*", "https://*/*"], 63 | "exclude_matches" : ["https://app.simplelogin.io/dashboard/*"], 64 | "run_at": "document_idle" 65 | } 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /src/popup/APIService.js: -------------------------------------------------------------------------------- 1 | import EventManager from "./EventManager"; 2 | import Navigation from "./Navigation"; 3 | import SLStorage from "./SLStorage"; 4 | import Utils from "./Utils"; 5 | 6 | const API_ROUTE = { 7 | GET_USER_INFO: { method: "GET", path: "/api/user_info" }, 8 | LOGOUT: { method: "GET", path: "/api/logout" }, 9 | LOGIN: { method: "POST", path: "/api/auth/login" }, 10 | MFA: { method: "POST", path: "/api/auth/mfa" }, 11 | GET_ALIAS_OPTIONS: { 12 | method: "GET", 13 | path: "/api/v4/alias/options?hostname=:hostname", 14 | }, 15 | GET_MAILBOXES: { 16 | method: "GET", 17 | path: "/api/mailboxes", 18 | }, 19 | GET_ALIASES: { method: "POST", path: "/api/v2/aliases?page_id=:page_id" }, 20 | NEW_ALIAS: { 21 | method: "POST", 22 | path: "/api/v2/alias/custom/new?hostname=:hostname", 23 | }, 24 | NEW_RANDOM_ALIAS: { 25 | method: "POST", 26 | path: "/api/alias/random/new?hostname=:hostname", 27 | }, 28 | TOGGLE_ALIAS: { method: "POST", path: "/api/aliases/:alias_id/toggle" }, 29 | EDIT_ALIAS: { method: "PUT", path: "/api/aliases/:alias_id" }, 30 | DELETE_ALIAS: { method: "DELETE", path: "/api/aliases/:alias_id" }, 31 | CREATE_REVERSE_ALIAS: { 32 | method: "POST", 33 | path: "/api/aliases/:alias_id/contacts", 34 | }, 35 | GET_API_KEY_FROM_COOKIE: { method: "POST", path: "/api/api_key" }, 36 | }; 37 | 38 | const API_ON_ERR = { 39 | IGNORE: 1, 40 | TOAST: 2, 41 | THROW: 3, 42 | }; 43 | 44 | const SETTINGS = { 45 | apiKey: "", 46 | apiUrl: "", 47 | }; 48 | 49 | const initService = async () => { 50 | await reloadSettings(); 51 | 52 | EventManager.addListener(EventManager.EVENT.SETTINGS_CHANGED, reloadSettings); 53 | }; 54 | 55 | const reloadSettings = async () => { 56 | SETTINGS.apiKey = await SLStorage.get(SLStorage.SETTINGS.API_KEY); 57 | SETTINGS.apiUrl = await SLStorage.get(SLStorage.SETTINGS.API_URL); 58 | }; 59 | 60 | const callAPI = async function ( 61 | route, 62 | params = {}, 63 | data = {}, 64 | errHandlerMethod = API_ON_ERR.THROW 65 | ) { 66 | const { method, path } = route; 67 | const url = SETTINGS.apiUrl + bindQueryParams(path, params); 68 | const headers = {}; 69 | 70 | if (SETTINGS.apiKey) { 71 | headers["Authentication"] = SETTINGS.apiKey; 72 | } 73 | 74 | let fetchParam = { 75 | method: method, 76 | headers: headers, 77 | }; 78 | if (method === "POST" || method === "PUT") { 79 | fetchParam.body = JSON.stringify(data); 80 | headers["Content-Type"] = "application/json"; 81 | } 82 | 83 | let res = await fetch(url, fetchParam); 84 | if (res.ok) { 85 | const apiRes = await res.json(); 86 | // wrap apiRes in data to look like axios which was used before 87 | return { 88 | status: res.status, 89 | data: apiRes, 90 | }; 91 | } else { 92 | if (errHandlerMethod !== API_ON_ERR.IGNORE) { 93 | console.error(res); 94 | } 95 | 96 | if (res.status === 401) { 97 | await handle401Error(); 98 | return null; 99 | } 100 | 101 | if (errHandlerMethod === API_ON_ERR.TOAST) { 102 | let apiRes = await res.json(); 103 | if (apiRes.error) { 104 | Utils.showError(apiRes.error); 105 | } else { 106 | Utils.showError("Unknown error"); 107 | } 108 | return null; 109 | } 110 | 111 | if (errHandlerMethod === API_ON_ERR.THROW) { 112 | throw { 113 | response: { 114 | status: res.status, 115 | data: await res.json(), 116 | }, 117 | }; 118 | } 119 | } 120 | }; 121 | 122 | async function handle401Error() { 123 | Utils.showError("Authentication error, please login again"); 124 | await SLStorage.remove(SLStorage.SETTINGS.API_KEY); 125 | EventManager.broadcast(EventManager.EVENT.SETTINGS_CHANGED); 126 | Navigation.clearHistoryAndNavigateTo(Navigation.PATH.LOGIN); 127 | } 128 | 129 | function bindQueryParams(url, params) { 130 | for (const key of Object.keys(params)) { 131 | url = url.replace(`:${key}`, encodeURIComponent(params[key])); 132 | } 133 | 134 | return url; 135 | } 136 | 137 | export { callAPI, API_ROUTE, API_ON_ERR, reloadSettings }; 138 | export default { initService }; 139 | -------------------------------------------------------------------------------- /src/popup/App-color.scss: -------------------------------------------------------------------------------- 1 | $primary: #b02a8f; 2 | $success: #b02a8f; -------------------------------------------------------------------------------- /src/popup/App-scrollbar.scss: -------------------------------------------------------------------------------- 1 | .app .content { 2 | scrollbar-width: thin; 3 | } 4 | 5 | .content::-webkit-scrollbar-track { 6 | background: rgba(0,0,0,0); 7 | } 8 | 9 | .content::-webkit-scrollbar { 10 | width: 0.6em; 11 | } 12 | 13 | .content::-webkit-scrollbar-thumb { 14 | background: transparent; 15 | border: solid 2px transparent; 16 | box-shadow: inset 0 0 10px 10px rgba(0, 0, 0, 0.25); 17 | border-radius: 16px; 18 | } -------------------------------------------------------------------------------- /src/popup/App.scss: -------------------------------------------------------------------------------- 1 | @import "./App-color.scss"; 2 | @import "./App-scrollbar.scss"; 3 | @import "./Theme.scss"; 4 | @import "~bootstrap/scss/bootstrap.scss"; 5 | @import "~bootstrap-vue/src/index.scss"; 6 | @import "~tippy.js/dist/tippy.css"; 7 | 8 | body { 9 | box-sizing: content-box !important; 10 | width: 470px; 11 | background-color: var(--bg-color); 12 | color: var(--text-color); 13 | } 14 | 15 | input.form-control, 16 | select.form-control, 17 | textarea.form-control, 18 | .dropdown-menu.show { 19 | color: var(--text-color); 20 | background-color: var(--input-bg-color); 21 | border-color: var(--input-border-color); 22 | } 23 | 24 | input.form-control:disabled, 25 | select.form-control:disabled, 26 | textarea.form-control:disabled { 27 | color: var(--text-color); 28 | background-color: var(--bg-color); 29 | border-color: var(--input-border-color); 30 | } 31 | 32 | input.form-control:focus, 33 | select.form-control:focus, 34 | textarea.form-control:focus { 35 | color: var(--text-color); 36 | background-color: var(--input-bg-focus); 37 | border-color: var(--input-border-color); 38 | } 39 | 40 | .v--modal-box.v--modal.vue-dialog div, 41 | .v--modal-box.v--modal.vue-dialog button { 42 | color: var(--text-color); 43 | background-color: var(--input-bg-color); 44 | border-color: var(--delimiter-color); 45 | } 46 | 47 | .header { 48 | height: 45px; 49 | width: 460px; 50 | position: absolute; 51 | top: 0; 52 | left: 0; 53 | z-index: 10; 54 | } 55 | 56 | .header .back { 57 | cursor: pointer; 58 | } 59 | 60 | .overlay { 61 | position: fixed; 62 | top: 0; 63 | left: 0; 64 | z-index: 20; 65 | height: 100vh; 66 | width: 100vw; 67 | min-height: 350px; 68 | background-color: var(--overlay-background-color); 69 | } 70 | 71 | .overlay-content { 72 | position: fixed; 73 | z-index: 21; 74 | top: 50%; 75 | left: 50%; 76 | transform: translate(-50%, -50%); 77 | text-align: center; 78 | } 79 | 80 | .app { 81 | width: 470px; 82 | box-sizing: content-box !important; 83 | overflow-y: hidden; 84 | } 85 | 86 | .app .content { 87 | box-sizing: border-box; 88 | margin-top: 45px; 89 | padding-top: 15px; 90 | padding-bottom: 40px; 91 | max-height: 500px; 92 | overflow-y: auto; 93 | } 94 | 95 | .splash .logo { 96 | width: 200px; 97 | } 98 | 99 | .splash .loading { 100 | width: 30px; 101 | padding-top: 20px; 102 | } 103 | 104 | .splash.overlay { 105 | background-color: var(--background-color); 106 | } 107 | 108 | em { 109 | font-style: normal; 110 | background-color: #ffff00; 111 | } 112 | 113 | .small-text { 114 | font-size: 12px; 115 | font-weight: lighter; 116 | } 117 | 118 | .tooltip-inner { 119 | max-width: 400px; 120 | } 121 | 122 | .card-rating { 123 | border-radius: 8px; 124 | border: 1px solid var(--brand-color); 125 | } 126 | 127 | /* list aliases */ 128 | .vue-js-switch { 129 | margin-bottom: 0; 130 | } 131 | 132 | .list-item-alias .disabled { 133 | opacity: 0.7; 134 | } 135 | 136 | .list-item-email { 137 | margin-right: 30px !important; 138 | position: relative; 139 | overflow: hidden; 140 | cursor: pointer; 141 | } 142 | 143 | .list-item-email > a { 144 | white-space: nowrap; 145 | } 146 | 147 | .list-item-email .email-sub { 148 | font-size: 12px; 149 | } 150 | 151 | .list-item-email-fade { 152 | right: 0; 153 | width: 30px; 154 | height: 100%; 155 | background: linear-gradient(to right, transparent, var(--bg-color)); 156 | top: 0; 157 | position: absolute; 158 | } 159 | 160 | .list-item-alias .alias-note-preview { 161 | font-size: 12px; 162 | max-height: calc(12px * 1.5 * 3); /* font-size * line-height */ 163 | overflow-y: hidden; 164 | } 165 | 166 | .header { 167 | .actions-container { 168 | position: absolute; 169 | right: 0.5rem; 170 | } 171 | 172 | .header-button { 173 | height: 20px; 174 | margin-top: 2px; 175 | margin-left: 10px; 176 | margin-right: 7px; 177 | color: var(--brand-color); 178 | cursor: pointer; 179 | } 180 | } 181 | 182 | .app-header-menu { 183 | left: auto !important; 184 | float: right !important; 185 | right: 10px; 186 | margin-top: 5px !important; 187 | } 188 | 189 | /* toasted: white close button */ 190 | .toasted.toasted-primary > .action.ripple { 191 | color: white !important; 192 | } 193 | 194 | /* button img */ 195 | .btn-svg { 196 | cursor: pointer; 197 | padding: 5px; 198 | display: inline; 199 | } 200 | 201 | /* send btn*/ 202 | .btn-send { 203 | width: 14px; 204 | height: 14px; 205 | color: var(--brand-color); 206 | } 207 | 208 | /* more options */ 209 | .btn-delete:hover, 210 | .btn-svg:hover { 211 | background-color: var(--delete-button-hover-color); 212 | } 213 | 214 | .btn-delete { 215 | float: right; 216 | } 217 | 218 | .btn-delete > img { 219 | vertical-align: unset; 220 | height: 14px; 221 | } 222 | 223 | .more-options { 224 | margin-top: 10px; 225 | } 226 | 227 | .cursor { 228 | cursor: pointer; 229 | } 230 | 231 | span.link { 232 | color: var(--brand-color); 233 | } 234 | 235 | .more-options > .action { 236 | margin-top: 10px; 237 | min-height: 25px; 238 | } 239 | 240 | .more-options > label { 241 | margin-bottom: 0; 242 | font-size: 12px; 243 | } 244 | 245 | /* BETA badge */ 246 | .beta-badge { 247 | padding: 0.1em 0.4em; 248 | font-size: 0.65em; 249 | margin-left: 1em; 250 | border: 0.7px solid var(--brand-color); 251 | display: inline-block; 252 | color: var(--brand-color); 253 | } 254 | 255 | /* App Settings */ 256 | table.settings-list > tr { 257 | border-bottom: 1px solid var(--delimiter-color); 258 | } 259 | 260 | table.settings-list > tr > td { 261 | vertical-align: top; 262 | padding: 6px; 263 | } 264 | 265 | table.settings-list { 266 | margin-bottom: 3em; 267 | 268 | tr.disabled { 269 | opacity: 0.7; 270 | pointer-events: none; 271 | } 272 | } 273 | 274 | /* Quick fix for Firefox Overflow Menu */ 275 | .app.ff-overflow-menu { 276 | width: auto; 277 | font-size: 88%; 278 | 279 | .content { 280 | max-height: 500px; 281 | overflow-y: auto; 282 | scrollbar-width: thin; 283 | } 284 | 285 | textarea, 286 | input { 287 | font-size: 88%; 288 | } 289 | 290 | .header { 291 | width: 100%; 292 | 293 | .dashboard-btn { 294 | height: 20px; 295 | margin-top: 2px; 296 | padding: 0 0.5rem !important; 297 | } 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /src/popup/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 76 | -------------------------------------------------------------------------------- /src/popup/EventManager.js: -------------------------------------------------------------------------------- 1 | const listeners = {}; 2 | 3 | class EventManager { 4 | static EVENT = { 5 | SETTINGS_CHANGED: "settings_changed", 6 | }; 7 | 8 | static addListener(eventName, callback) { 9 | if (!listeners[eventName]) { 10 | listeners[eventName] = []; 11 | } 12 | if (listeners[eventName].indexOf(callback) === -1) { 13 | // make sure the callback function is added only once 14 | listeners[eventName].push(callback); 15 | } 16 | } 17 | 18 | static removeListener(callback) { 19 | for (const eventCallbacks of Object.values(listeners)) { 20 | const index = eventCallbacks.indexOf(callback); 21 | if (index !== -1) { 22 | eventCallbacks.splice(index, 1); 23 | } 24 | } 25 | } 26 | 27 | static broadcast(eventName, data) { 28 | if (listeners[eventName]) { 29 | for (const callback of listeners[eventName]) { 30 | callback(data); 31 | } 32 | } 33 | } 34 | } 35 | 36 | export default EventManager; 37 | -------------------------------------------------------------------------------- /src/popup/Navigation.js: -------------------------------------------------------------------------------- 1 | let router = null; 2 | 3 | const PATH = { 4 | ROOT: "/", 5 | MAIN: "/main", 6 | NEW_ALIAS_RESULT: "/new-alias-result/", 7 | LOGIN: "/login", 8 | API_KEY_SETTING: "/api-key-setting", 9 | SELF_HOST_SETTING: "/self-host-setting", 10 | REVERSE_ALIAS: "/reverse-alias", 11 | APP_SETTINGS: "/app-settings", 12 | }; 13 | 14 | class Navigation { 15 | static PATH = PATH; 16 | 17 | static getRoutes(components) { 18 | return [ 19 | { 20 | path: Navigation.PATH.ROOT, 21 | component: components.SplashScreen, 22 | }, 23 | { 24 | path: Navigation.PATH.LOGIN, 25 | component: components.Login, 26 | }, 27 | { 28 | path: Navigation.PATH.API_KEY_SETTING, 29 | component: components.ApiKeySetting, 30 | }, 31 | { 32 | path: Navigation.PATH.SELF_HOST_SETTING, 33 | component: components.SelfHostSetting, 34 | }, 35 | { 36 | path: Navigation.PATH.MAIN, 37 | component: components.Main, 38 | }, 39 | { 40 | path: Navigation.PATH.NEW_ALIAS_RESULT, 41 | component: components.NewAliasResult, 42 | }, 43 | { 44 | path: Navigation.PATH.REVERSE_ALIAS, 45 | component: components.ReverseAlias, 46 | }, 47 | { 48 | path: Navigation.PATH.APP_SETTINGS, 49 | component: components.AppSettings, 50 | }, 51 | ]; 52 | } 53 | 54 | static setRouter($router) { 55 | router = $router; 56 | } 57 | 58 | static navigateTo(path, canGoBack) { 59 | if (canGoBack) { 60 | router.push(path); 61 | } else { 62 | router.replace(path); 63 | } 64 | } 65 | 66 | static canGoBack() { 67 | return router.history.index > 0; 68 | } 69 | 70 | static navigateBack() { 71 | router.go(-1); 72 | } 73 | 74 | static clearHistoryAndNavigateTo(path) { 75 | router.history.stack = []; 76 | router.history.index = -1; 77 | setTimeout(() => router.push(path), 10); 78 | } 79 | } 80 | 81 | export default Navigation; 82 | -------------------------------------------------------------------------------- /src/popup/SLStorage.js: -------------------------------------------------------------------------------- 1 | import Utils from "./Utils"; 2 | import browser from "webextension-polyfill"; 3 | import { THEME_SYSTEM } from "./Theme"; 4 | 5 | const TEMP = {}; 6 | 7 | class SLStorage { 8 | static SETTINGS = { 9 | API_URL: "apiUrl", 10 | API_KEY: "apiKey", 11 | NOT_ASKING_RATE: "notAskingRate", 12 | SHOW_SL_BUTTON: "showSLButton", 13 | SL_BUTTON_POSITION: "SLButtonPosition", 14 | THEME: "SLTheme", 15 | EXTRA_ALLOWED_DOMAINS: [], 16 | }; 17 | 18 | static DEFAULT_SETTINGS = { 19 | [SLStorage.SETTINGS.API_URL]: devConfig 20 | ? devConfig.DEFAULT_API_URL 21 | : "https://app.simplelogin.io", 22 | [SLStorage.SETTINGS.API_KEY]: "", 23 | [SLStorage.SETTINGS.NOT_ASKING_RATE]: false, 24 | [SLStorage.SETTINGS.SHOW_SL_BUTTON]: true, 25 | [SLStorage.SETTINGS.SL_BUTTON_POSITION]: "right-inside", 26 | [SLStorage.SETTINGS.THEME]: THEME_SYSTEM, 27 | [SLStorage.SETTINGS.EXTRA_ALLOWED_DOMAINS]: devConfig 28 | ? devConfig.EXTRA_ALLOWED_DOMAINS 29 | : [], 30 | }; 31 | 32 | static set(key, value) { 33 | return browser.storage.sync.set({ [key]: value }); 34 | } 35 | 36 | static async get(key) { 37 | const data = await browser.storage.sync.get(key); 38 | 39 | if (data[key] === undefined || data[key] === null) { 40 | return SLStorage.DEFAULT_SETTINGS[key] || ""; 41 | } else { 42 | return data[key]; 43 | } 44 | } 45 | 46 | static remove(key) { 47 | return browser.storage.sync.remove(key); 48 | } 49 | 50 | static clear() { 51 | return browser.storage.sync.clear(); 52 | } 53 | 54 | static setTemporary(key, value) { 55 | TEMP[key] = Utils.cloneObject(value); 56 | } 57 | 58 | static getTemporary(key) { 59 | return TEMP[key]; 60 | } 61 | } 62 | 63 | export default SLStorage; 64 | -------------------------------------------------------------------------------- /src/popup/Theme.js: -------------------------------------------------------------------------------- 1 | import SLStorage from "./SLStorage"; 2 | 3 | export const THEME_LIGHT = "theme-light"; 4 | export const THEME_DARK = "theme-dark"; 5 | export const THEME_SYSTEM = "theme-system"; 6 | 7 | export const THEMES = [THEME_LIGHT, THEME_DARK, THEME_SYSTEM]; 8 | 9 | export const THEME_LABELS = { 10 | [THEME_LIGHT]: "Light", 11 | [THEME_DARK]: "Dark", 12 | [THEME_SYSTEM]: "System", 13 | }; 14 | 15 | export async function getSavedTheme() { 16 | return (await SLStorage.get(SLStorage.SETTINGS.THEME)) ?? THEME_SYSTEM; 17 | } 18 | 19 | export async function setThemeClass(nextTheme, prevTheme) { 20 | await SLStorage.set(SLStorage.SETTINGS.THEME, nextTheme); 21 | 22 | if (prevTheme === undefined) { 23 | return document.body.classList.add(nextTheme); 24 | } 25 | 26 | document.body.classList.replace(prevTheme, nextTheme); 27 | } 28 | -------------------------------------------------------------------------------- /src/popup/Theme.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --brand-color: #b02a8f; 3 | --muted-brand-color: rgba(176, 42, 143, 0.7); 4 | } 5 | 6 | .theme-light { 7 | --bg-color: white; 8 | --input-bg-color: white; 9 | --overlay-bg-color: rgba(255, 255, 255, 0.8); 10 | --text-color: black; 11 | --delimiter-color: #bbb; 12 | --delete-button-hover-color: rgba(0, 0, 0, 0.1); 13 | } 14 | 15 | .theme-dark { 16 | --bg-color: #222; 17 | --input-bg-color: #333; 18 | --input-bg-focus: #444; 19 | --input-border-color: black; 20 | --overlay-bg-color: rgba(0, 0, 0, 0.8); 21 | --text-color: #ddd; 22 | --delimiter-color: #555; 23 | --delete-button-hover-color: rgba(255, 255, 255, 0.1); 24 | 25 | // For switching colors of monochrome images 26 | .invertable, .btn.dropdown-toggle { 27 | filter: invert(0.8); 28 | } 29 | 30 | .btn-primary.btn-primary-muted { 31 | background-color: var(--muted-brand-color); 32 | } 33 | } 34 | 35 | @media (prefers-color-scheme: light) { 36 | // copy of .theme-light 37 | .theme-system { 38 | --bg-color: white; 39 | --input-bg-color: white; 40 | --overlay-bg-color: rgba(255, 255, 255, 0.8); 41 | --text-color: black; 42 | --delimiter-color: #bbb; 43 | --delete-button-hover-color: rgba(0, 0, 0, 0.1); 44 | } 45 | } 46 | 47 | @media (prefers-color-scheme: dark) { 48 | // copy of .theme-dark 49 | .theme-system { 50 | --bg-color: #222; 51 | --input-bg-color: #333; 52 | --input-bg-focus: #444; 53 | --input-border-color: black; 54 | --overlay-bg-color: rgba(0, 0, 0, 0.8); 55 | --text-color: #ddd; 56 | --delimiter-color: #555; 57 | --delete-button-hover-color: rgba(255, 255, 255, 0.1); 58 | 59 | // For switching colors of monochrome images 60 | .invertable, .btn.dropdown-toggle { 61 | filter: invert(0.8); 62 | } 63 | 64 | .btn-primary.btn-primary-muted { 65 | background-color: var(--muted-brand-color); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/popup/Utils.js: -------------------------------------------------------------------------------- 1 | const browser = require("webextension-polyfill"); 2 | const buildConfig = require("./buildConfig.json"); 3 | 4 | let toasted = null; 5 | 6 | class Utils { 7 | static getRandomIntBetween(min, max) { 8 | return Math.floor(min + Math.random() * Math.floor(max)); 9 | } 10 | 11 | static async getHostName(currentUrl) { 12 | try { 13 | if (currentUrl) { 14 | const url = new URL(currentUrl); 15 | return url.hostname; 16 | } else { 17 | const result = await browser.tabs.query({ 18 | active: true, 19 | currentWindow: true, 20 | }); 21 | const url = new URL(result[0].url); 22 | return url.hostname; 23 | } 24 | } catch (error) { 25 | console.log(error); 26 | } 27 | } 28 | 29 | static async getDefaultNote() { 30 | const hostName = await Utils.getHostName(); 31 | let note = ""; 32 | 33 | // ignore hostName that doesn't look like an url 34 | if (hostName && hostName.indexOf(".") > -1) { 35 | note = `Used on ${hostName}`; 36 | } 37 | 38 | return note; 39 | } 40 | 41 | static getDeviceName() { 42 | const isFirefox = typeof InstallTrigger !== "undefined"; 43 | const browserName = isFirefox ? "Firefox" : "Chrome"; 44 | return `${browserName} (${navigator.platform})`; 45 | } 46 | 47 | static getExtensionURL() { 48 | const isFirefox = typeof InstallTrigger !== "undefined", 49 | firefoxExtensionUrl = 50 | "https://addons.mozilla.org/en-GB/firefox/addon/simplelogin/", 51 | chromeExtensionUrl = 52 | "https://chrome.google.com/webstore/detail/simplelogin-your-anti-spa/dphilobhebphkdjbpfohgikllaljmgbn"; 53 | return isFirefox ? firefoxExtensionUrl : chromeExtensionUrl; 54 | } 55 | 56 | static setToasted($toasted) { 57 | toasted = $toasted; 58 | } 59 | 60 | static showSuccess(message) { 61 | if (toasted) { 62 | toasted.show(message, { 63 | type: "success", 64 | duration: 2500, 65 | }); 66 | } 67 | } 68 | 69 | static showError(message) { 70 | if (toasted) { 71 | toasted.show(message, { 72 | type: "error", 73 | duration: 3000, 74 | action: { 75 | text: "×", 76 | onClick: (e, toastObject) => { 77 | toastObject.goAway(0); 78 | }, 79 | }, 80 | }); 81 | } 82 | } 83 | 84 | static cloneObject(obj) { 85 | return JSON.parse(JSON.stringify(obj)); 86 | } 87 | 88 | static getBuildConfig() { 89 | return buildConfig; 90 | } 91 | } 92 | 93 | export default Utils; 94 | -------------------------------------------------------------------------------- /src/popup/buildConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "features": { 3 | "loginWithProtonEnabled": true 4 | }, 5 | "buildTime": 1655462531232 6 | } -------------------------------------------------------------------------------- /src/popup/components/AliasMoreOptions.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 90 | 91 | 279 | -------------------------------------------------------------------------------- /src/popup/components/ApiKeySetting.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 85 | -------------------------------------------------------------------------------- /src/popup/components/AppSettings.vue: -------------------------------------------------------------------------------- 1 | 113 | 114 | 241 | -------------------------------------------------------------------------------- /src/popup/components/ExpandTransition.vue: -------------------------------------------------------------------------------- 1 | 70 | 71 | 79 | 80 | 92 | -------------------------------------------------------------------------------- /src/popup/components/Header.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 146 | -------------------------------------------------------------------------------- /src/popup/components/Login.vue: -------------------------------------------------------------------------------- 1 | 97 | 98 | 190 | 191 | 206 | -------------------------------------------------------------------------------- /src/popup/components/Main.vue: -------------------------------------------------------------------------------- 1 | 219 | 220 | 625 | -------------------------------------------------------------------------------- /src/popup/components/NewAliasResult.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 108 | -------------------------------------------------------------------------------- /src/popup/components/ReverseAlias.vue: -------------------------------------------------------------------------------- 1 | 95 | 96 | 145 | -------------------------------------------------------------------------------- /src/popup/components/SelfHostSetting.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 59 | -------------------------------------------------------------------------------- /src/popup/components/SplashScreen.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 75 | -------------------------------------------------------------------------------- /src/popup/components/TextareaAutosize.vue: -------------------------------------------------------------------------------- 1 | 4 | 121 | -------------------------------------------------------------------------------- /src/popup/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SimpleLogin Extension 6 | 7 | 8 | 9 | <% if (NODE_ENV === 'development') { %> 10 | 11 | <% } %> 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/popup/popup.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./App"; 3 | import Clipboard from "v-clipboard"; 4 | import Toasted from "vue-toasted"; 5 | import BootstrapVue from "bootstrap-vue"; 6 | import SLStorage from "./SLStorage"; 7 | 8 | import * as Sentry from "@sentry/browser"; 9 | import * as Integrations from "@sentry/integrations"; 10 | import VModal from "vue-js-modal"; 11 | import VueRouter from "vue-router"; 12 | import ToggleButton from "vue-js-toggle-button"; 13 | 14 | import { library } from "@fortawesome/fontawesome-svg-core"; 15 | import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; 16 | import { 17 | faRandom, 18 | faExternalLinkAlt, 19 | faTrash, 20 | faLongArrowAltUp, 21 | faChevronLeft, 22 | faCopy, 23 | faStar, 24 | faSave, 25 | faBug, 26 | faQuestionCircle, 27 | faCog, 28 | faPaperPlane, 29 | faArrowLeft, 30 | } from "@fortawesome/free-solid-svg-icons"; 31 | 32 | library.add( 33 | faRandom, 34 | faExternalLinkAlt, 35 | faTrash, 36 | faLongArrowAltUp, 37 | faChevronLeft, 38 | faCopy, 39 | faStar, 40 | faSave, 41 | faBug, 42 | faQuestionCircle, 43 | faCog, 44 | faPaperPlane, 45 | faArrowLeft 46 | ); 47 | 48 | global.browser = require("webextension-polyfill"); 49 | Vue.prototype.$browser = global.browser; 50 | 51 | // async wrapper 52 | async function initApp() { 53 | const apiUrl = await SLStorage.get(SLStorage.SETTINGS.API_URL); 54 | 55 | if ( 56 | // only enable Sentry for non self-hosting users 57 | apiUrl === SLStorage.DEFAULT_SETTINGS[SLStorage.SETTINGS.API_URL] && 58 | // and not in development mode 59 | process.env.NODE_ENV !== "development" 60 | ) { 61 | Sentry.init({ 62 | dsn: "https://6990c2b0a6e94b57a2b80587efcb4354@api.protonmail.ch/core/v4/reports/sentry/51", 63 | integrations: [ 64 | new Integrations.Vue({ Vue, attachProps: true, logErrors: true }), 65 | ], 66 | environment: process.env.BETA ? "beta" : "prod", 67 | }); 68 | } 69 | 70 | Vue.use(Clipboard); 71 | Vue.use(Toasted, { duration: 1000, position: "bottom-right" }); 72 | Vue.use(BootstrapVue); 73 | Vue.use(VModal, { dialog: true }); 74 | Vue.use(VueRouter); 75 | Vue.use(ToggleButton); 76 | Vue.component("font-awesome-icon", FontAwesomeIcon); 77 | 78 | /* eslint-disable no-new */ 79 | new Vue({ 80 | el: "#app", 81 | render: (h) => h(App), 82 | }); 83 | } 84 | 85 | initApp(); 86 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const ejs = require('ejs'); 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 4 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 5 | const { VueLoaderPlugin } = require('vue-loader'); 6 | const { version, betaRev } = require('./package.json'); 7 | const fs = require('fs'); 8 | 9 | 10 | const loadDevConfig = () => { 11 | if (fs.existsSync('./.dev.json')) { 12 | return JSON.parse(fs.readFileSync('./.dev.json').toString()); 13 | } else { 14 | return JSON.parse(fs.readFileSync('./.dev.sample.json').toString()); 15 | } 16 | }; 17 | 18 | const devConfig = loadDevConfig(); 19 | 20 | const config = { 21 | mode: process.env.NODE_ENV, 22 | context: __dirname + '/src', 23 | entry: { 24 | 'background': './background/index.js', 25 | 'popup/popup': './popup/popup.js', 26 | }, 27 | output: { 28 | path: __dirname + '/dist', 29 | filename: '[name].js', 30 | }, 31 | resolve: { 32 | extensions: ['.js', '.vue'], 33 | }, 34 | module: { 35 | rules: [ 36 | { 37 | test: /\.vue$/, 38 | use: 'vue-loader', 39 | }, 40 | { 41 | test: /\.js$/, 42 | loader: 'babel-loader', 43 | exclude: /node_modules/, 44 | options: { 45 | presets: [ 46 | {'plugins': ['@babel/plugin-proposal-class-properties'] 47 | } 48 | ] 49 | } 50 | }, 51 | { 52 | test: /\.css$/, 53 | use: [MiniCssExtractPlugin.loader, 'css-loader'], 54 | }, 55 | { 56 | test: /\.scss$/, 57 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'], 58 | }, 59 | { 60 | test: /\.sass$/, 61 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader?indentedSyntax'], 62 | }, 63 | { 64 | test: /\.(png|jpg|jpeg|gif|svg|ico)$/, 65 | loader: 'file-loader', 66 | options: { 67 | name: '[name].[ext]', 68 | outputPath: '/images/', 69 | emitFile: false, 70 | }, 71 | }, 72 | { 73 | test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/, 74 | loader: 'file-loader', 75 | options: { 76 | name: '[name].[ext]', 77 | outputPath: '/fonts/', 78 | emitFile: false, 79 | }, 80 | }, 81 | ], 82 | }, 83 | plugins: [ 84 | new webpack.DefinePlugin({ 85 | global: 'window', 86 | }), 87 | new VueLoaderPlugin(), 88 | new MiniCssExtractPlugin({ 89 | filename: '[name].css', 90 | }), 91 | new CopyWebpackPlugin({ 92 | patterns: [ 93 | { from: 'icons', to: 'icons', globOptions: { ignoreFiles: ['icon.xcf'] } }, 94 | { from: 'images', to: 'images' }, 95 | { from: 'content_script', to: 'content_script' }, 96 | { from: 'popup/popup.html', to: 'popup/popup.html', transform: transformHtml }, 97 | { 98 | from: 'manifest.json', 99 | to: 'manifest.json', 100 | transform: (content) => { 101 | const jsonContent = JSON.parse(content); 102 | jsonContent.version = version; 103 | 104 | if (config.mode === 'development') { 105 | jsonContent.permissions = jsonContent.permissions.concat(devConfig.permissions); 106 | } 107 | 108 | if (process.env.BETA) { 109 | const geckoId = jsonContent.browser_specific_settings.gecko.id; 110 | jsonContent.name = 'SimpleLogin BETA'; 111 | jsonContent.icons = { 112 | '48': 'icons/icon_beta_48.png', 113 | '128': 'icons/icon_beta_128.png' 114 | }; 115 | jsonContent.version = version + '.' + betaRev; 116 | jsonContent.browser_specific_settings.gecko.id = geckoId.replace('@', '-beta@'); 117 | } 118 | 119 | if (process.env.FIREFOX) { 120 | jsonContent.background = { 121 | "scripts": ["background.js"] 122 | }; 123 | } else { // CHROME 124 | jsonContent.background = { 125 | "service_worker": "background.js", 126 | "type": "module" 127 | }; 128 | } 129 | 130 | if (process.env.LITE) { 131 | // Remove "All sites" permissions 132 | const PERMISSIONS_TO_REMOVE = [ 133 | "https://*/*", 134 | "http://*/*" 135 | ]; 136 | 137 | const finalPermissions = []; 138 | for (const perm of jsonContent.permissions) { 139 | if (!PERMISSIONS_TO_REMOVE.includes(perm)) { 140 | finalPermissions.push(perm); 141 | } 142 | } 143 | jsonContent.permissions = finalPermissions; 144 | 145 | // Change metadata 146 | jsonContent.name = "SimpleLogin Without SL icon"; 147 | jsonContent.short_name = "SimpleLogin Without SL icon"; 148 | } 149 | 150 | if (process.env.MAC) { 151 | jsonContent.permissions.push("nativeMessaging"); 152 | } 153 | 154 | return JSON.stringify(jsonContent, null, 2); 155 | }, 156 | }, 157 | ] 158 | }), 159 | ], 160 | }; 161 | 162 | console.log(`[Build] Using config.mode = ${config.mode}`); 163 | 164 | if (config.mode === 'development') { 165 | const pluginConfig = { 166 | 'devConfig': JSON.stringify(devConfig), 167 | 'process.env.BETA': JSON.stringify(!!process.env.BETA), 168 | }; 169 | 170 | console.log(`[development] Using pluginConfig: ${JSON.stringify(pluginConfig)}`); 171 | config.plugins = (config.plugins || []).concat([ 172 | new webpack.DefinePlugin(pluginConfig), 173 | ]); 174 | } 175 | 176 | if (process.env.MAC){ 177 | config.plugins = (config.plugins || []).concat([ 178 | new webpack.DefinePlugin({ 179 | 'process.env.MAC': JSON.stringify(!!process.env.MAC), 180 | }), 181 | ]); 182 | } 183 | 184 | if (config.mode === 'production') { 185 | const pluginConfig = { 186 | 'devConfig': 'null', 187 | 'process.env': { 188 | 'NODE_ENV': '"production"', 189 | 'BETA': JSON.stringify(!!process.env.BETA), 190 | } 191 | }; 192 | console.log(`[production] Using pluginConfig: ${JSON.stringify(pluginConfig)}`); 193 | config.plugins = (config.plugins || []).concat([ 194 | new webpack.DefinePlugin(pluginConfig), 195 | ]); 196 | } 197 | 198 | function transformHtml(content) { 199 | return ejs.render(content.toString(), { 200 | ...process.env, 201 | devConfig, 202 | }); 203 | } 204 | 205 | if (config.mode === 'production') { 206 | config.devtool="source-map"; 207 | } else { 208 | config.devtool="cheap-source-map"; 209 | } 210 | 211 | module.exports = config; --------------------------------------------------------------------------------