From 780d9192cbddc498f2fb0016d13f37b8da1862b4 Mon Sep 17 00:00:00 2001 From: Bruno Bernardino Date: Sun, 2 Jan 2022 23:00:16 +0000 Subject: [PATCH] Explore end-to-end encryption for data (v2) This enables end-to-end encryption via Etebase. Everything works, but still lacks billing control. Currently deployed to https://v2.budgetzen.net --- .babelrc | 11 +- .env.sample | 1 + .eslintrc.js | 4 +- .github/FUNDING.yml | 6 +- .gitignore | 1 + .npmrc | 1 + .nvmrc | 2 +- Makefile | 2 +- README.md | 15 +- components/BudgetModal.tsx | 35 +- components/Button/__snapshots__/test.tsx.snap | 2 +- components/Button/index.tsx | 25 +- .../Button/{style.scss => styles.module.scss} | 2 +- components/ExpenseModal.tsx | 33 +- components/FilterBudgetModal.tsx | 9 +- components/ImportExportModal.tsx | 49 +- .../{Footer.scss => Footer.module.scss} | 2 +- components/Layout/Footer.tsx | 16 +- .../{Header.scss => Header.module.scss} | 2 +- components/Layout/Header.tsx | 6 +- components/Loading/index.tsx | 8 +- .../{style.scss => styles.module.scss} | 0 components/Panels/AddExpense.tsx | 23 +- components/Panels/All.tsx | 43 +- components/Panels/Budgets.tsx | 19 +- components/Panels/Expenses.tsx | 14 +- components/Panels/Login.tsx | 9 +- components/Panels/Settings.tsx | 44 +- components/TextInput/index.tsx | 21 +- .../{style.scss => styles.module.scss} | 2 +- jest.setup.js | 2 +- lib/constants.ts | 3 +- lib/data-utils.ts | 832 +- lib/types.ts | 31 +- lib/utils.ts | 10 +- modules/auth/LoginButton.tsx | 56 +- modules/auth/LogoutLink.tsx | 10 +- next-env.d.ts | 5 +- next.config.js | 60 +- package-lock.json | 37538 +++++++++------- package.json | 86 +- serverless.yml | 15 +- styles/__base.scss | 10 +- styles/_common.scss | 7 +- styles/main.scss | 2 +- tsconfig.json | 3 +- 46 files changed, 22024 insertions(+), 17053 deletions(-) create mode 100644 .npmrc rename components/Button/{style.scss => styles.module.scss} (98%) rename components/Layout/{Footer.scss => Footer.module.scss} (98%) rename components/Layout/{Header.scss => Header.module.scss} (83%) rename components/Loading/{style.scss => styles.module.scss} (100%) rename components/TextInput/{style.scss => styles.module.scss} (97%) diff --git a/.babelrc b/.babelrc index 854cb73..14ef043 100644 --- a/.babelrc +++ b/.babelrc @@ -1,4 +1,13 @@ { "presets": ["next/babel"], - "plugins": [["styled-components", { "ssr": true }]] + "plugins": [ + [ + "styled-components", + { + "ssr": true, + "displayName": true, + "preprocess": false + } + ] + ] } diff --git a/.env.sample b/.env.sample index c25efa0..db2d2d9 100644 --- a/.env.sample +++ b/.env.sample @@ -1,3 +1,4 @@ NODE_PATH=/ AWS_ACCESS_KEY_ID=accesskey AWS_SECRET_ACCESS_KEY=sshhh +NEXT_PUBLIC_ETEBASE_SERVER_URL=https://api.etebase.com/example diff --git a/.eslintrc.js b/.eslintrc.js index 428ca8c..dfc7bd0 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,7 +4,7 @@ const eslint = { 'airbnb', 'eslint:recommended', 'plugin:@typescript-eslint/recommended', - 'prettier/@typescript-eslint', + 'prettier', ], rules: { semi: 2, @@ -69,6 +69,8 @@ const eslint = { 'function-paren-newline': 'off', 'no-confusing-arrow': 'off', 'react/jsx-curly-newline': 'off', + 'react/function-component-definition': 'off', + 'react/jsx-no-useless-fragment': 'off', }, parserOptions: { ecmaFeatures: { diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 1013541..30b98b4 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,6 @@ github: [BrunoBernardino] -custom: ["https://paypal.me/brunobernardino", "https://gist.github.com/BrunoBernardino/ff5b54c13dd96ac7f9fee6fbfd825b09"] +custom: + [ + 'https://paypal.me/brunobernardino', + 'https://gist.github.com/BrunoBernardino/ff5b54c13dd96ac7f9fee6fbfd825b09', + ] diff --git a/.gitignore b/.gitignore index 56633fa..59ee055 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ node_modules **/public/workbox-*.js.map **/public/worker-*.js.map **/public/fallback-*.js +*.tsbuildinfo diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..cffe8cd --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +save-exact=true diff --git a/.nvmrc b/.nvmrc index 59db31c..f0b10f1 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v12.16.1 +v16.13.1 diff --git a/Makefile b/Makefile index d24f7f4..0fbce29 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: install install: -cp -n .env.sample .env - npm install + npm install --legacy-peer-deps .PHONY: start start: diff --git a/README.md b/README.md index f7dd2eb..6dc2c16 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ -# Budget Zen - Web App +# Budget Zen v2 - Web App [![](https://github.com/BrunoBernardino/budgetzen-web/workflows/Run%20Tests/badge.svg)](https://github.com/BrunoBernardino/budgetzen-web/actions?workflow=Run+Tests) -This is the web app for the [Budget Zen app](https://budgetzen.net), built with Next.js and deployed to AWS with Vercel. +This is the web app for the [Budget Zen app](https://budgetzen.net), built with Next.js and deployed to AWS with Serverless. -It runs completely in the browser, using `localStorage` and `IndexedDB`. +It's v2, meaning it is [end-to-end encrypted via etebase](https://etebase.com), and requires an email + [token](https://budgetzen.net/get-sync-token) to work. -It's not thoroughly tested just yet, so it's available but not announced. +It also means it's not compatible with Budget Zen v1, which you can still get locally from [this commit](https://github.com/BrunoBernardino/budgetzen-web/tree/397d625469b7dfd8d1968c847b32e607ee7c8ee9). ## Development @@ -15,12 +15,13 @@ make install # installs dependencies make start # starts the app make pretty # prettifies the code make test # runs linting and tests -make deploy # deploys to app.budgetzen.net (requires `serverless` to be installed globally) +make deploy # deploys to v2.budgetzen.net (requires `serverless` to be installed globally) ``` ## TODOs -- [ ] Allow using app without a Sync Token +- [ ] Implement billing in signup +- [ ] Implement fetching in chunks + incrementally, via stoken? +- [ ] Implement encrypted session? - [ ] Improve UI/UX in general - [ ] Improve dark/light mode -- [ ] Improve mobile view (collapse panels and show tab bar to navigate between them?) diff --git a/components/BudgetModal.tsx b/components/BudgetModal.tsx index b474cbc..a3b694a 100644 --- a/components/BudgetModal.tsx +++ b/components/BudgetModal.tsx @@ -2,7 +2,7 @@ import React, { useState, useCallback } from 'react'; import styled from 'styled-components'; import Rodal from 'rodal'; import Swal from 'sweetalert2'; -import { RxDatabase } from 'rxdb'; +import * as Etebase from 'etebase'; import Button from 'components/Button'; import { showNotification } from 'lib/utils'; @@ -18,7 +18,7 @@ interface BudgetModalProps { month: string; value: number; reloadData: () => Promise; - db: RxDatabase; + etebase: Etebase.Account; } const Container = styled.section` @@ -59,17 +59,13 @@ const Input = styled.input` } `; -const StyledButton = styled(Button)` - margin: 20px 0; -`; - const BudgetModal = (props: BudgetModalProps) => { const [isSubmitting, setIsSubmitting] = useState(false); const [name, setName] = useState(props.name); const [month, setMonth] = useState(`${props.month}-01`); const [value, setValue] = useState(props.value.toString()); - const { id, isOpen, reloadData, db } = props; + const { id, isOpen, reloadData, etebase } = props; const onClose = useCallback(() => { const { onClose: closeModal } = props; @@ -91,10 +87,10 @@ const BudgetModal = (props: BudgetModalProps) => { id: id || 'newBudget', value: Number.parseFloat(value.replace(',', '.')), name, - month: month ? month.substr(0, 7) : '', + month: month ? month.substring(0, 7) : '', }; - const success = await saveBudget(db, parsedBudget); + const success = await saveBudget(etebase, parsedBudget); setIsSubmitting(false); @@ -115,8 +111,7 @@ const BudgetModal = (props: BudgetModalProps) => { const confirmationResult = await Swal.fire({ icon: 'warning', title: 'Are you sure?', - text: - 'Are you sure you want to delete this budget?\n\nThis action is irreversible.', + text: 'Are you sure you want to delete this budget?\n\nThis action is irreversible.', showDenyButton: true, showCancelButton: true, confirmButtonText: 'Yes!', @@ -129,7 +124,7 @@ const BudgetModal = (props: BudgetModalProps) => { setIsSubmitting(true); - const success = await deleteBudget(db, id); + const success = await deleteBudget(etebase, id); setIsSubmitting(false); @@ -185,14 +180,22 @@ const BudgetModal = (props: BudgetModalProps) => { onKeyDown={onKeyDown} /> - addBudget()} type="primary"> + {Boolean(id) && ( - removeBudget()} type="delete"> + )} diff --git a/components/Button/__snapshots__/test.tsx.snap b/components/Button/__snapshots__/test.tsx.snap index dc5f8c9..14165c8 100644 --- a/components/Button/__snapshots__/test.tsx.snap +++ b/components/Button/__snapshots__/test.tsx.snap @@ -2,7 +2,7 @@ exports[`Button renders the button as expected 1`] = ` {Boolean(id) && ( - removeExpense()} type="delete"> + )} diff --git a/components/FilterBudgetModal.tsx b/components/FilterBudgetModal.tsx index 878f0a1..f2414db 100644 --- a/components/FilterBudgetModal.tsx +++ b/components/FilterBudgetModal.tsx @@ -45,13 +45,8 @@ const BudgetName = styled.span` `; const FilterBudgetModal = (props: FilterBudgetModalProps) => { - const { - isOpen, - onClose, - budgets, - onFilterBudgetToggle, - filterBudgets, - } = props; + const { isOpen, onClose, budgets, onFilterBudgetToggle, filterBudgets } = + props; return ( diff --git a/components/ImportExportModal.tsx b/components/ImportExportModal.tsx index 9430e7f..ad62d7d 100644 --- a/components/ImportExportModal.tsx +++ b/components/ImportExportModal.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import styled from 'styled-components'; import Rodal from 'rodal'; import Swal from 'sweetalert2'; -import { RxDatabase } from 'rxdb'; +import * as Etebase from 'etebase'; import Button from 'components/Button'; import { showNotification } from 'lib/utils'; @@ -16,8 +16,8 @@ type ImportedFileData = { }; interface ImportExportModalProps { - db: RxDatabase; - syncToken: string; + etebase: Etebase.Account; + session: string; isOpen: boolean; onClose: () => void; } @@ -46,15 +46,10 @@ const Note = styled.span` margin-top: 30px; `; -const StyledButton = styled(Button)` - margin-top: 20px; - align-self: center; -`; - const ImportExportModal = (props: ImportExportModalProps) => { const [isSubmitting, setIsSubmitting] = useState(false); - const { isOpen, onClose, db, syncToken } = props; + const { isOpen, onClose, etebase, session } = props; const onRequestImport = async () => { if (isSubmitting) { @@ -105,8 +100,7 @@ const ImportExportModal = (props: ImportExportModalProps) => { const mergeOrReplaceDialogResult = await Swal.fire({ icon: 'question', title: 'Merge or Replace?', - text: - 'Do you want to merge this with your existing data, or replace it?', + text: 'Do you want to merge this with your existing data, or replace it?', showCancelButton: true, showDenyButton: true, confirmButtonText: 'Merge', @@ -121,8 +115,8 @@ const ImportExportModal = (props: ImportExportModalProps) => { setIsSubmitting(true); const success = await importData( - db, - syncToken, + etebase, + session, mergeOrReplaceDialogResult.isDenied, budgets, expenses, @@ -149,10 +143,10 @@ const ImportExportModal = (props: ImportExportModalProps) => { const fileName = `data-export-${new Date() .toISOString() - .substr(0, 19) + .substring(0, 19) .replace(/:/g, '-')}.json`; - const exportData = await exportAllData(db); + const exportData = await exportAllData(etebase); const exportContents = JSON.stringify(exportData, null, 2); @@ -176,23 +170,34 @@ const ImportExportModal = (props: ImportExportModalProps) => { - Import a JSON file exported from Budget Zen before. + + Import a JSON file exported from Budget Zen (v1 or v2) before. + - Learn more - + - onRequestImport()} type="secondary"> + - onRequestExport()} type="primary"> + ); diff --git a/components/Layout/Footer.scss b/components/Layout/Footer.module.scss similarity index 98% rename from components/Layout/Footer.scss rename to components/Layout/Footer.module.scss index a466096..8885c89 100644 --- a/components/Layout/Footer.scss +++ b/components/Layout/Footer.module.scss @@ -1,4 +1,4 @@ -@import '__variables'; +@import 'styles/__variables'; .Footer { display: block; diff --git a/components/Layout/Footer.tsx b/components/Layout/Footer.tsx index 4421917..32e16a9 100644 --- a/components/Layout/Footer.tsx +++ b/components/Layout/Footer.tsx @@ -1,13 +1,13 @@ -import './Footer.scss'; +import styles from './Footer.module.scss'; const Footer = () => { return ( -