diff --git a/backend/api/account/accountController.js b/backend/api/account/accountController.js index 433614b..f36a56a 100644 --- a/backend/api/account/accountController.js +++ b/backend/api/account/accountController.js @@ -3,10 +3,9 @@ const accountService = require('./accountService'); const getAccountAllowance = async (req, res) => { try { - const response = await accountService.getAccountAllowance(); - + const username = req.body.username; + const response = await accountService.getAccountAllowance(username); debug.info(`Account Allowance Response: ${JSON.stringify(response)}`); - if (!response.success) res.status(500).json(response); else res.status(200).json(response); @@ -18,10 +17,10 @@ const getAccountAllowance = async (req, res) => { const payInvoice = async (req, res) => { try { - const response = await accountService.getAccountAllowance(); - - debug.info(`Account Allowance Response: ${JSON.stringify(response)}`); + const invoice = req.body.invoice; + const response = await accountService.payInvoice(invoice); + debug.info(`Pay Invoice Response: ${JSON.stringify(response)}`); if (!response.success) res.status(500).json(response); else res.status(200).json(response); @@ -31,12 +30,11 @@ const payInvoice = async (req, res) => { } }; -const createInvoice = async (req, res) => { +const getAccountBalance = async (req, res) => { try { - const response = await accountService.getAccountAllowance(); - - debug.info(`Account Allowance Response: ${JSON.stringify(response)}`); + const response = await accountService.getAccountBalance(); + debug.info(`Account Balance Response: ${JSON.stringify(response)}`); if (!response.success) res.status(500).json(response); else res.status(200).json(response); @@ -46,4 +44,18 @@ const createInvoice = async (req, res) => { } }; -module.exports = { getAccountAllowance, payInvoice, createInvoice }; +const createInvoice = async (req, res) => { + try { + const amountMillisats = req.body.amountMillisats; + const description = req.body.description; + const response = await accountService.createInvoice(amountMillisats, description); + debug.info(`Create Invoice Response: ${JSON.stringify(response)}`); + if (!response.success) res.status(500).json(response); + else res.status(200).json(response); + } catch (error) { + debug.error(error.stack); + res.status(500).json({ message: error.message, error: error.stack }); + } +}; + +module.exports = { getAccountAllowance, payInvoice, createInvoice, getAccountBalance }; diff --git a/backend/api/account/accountService.js b/backend/api/account/accountService.js index f94b8ba..16c000a 100644 --- a/backend/api/account/accountService.js +++ b/backend/api/account/accountService.js @@ -5,49 +5,41 @@ const accounts = require('../../db/collection'); const getAccountAllowance = async (username) => { try { // get allowance from firestore - return { success: true, message: charge }; + return { success: true, message: nodes }; } catch (error) { - debug.error(error.stack, error.status, error.message); + debug.error(error.stack); throw new Error(error); } }; -const getAccountBalance = async (username) => { +const getAccountBalance = async () => { try { - - return { success: true, message: charge }; + const response = await senseiNodes.getBalance(); + return { success: true, message: response.balance_satoshis }; } catch (error) { - debug.error(error.stack, error.status, error.message); + debug.error(error.stack); throw new Error(error); } }; -const payInvoice = async (req, res) => { +const payInvoice = async (invoice) => { try { - const invoice = req.body.invoice; const response = await senseiNodes.payInvoice(invoice); - debug.info(`Pay Invoice Response: ${JSON.stringify(response)}`); - if (!response.success) res.status(500).json(response); - else res.status(200).json(response); + return { success: true, message: response }; } catch (error) { debug.error(error.stack); - res.status(500).json({ message: error.message, error: error.stack }); + throw new Error(error); } }; -const createInvoice = async (req, res) => { +const createInvoice = async (amountMillisats, description) => { try { - const amountMillisats = req.body.amountMillisats; - const description = req.body.description; const response = await senseiNodes.createInvoice(amountMillisats, description); - debug.info(`Create Invoice Response: ${JSON.stringify(response)}`); - if (!response.success) res.status(500).json(response); - else res.status(200).json(response); - + return { success: true, message: response }; } catch (error) { debug.error(error.stack); - res.status(500).json({ message: error.message, error: error.stack }); + throw new Error(error); } }; -module.exports = { getAccountAllowance, payInvoice, createInvoice }; +module.exports = { getAccountAllowance, payInvoice, createInvoice, getAccountBalance }; diff --git a/backend/api/account/index.js b/backend/api/account/index.js index 32a31c9..98ac1ff 100644 --- a/backend/api/account/index.js +++ b/backend/api/account/index.js @@ -1,11 +1,9 @@ const express = require('express'); const router = express(); -const { getAccountAllowance, payInvoice, createInvoice } = require('./accountController'); +const { getAccountAllowance, payInvoice, createInvoice, getAccountBalance } = require('./accountController'); -const { getBalance } = require('../../sensei/nodes'); - -router.get('/account/balance', getBalance); +router.get('/account/balance', getAccountBalance); router.get('/allowance', getAccountAllowance); router.get('/payment/send', payInvoice); router.get('/payment/receive', createInvoice); diff --git a/backend/api/admin/adminController.js b/backend/api/admin/adminController.js index c463efe..d9e9da5 100644 --- a/backend/api/admin/adminController.js +++ b/backend/api/admin/adminController.js @@ -51,4 +51,52 @@ const keysend = async (req, res) => { } } -module.exports = { getAllBalances, addNewAccount, keysend }; +// Request body: +// ``` +// { +// permissions: { +// isAdmin: boolean, +// hasAllowance: boolean, +// canSpend: boolean, +// } +// } +// ``` +const updatePermissions = async (req, res) => { + try { + // TODO: Input verification logic? + const accountName = req.params.username; + const newPermissions = req.body.permissions; + const response = await adminService.updatePermissions(accountName, newPermissions); + debug.info(`Response for updating a family member's permissions: ${JSON.stringify(response)}`); + + if (!response.success) res.status(500).json(response); + else res.status(200).json(response); + } catch (error) { + debug.error(error.stack); + res.status(500).json({ message: error.message, error: error.stack }); + } +}; + +// Request body: +// ``` +// { +// allowance: number, +// } +// ``` +const setAccountAllowance = async (req, res) => { + try { + // TODO: Input verification logic? + const accountName = req.params.username; + const newAllowance = req.body.allowance; + const response = await adminService.setAccountAllowance(accountName, newAllowance); + debug.info(`Response for setting a family member's allowance: ${JSON.stringify(response)}`); + + if (!response.success) res.status(500).json(response); + else res.status(200).json(response); + } catch (error) { + debug.error(error.stack); + res.status(500).json({ message: error.message, error: error.stack }); + } +}; + +module.exports = { getAllBalances, addNewAccount, keysend, updatePermissions, setAccountAllowance }; diff --git a/backend/api/admin/adminService.js b/backend/api/admin/adminService.js index 61896b1..25251c3 100644 --- a/backend/api/admin/adminService.js +++ b/backend/api/admin/adminService.js @@ -1,14 +1,17 @@ const debug = require('../../utils/debug'); -const accounts = require('../../db/collection'); +const collection = require('../../db/collection'); const senseiAdmin = require('../../sensei/admin'); const senseiNodes = require('../../sensei/nodes'); +const DOC_NAME = 'nicks-family'; +const MEMBERS_SUBCOLLECTION_NAME = 'members'; + const getAllBalances = async (req, res) => { try { const response = await senseiAdmin.listNodes(); let nodes = []; for (let node of response.nodes) { - node.balance = (await senseiNodes.getBalance(node.username)).balance_satoshis; + node.balance = (await senseiNodes.getBalance()).balance_satoshis; nodes.push(node); } return { success: true, message: nodes }; @@ -18,4 +21,32 @@ const getAllBalances = async (req, res) => { } }; -module.exports = { getAllBalances }; +const updatePermissions = async (accountName, newPermissions) => { + const docRef = await collection.doc(DOC_NAME).collection(MEMBERS_SUBCOLLECTION_NAME).doc(accountName); + const doc = await docRef.get(); + if (!doc.exists) { + const errMsg = `Firestore document "${docName}/${subcollectionName}/${accountName}" does not exist in the families collection`; + debug.error(errMsg); + throw new Error(errMsg); + } + + // TODO: Error handling? + await docRef.update({permissions: newPermissions}); + return { success: true }; +}; + +const setAccountAllowance = async (accountName, newAllowance) => { + const docRef = await collection.doc(DOC_NAME).collection(MEMBERS_SUBCOLLECTION_NAME).doc(accountName); + const doc = await docRef.get(); + if (!doc.exists) { + const errMsg = `Firestore document "${docName}/${subcollectionName}/${accountName}" does not exist in the families collection`; + debug.error(errMsg); + throw new Error(errMsg); + } + + // TODO: Error handling? + await docRef.update({ allowance: newAllowance, "permissions.hasAllowance": true }); + return { success: true }; +}; + +module.exports = { getAllBalances, updatePermissions, setAccountAllowance }; diff --git a/backend/api/admin/index.js b/backend/api/admin/index.js index 1c6b6a1..a73d5ed 100644 --- a/backend/api/admin/index.js +++ b/backend/api/admin/index.js @@ -1,10 +1,12 @@ const express = require('express'); const router = express(); -const { getAllBalances, addNewAccount, keysend } = require('./adminController'); +const { getAllBalances, addNewAccount, keysend, updatePermissions, setAccountAllowance } = require('./adminController'); router.get('/balances', getAllBalances); router.post('/add', addNewAccount); router.post('/transfer', keysend); +router.post('/account/:username/permissions', updatePermissions) +router.post('/account/:username/allowance', setAccountAllowance) module.exports = router; diff --git a/backend/sensei/admin.js b/backend/sensei/admin.js index f8510e9..f321241 100644 --- a/backend/sensei/admin.js +++ b/backend/sensei/admin.js @@ -1,205 +1,73 @@ -const fetch = require('node-fetch'); -const BASE_URL = process.env.BASE_URL; -const MACAROON = process.env.MACAROON; -const TOKEN = process.env.TOKEN; +const { apiCall } = require('../utils/apiCall'); const initSensei = async (username, passphrase, alias, electrum_url, start) => { - const res = await fetch(`${BASE_URL}/v1/init`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Cookie': `macaroon=${MACAROON}; token=${TOKEN}` - }, - credentials: 'include', - body: { - username, - passphrase, - alias, - electrum_url, - start, - }, + const res = await apiCall('/v1/init', 'POST', { + username, + passphrase, + alias, + electrum_url, + start, }); - - console.log(res); return await res.json(); }; const listNodes = async (page, take, query) => { - const res = await fetch(`${BASE_URL}/v1/nodes?page=${0}&take=${10}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Cookie': `macaroon=${MACAROON}; token=${TOKEN}` - } - }); - - console.log(res); + const res = await apiCall('/v1/nodes?page=${0}&take=${10}', 'GET'); return await res.json(); }; const createNode = async (username, passphrase, alias, start) => { - const res = await fetch(`${BASE_URL}/v1/nodes`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Cookie': `macaroon=${MACAROON}; token=${TOKEN}` - }, - credentials: 'include', - body: { - username, - passphrase, - alias, - start, - }, + const res = await apiCall('/v1/nodes', 'POST', { + username, + passphrase, + alias, + start, }); - - console.log(res); return await res.json(); }; const startNode = async (pubkey, passphrase) => { - const res = await fetch(`${BASE_URL}/v1/nodes/start`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Cookie': `macaroon=${MACAROON}; token=${TOKEN}` - }, - credentials: 'include', - body: { - pubkey, - passphrase, - }, - }); - - console.log(res); + const res = await apiCall('/v1/nodes/start', 'POST', { pubkey, passphrase }); return await res.json(); }; const stopNode = async (pubkey) => { - const res = await fetch(`${BASE_URL}/v1/nodes/stop`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Cookie': `macaroon=${MACAROON}; token=${TOKEN}` - }, - credentials: 'include', - body: { - pubkey, - }, - }); - - console.log(res); + const res = await apiCall('/v1/nodes/stop', 'POST', { pubkey }); return await res.json(); }; const deleteNode = async (pubkey) => { - const res = await fetch(`${BASE_URL}/v1/nodes/delete`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Cookie': `macaroon=${MACAROON}; token=${TOKEN}` - }, - credentials: 'include', - body: { - pubkey, - }, - }); - - console.log(res); + const res = await apiCall('/v1/nodes/delete', 'POST', { pubkey }); return await res.json(); }; const nodeStatus = async () => { - const res = await fetch(`${BASE_URL}/v1/status`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Cookie': `macaroon=${MACAROON}; token=${TOKEN}` - }, - credentials: 'include', - }); - - console.log(res); + const res = await apiCall('/v1/status', 'GET'); return await res.json(); }; const startSensi = async (passphrase) => { - const res = await fetch(`${BASE_URL}/v1/start`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Cookie': `macaroon=${MACAROON}; token=${TOKEN}` - }, - credentials: 'include', - body: { - passphrase, - }, - }); - - console.log(res); + const res = await apiCall('/v1/start', 'POST', { passphrase }); return await res.json(); }; const login = async (username, passphrase) => { - const res = await fetch(`${BASE_URL}/v1/login`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Cookie': `macaroon=${MACAROON}; token=${TOKEN}` - }, - credentials: 'include', - body: { - username, - passphrase, - }, - }); - - console.log(res); + const res = await apiCall('/v1/login', 'POST', { username, passphrase }); return await res.json(); }; const logout = async () => { - const res = await fetch(`${BASE_URL}/v1/logout`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Cookie': `macaroon=${MACAROON}; token=${TOKEN}` - }, - credentials: 'include', - }); - - console.log(res); + const res = await apiCall('/v1/logout', 'POST'); return await res.json(); }; const getConfig = async () => { - const res = await fetch(`${BASE_URL}/v1/config`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Cookie': `macaroon=${MACAROON}; token=${TOKEN}` - }, - credentials: 'include', - }); - - console.log(res); + const res = await apiCall('/v1/config', 'GET'); return await res.json(); }; const updateConfig = async (electrum_url) => { - const res = await fetch(`${BASE_URL}/v1/config`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Cookie': `macaroon=${MACAROON}; token=${TOKEN}` - }, - credentials: 'include', - body: { - electrum_url, - }, - }); - - console.log(res); + const res = await apiCall('/v1/config', 'POST', { electrum_url }); return await res.json(); }; diff --git a/frontend/public/brontosaurus.svg b/frontend/public/brontosaurus.svg new file mode 100644 index 0000000..0ebf409 --- /dev/null +++ b/frontend/public/brontosaurus.svg @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/flowers.jpg b/frontend/public/flowers.jpg new file mode 100644 index 0000000..97fc70f Binary files /dev/null and b/frontend/public/flowers.jpg differ diff --git a/frontend/public/spinosaurus.svg b/frontend/public/spinosaurus.svg new file mode 100644 index 0000000..aaa585d --- /dev/null +++ b/frontend/public/spinosaurus.svg @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/App.js b/frontend/src/App.js index ae375b5..3403edf 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -18,6 +18,8 @@ import { ContactsIcon, CrossIcon, GearIcon, HomeIcon, MenuIcon } from '@bitcoin- import { ChartSquareBarIcon } from '@heroicons/react/solid'; import NewFamily from './routes/NewFamily'; import Invite from './routes/Invite'; +import Kid from './routes/Kid'; +import Grandma from './routes/Grandma'; const Child = {}; @@ -78,6 +80,8 @@ function App() { } /> } /> + } /> + } /> ); diff --git a/frontend/src/index.css b/frontend/src/index.css index 4523ea7..713d987 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -17,6 +17,14 @@ background-size: cover; } +.grandma-bg { + @apply fixed h-screen w-screen z-0; + /*opacity: 0.10;*/ + filter: blur(2px); + background-image: url('../public/flowers.jpg'); + background-size: cover; +} + h1, h2 { @apply font-display; } @@ -31,4 +39,16 @@ h1, h2 { .slots *:last-child { @apply border-b-0; +} + +.kid { + @apply bg-[#7350BC]; +} + +.grandma { + @apply bg-rose-100 text-neutral-600 border-rose-300; +} + +.grandma.slots > div { + @apply border-rose-300; } \ No newline at end of file diff --git a/frontend/src/routes/Family.js b/frontend/src/routes/Family.js index ff8dbbf..1d139e5 100644 --- a/frontend/src/routes/Family.js +++ b/frontend/src/routes/Family.js @@ -21,13 +21,13 @@ const Family = () => { name: 'Jacob', photo: '/child.jpg', balance: '2,350', - id: 'jacob' + id: 'jacob/kid' }, { name: 'Gertrude', photo: '/grandmother.jpg', balance: '90,000,023', - id: 'gertrude' + id: 'gertrude/grandma' } ]) diff --git a/frontend/src/routes/Grandma.js b/frontend/src/routes/Grandma.js new file mode 100644 index 0000000..0d9d170 --- /dev/null +++ b/frontend/src/routes/Grandma.js @@ -0,0 +1,119 @@ +import React from "react"; +import {Link, useParams} from "react-router-dom"; +import {ArrowDownIcon, ArrowUpIcon, BitcoinCircleIcon} from '@bitcoin-design/bitcoin-icons-react/filled'; +// unique account within family page +const Account = () => { + + + const {account} = useParams() + + const [transactions, setTransactions] = React.useState([ + { + counterparty: 'Miller Insurance Co.', + memo: 'Insurance payout', + date: '2022-06-01 14:55', + amount: 200000, + direction: 'receive' + }, + { + counterparty: 'Sunnyville', + memo: 'Assisted living rent', + date: '2022-06-01 14:55', + amount: 100000, + direction: 'send' + }, + { + counterparty: 'Dr. Melissa Schafer, MD', + memo: 'Quarterly checkup', + date: '2022-06-01 14:55', + amount: 100000, + direction: 'send' + }, + { + counterparty: 'Miller Insurance Co.', + memo: 'Insurance payout', + date: '2022-06-01 14:55', + amount: 200000, + direction: 'receive' + }, + { + counterparty: 'Sunnyville', + memo: 'Assisted living rent', + date: '2022-06-01 14:55', + amount: 100000, + direction: 'send' + }, + { + counterparty: 'Dr. Melissa Schafer, MD', + memo: 'Quarterly checkup', + date: '2022-06-01 14:55', + amount: 100000, + direction: 'send' + }, + { + counterparty: 'Miller Insurance Co.', + memo: 'Insurance payout', + date: '2022-06-01 14:55', + amount: 200000, + direction: 'receive' + }, + { + counterparty: 'Sunnyville', + memo: 'Assisted living rent', + date: '2022-06-01 14:55', + amount: 100000, + direction: 'send' + }, + { + counterparty: 'Dr. Melissa Schafer, MD', + memo: 'Quarterly checkup', + date: '2022-06-01 14:55', + amount: 100000, + direction: 'send' + }, + ]) + + return ( +
+
+
+

Gertrude's Wallet

+ +
+

Your Balance

+

70,000,000 sats

+
+ +
+ {transactions.map((tx, index)=>{ + return( +
+
+ {tx.direction === 'send' ? + + + + : + + + + } +
+
+

{tx.counterparty}

+

{tx.date}

+

{tx.memo}

+
+
+

{tx.amount}

+
+
+ ) + })} +
+
+
+ ); +}; + +export default Account; diff --git a/frontend/src/routes/Kid.js b/frontend/src/routes/Kid.js new file mode 100644 index 0000000..9c01e93 --- /dev/null +++ b/frontend/src/routes/Kid.js @@ -0,0 +1,128 @@ +import React from "react"; +import {Link, useParams} from "react-router-dom"; +import {ArrowDownIcon, ArrowUpIcon, BitcoinCircleIcon} from '@bitcoin-design/bitcoin-icons-react/filled'; +// unique account within family page +const Account = () => { + + + const {account} = useParams() + + const [accountInfo, setAccountInfo] = React.useState({ + name: '· · ·', + photo: '/father.jpg', + balance: '- - -', + id: 'placeholder' + }) + + const [transactions, setTransactions] = React.useState([ + { + counterparty: 'Parents', + memo: 'Savings', + date: '2022-06-01 14:55', + amount: 10000, + direction: 'receive' + }, + { + counterparty: 'Parents', + memo: 'Savings', + date: '2022-06-01 14:55', + amount: 10000, + direction: 'receive' + }, + { + counterparty: 'Parents', + memo: 'Savings', + date: '2022-06-01 14:55', + amount: 10000, + direction: 'receive' + }, + { + counterparty: 'Parents', + memo: 'Savings', + date: '2022-06-01 14:55', + amount: 10000, + direction: 'receive' + }, + { + counterparty: 'Parents', + memo: 'Savings', + date: '2022-06-01 14:55', + amount: 10000, + direction: 'receive' + }, + { + counterparty: 'Parents', + memo: 'Savings', + date: '2022-06-01 14:55', + amount: 10000, + direction: 'receive' + }, + { + counterparty: 'Parents', + memo: 'Savings', + date: '2022-06-01 14:55', + amount: 10000, + direction: 'receive' + }, + { + counterparty: 'Parents', + memo: 'Savings', + date: '2022-06-01 14:55', + amount: 10000, + direction: 'receive' + }, + ]) + + + return ( +
+
+

Jacob's Wallet

+ +
+

Your Balance

+

1000 sats

+
+ +
+ Spinosaurus +
+ +
+ {transactions.map((tx, index)=>{ + return( +
+
+ {tx.direction === 'send' ? + + + + : + + + + } +
+
+

{tx.counterparty}

+

{tx.date}

+

{tx.memo}

+
+
+

{tx.amount}

+
+
+ ) + })} +
+ +
+ Brontosaurus +
+ +
+
+ ); +}; + +export default Account;