diff --git a/.gitignore b/.gitignore index 57b9554..bd70049 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,15 @@ psd thumb sketch +<<<<<<< HEAD +*.lock + +# End of https://www.toptal.com/developers/gitignore/api/react +======= # End of https://www.toptal.com/developers/gitignore/api/react **/service-account.json +>>>>>>> 18ffdb861cdf24a7ebaea8c6f91205ed97a03c98 + +# Misc. secrets (API keys, GCP credentials, etc.) +.env diff --git a/backend/.github/workflows/deploy.yml b/backend/.github/workflows/deploy.yml new file mode 100644 index 0000000..864187b --- /dev/null +++ b/backend/.github/workflows/deploy.yml @@ -0,0 +1,49 @@ +name: park-ln + +on: + push: + branches: + - master + - devops/deploy/backend + +env: + CLOUD_RUN_PROJECT_ID: park-lightning + CLOUD_RUN_REGION: us-east1 + REPO_NAME: park-lightning + +jobs: + deploy-backend: + name: Deploy Backend + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - uses: google-github-actions/setup-gcloud@master + with: + project_id: $CLOUD_RUN_PROJECT_ID + credentials_json: ${{ secrets.GCP_CREDENTIALS_JSON }} + + - name: test gcloud CLI + run: gcloud info + + - name: Enable the necessary APIs and enable docker auth + run: |- + gcloud services enable containerregistry.googleapis.com + gcloud services enable run.googleapis.com + gcloud --quiet auth configure-docker + - name: Build and tag image + run: |- + docker build . --tag "gcr.io/$CLOUD_RUN_PROJECT_ID/$REPO_NAME:$GITHUB_SHA" + - name: Push image to GCR + run: |- + docker push gcr.io/$CLOUD_RUN_PROJECT_ID/$REPO_NAME:$GITHUB_SHA + - name: Deploy + run: |- + gcloud components install beta --quiet + gcloud beta run deploy $REPO_NAME --image gcr.io/$CLOUD_RUN_PROJECT_ID/$REPO_NAME:$GITHUB_SHA \ + --project $CLOUD_RUN_PROJECT_ID \ + --platform managed \ + --region $CLOUD_RUN_REGION \ + --allow-unauthenticated \ + --quiet \ No newline at end of file diff --git a/backend/API.md b/backend/API.md new file mode 100644 index 0000000..6fcf284 --- /dev/null +++ b/backend/API.md @@ -0,0 +1,183 @@ +# API Route Spec +Below is an outline of the API routes available to both users (parking and paying) and admins (enforcing parking rules). + +## Routes +User Types +1. Drivers +2. Parking Enforcement + +API Data Objects +1. [Spot](./api/spot/) +2. [Invoice](./api/invoice/) + +5 Routes +1. Route: `/api/v1/spot/details` + - Method: `GET` + - URL: https://park-lightning-foiudx76uq-ue.a.run.app/api/v1/spot/details?uuid=d8d05dce-dbae-421b-9bdd-ea3ce75b7a77 +```json +Body: None + +Returns: + { + "success": true, + "message": { + "address": "691 John Wesley Dobbs Ave, Atlanta, GA 30312", + "expirationTime": 1648319420, + "startTime": 1648317620, + "expired": false, + "licensePlate": "SC39133", + "occupied": true, + "duration": 1800 + } + } +``` +2. Route: `/api/v1/spot/reserve` + - Method: `POST` + - URL: https://park-lightning-foiudx76uq-ue.a.run.app/api/v1/spot/reserve +```json +Body: + { + "uuid": "da1c0d1b-1ecf-4fe0-9acd-dc3d49640f8f", + "licensePlate": "BOE8359", + "duration": 1800 + } + +Returns: + { + "success": true, + "message": { + "_writeTime": { + "_seconds": 1648324950, + "_nanoseconds": 756013000 + } + } + } +``` +3. Route: `/api/v1/invoice/create` + - Method: `POST` + - URL: https://park-lightning-foiudx76uq-ue.a.run.app/api/v1/invoice/create +```json +Body: + { + "amount": 1, + "memo": { + "licensePlate": "BOE8359", + "uuid": "da1c0d1b-1ecf-4fe0-9acd-dc3d49640f8f", + "duration": 1800 + } + } + +Returns: + { + "success": true, + "message": { + "id": "dd610b73-aed9-456f-9cd6-7d4aeff29407", + "description": "{\"licensePlate\":\"BOE8359\",\"uuid\":\"da1c0d1b-1ecf-4fe0-9acd-dc3d49640f8f\",\"duration\":1800}", + "amount": 2261, + "missing_amt": 2261, + "status": "unpaid", + "fiat_value": 1, + "source_fiat_value": 1, + "currency": "USD", + "created_at": 1648392719, + "order_id": null, + "address": "31zEqCfzvvT8kBr9fh4U3bZj1AZM5Q4vsb", + "metadata": {}, + "expires_at": "2022-03-27T15:01:59.758Z", + "auto_settle": false, + "lightning_invoice": { + "created_at": 1648392719, + "expires_at": 1648393318, + "payreq": "lnbc22610n1p3yq7s0pp5nfzpah463t9v25c5ja0gpy4uq5ga3lq96a6pe6adsw76dvqgv8zqdyq0v3xc6trv4h8xe2sd3shgefz8g3yyn698qen2wfz9s382atfvs3r5gnyvyckxvryx93z6vt9vdnz6drxv5cz6wtpvdjz6errxdjrgwfkxscxvwrxygkzyer4wfsjut3wcqzpgxqzjhsp58x3y8h4npmp9ws76qrrhgadsr2pa9ls6p7zxtd4k2k9p87cqjq6q9qyyssqp65meh539k2ungxpq7vw9j8s37slhg3gjcxvmnpe0fw67xuey2xpu7pcgxt5c8c5kecr4l9y5a4rd8dt0flrg98m5r2w9rkyqgc4mzsqhvm98w", + "settled_at": null + }, + "chain_invoice": { + "address": "31zEqCfzvvT8kBr9fh4U3bZj1AZM5Q4vsb" + }, + "transactions": [], + "uri": "bitcoin:31zEqCfzvvT8kBr9fh4U3bZj1AZM5Q4vsb?amount=0.00002261&label={\"licensePlate\":\"BOE8359\",\"uuid\":\"da1c0d1b-1ecf-4fe0-9acd-dc3d49640f8f\",\"duration\":1800}&lightning=LNBC22610N1P3YQ7S0PP5NFZPAH463T9V25C5JA0GPY4UQ5GA3LQ96A6PE6ADSW76DVQGV8ZQDYQ0V3XC6TRV4H8XE2SD3SHGEFZ8G3YYN698QEN2WFZ9S382ATFVS3R5GNYVYCKXVRYX93Z6VT9VDNZ6DRXV5CZ6WTPVDJZ6ERRXDJRGWFKXSCXVWRXYGKZYER4WFSJUT3WCQZPGXQZJHSP58X3Y8H4NPMP9WS76QRRHGADSR2PA9LS6P7ZXTD4K2K9P87CQJQ6Q9QYYSSQP65MEH539K2UNGXPQ7VW9J8S37SLHG3GJCXVMNPE0FW67XUEY2XPU7PCGXT5C8C5KECR4L9Y5A4RD8DT0FLRG98M5R2W9RKYQGC4MZSQHVM98W" + } + } + +``` +4. Route: `/api/v1/invoice/check` + - Method: `POST` + - URL: https://park-lightning-foiudx76uq-ue.a.run.app/api/v1/invoice/create +```json +Body: + { + "id": "dd610b73-aed9-456f-9cd6-7d4aeff29407" + } + +Returns: + { + "success": true, + "message": { + "id": "dd610b73-aed9-456f-9cd6-7d4aeff29407", + "description": "{\"licensePlate\":\"BOE8359\",\"uuid\":\"da1c0d1b-1ecf-4fe0-9acd-dc3d49640f8f\",\"duration\":1800}", + "amount": 2261, + "missing_amt": 2261, + "status": "unpaid", + "fiat_value": 1, + "source_fiat_value": 1, + "currency": "USD", + "created_at": 1648392719, + "order_id": null, + "address": "31zEqCfzvvT8kBr9fh4U3bZj1AZM5Q4vsb", + "metadata": {}, + "expires_at": "2022-03-27T15:01:59.758Z", + "auto_settle": false, + "lightning_invoice": { + "created_at": 1648392719, + "expires_at": 1648393318, + "payreq": "lnbc22610n1p3yq7s0pp5nfzpah463t9v25c5ja0gpy4uq5ga3lq96a6pe6adsw76dvqgv8zqdyq0v3xc6trv4h8xe2sd3shgefz8g3yyn698qen2wfz9s382atfvs3r5gnyvyckxvryx93z6vt9vdnz6drxv5cz6wtpvdjz6errxdjrgwfkxscxvwrxygkzyer4wfsjut3wcqzpgxqzjhsp58x3y8h4npmp9ws76qrrhgadsr2pa9ls6p7zxtd4k2k9p87cqjq6q9qyyssqp65meh539k2ungxpq7vw9j8s37slhg3gjcxvmnpe0fw67xuey2xpu7pcgxt5c8c5kecr4l9y5a4rd8dt0flrg98m5r2w9rkyqgc4mzsqhvm98w", + "settled_at": null + }, + "chain_invoice": { + "address": "31zEqCfzvvT8kBr9fh4U3bZj1AZM5Q4vsb" + }, + "transactions": [], + "uri": "bitcoin:31zEqCfzvvT8kBr9fh4U3bZj1AZM5Q4vsb?amount=0.00002261&label={\"licensePlate\":\"BOE8359\",\"uuid\":\"da1c0d1b-1ecf-4fe0-9acd-dc3d49640f8f\",\"duration\":1800}&lightning=LNBC22610N1P3YQ7S0PP5NFZPAH463T9V25C5JA0GPY4UQ5GA3LQ96A6PE6ADSW76DVQGV8ZQDYQ0V3XC6TRV4H8XE2SD3SHGEFZ8G3YYN698QEN2WFZ9S382ATFVS3R5GNYVYCKXVRYX93Z6VT9VDNZ6DRXV5CZ6WTPVDJZ6ERRXDJRGWFKXSCXVWRXYGKZYER4WFSJUT3WCQZPGXQZJHSP58X3Y8H4NPMP9WS76QRRHGADSR2PA9LS6P7ZXTD4K2K9P87CQJQ6Q9QYYSSQP65MEH539K2UNGXPQ7VW9J8S37SLHG3GJCXVMNPE0FW67XUEY2XPU7PCGXT5C8C5KECR4L9Y5A4RD8DT0FLRG98M5R2W9RKYQGC4MZSQHVM98W" + } + } +``` + +5. Route: `/api/v1/spot/empty` + - Method: `GET` + - URL: https://park-lightning-foiudx76uq-ue.a.run.app/api/v1/spot/empty + ```json + Body: None + + Returns: + { + "success": true, + "message": { + "unoccupiedSpots": [ + { + "address": "691 John Wesley Dobbs Ave, Atlanta, GA 30312", + "occupied": false + } + ], + "expiredSpots": [ + { + "expired": true, + "expirationTime": 1648319420, + "duration": 1800, + "address": "691 John Wesley Dobbs Ave, Atlanta, GA 30312", + "occupied": true, + "licensePlate": "SC39133", + "startTime": 1648317620 + }, + { + "occupied": true, + "address": "79 5th St NW, Atlanta, GA 30308", + "expirationTime": 1648327169, + "expired": true, + "duration": 1800, + "licensePlate": "BOE8359", + "startTime": 1648325369 + } + ] + } + } +``` diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..4366065 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,11 @@ +FROM node:16.14.2 + +WORKDIR /usr/src/app + +COPY . . + +RUN yarn + +EXPOSE 8080 4000 + +CMD [ "yarn", "start" ] \ No newline at end of file diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..85d5b31 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,13 @@ +# FamilyBTC Backend + +An easy to use and easy to manage Bitcoin wallet catered to families with members in all stages of life. + +## Development + +### Environment Setup + +Obtain a JSON file with credentials for a Firebase service account. Then, + +```bash +echo "GOOGLE_APPLICATION_CREDENTIALS=`base64 -i PATH_TO_CREDS_FILE`" > .env +``` diff --git a/backend/api/Admin.js b/backend/api/Admin.js deleted file mode 100644 index fc5ec0f..0000000 --- a/backend/api/Admin.js +++ /dev/null @@ -1,85 +0,0 @@ -//Parent Account Requests - -// CreateNewAccount -// - ManageChildren -// - GetChildren -// - InviteChild -// - Role (1-4) -// - TransitionChildRole -// - SetChildAllowance -// - TransferFunds - -import db, { getFamily } from "../db/db"; -import { - collection, - doc, - setDoc, - updateDoc, - arrayUnion, - getDocs, - query, - where, -} from "firebase/firestore"; - -export const GetFamily = async (familyName) => { - console.log("Getting Family"); - return await getFamily(familyName); -}; - -export const CreateNewFamily = async (familyName) => { - await setDoc(doc(db, "families", familyName), { - familyName: familyName, - children: [], - }); - return { success: true }; -}; -export const GetChildren = async (familyId) => { - return { children }; -}; - -export const InviteChild = async (familyId, childName, role) => { - return { success: true }; -}; - -export const AddChild = async ( - familyName, - childName, - role, - balance, - allowance -) => { - const coll = collection(db, "families"); - const docs = await getDocs(coll); - console.log("getDocs", docs); - let famDoc = null; - await docs.forEach((doc) => { - console.log("snap", doc); - famDoc = doc; - }); - - await updateDoc(famDoc, { - children: arrayUnion({ - allowance: allowance, - name: childName, - role: role, - balance: balance, - }), - }); - return { success: true, familyDoc }; -}; - -export const TransitionChildRole = async (familyId, childId, newRole) => { - return { success: true }; -}; - -export const SetChildAllowance = async (familyId, childId, allowance) => { - return { success: true }; -}; - -export const TransferFunds = async (familyId, childId, amount) => { - return { success: true }; -}; - -export const ExportChild = async (familyId, childId) => { - return { success: true }; -}; diff --git a/backend/api/Child.js b/backend/api/Child.js deleted file mode 100644 index 021a07a..0000000 --- a/backend/api/Child.js +++ /dev/null @@ -1,26 +0,0 @@ -// Children Account Requests -// - GetBalance -// - GetTransactions -// - GetAllowance -// - SendPayment -// - RequestOneTimeAllowance - -export const GetBalance = async (familyId, childId) => { - return { balance: 0 }; -}; - -export const GetTransactions = async (familyId, childId) => { - return { transactions: [] }; -}; - -export const GetAllowance = async (familyId, childId) => { - return { allowance: 0 }; -}; - -export const SendPayment = async (familyId, childId, amount) => { - return { success: true }; -}; - -export const RequestOneTimeAllowance = async (familyId, childId, amount) => { - return { success: true }; -}; diff --git a/backend/api/account/accountController.js b/backend/api/account/accountController.js new file mode 100644 index 0000000..433614b --- /dev/null +++ b/backend/api/account/accountController.js @@ -0,0 +1,49 @@ +const debug = require('../../utils/debug'); +const accountService = require('./accountService'); + +const getAccountAllowance = async (req, res) => { + try { + const response = await accountService.getAccountAllowance(); + + debug.info(`Account Allowance 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 }); + } +}; + +const payInvoice = async (req, res) => { + try { + const response = await accountService.getAccountAllowance(); + + debug.info(`Account Allowance 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 }); + } +}; + +const createInvoice = async (req, res) => { + try { + const response = await accountService.getAccountAllowance(); + + debug.info(`Account Allowance 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 }; diff --git a/backend/api/account/accountService.js b/backend/api/account/accountService.js new file mode 100644 index 0000000..f94b8ba --- /dev/null +++ b/backend/api/account/accountService.js @@ -0,0 +1,53 @@ +const debug = require('../../utils/debug'); +const senseiNodes = require('../../sensei/nodes'); +const accounts = require('../../db/collection'); + +const getAccountAllowance = async (username) => { + try { + // get allowance from firestore + return { success: true, message: charge }; + } catch (error) { + debug.error(error.stack, error.status, error.message); + throw new Error(error); + } +}; + +const getAccountBalance = async (username) => { + try { + + return { success: true, message: charge }; + } catch (error) { + debug.error(error.stack, error.status, error.message); + throw new Error(error); + } +}; + +const payInvoice = async (req, res) => { + 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); + } catch (error) { + debug.error(error.stack); + res.status(500).json({ message: error.message, error: error.stack }); + } +}; + +const createInvoice = async (req, res) => { + 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); + + } catch (error) { + debug.error(error.stack); + res.status(500).json({ message: error.message, error: error.stack }); + } +}; + +module.exports = { getAccountAllowance, payInvoice, createInvoice }; diff --git a/backend/api/account/index.js b/backend/api/account/index.js new file mode 100644 index 0000000..32a31c9 --- /dev/null +++ b/backend/api/account/index.js @@ -0,0 +1,13 @@ +const express = require('express'); +const router = express(); + +const { getAccountAllowance, payInvoice, createInvoice } = require('./accountController'); + +const { getBalance } = require('../../sensei/nodes'); + +router.get('/account/balance', getBalance); +router.get('/allowance', getAccountAllowance); +router.get('/payment/send', payInvoice); +router.get('/payment/receive', createInvoice); + +module.exports = router; diff --git a/backend/api/admin/adminController.js b/backend/api/admin/adminController.js new file mode 100644 index 0000000..c463efe --- /dev/null +++ b/backend/api/admin/adminController.js @@ -0,0 +1,54 @@ +const debug = require('../../utils/debug'); +const adminService = require('./adminService'); +const senseiAdmin = require('../../sensei/admin'); +const senseiNodes = require('../../sensei/nodes'); + +const getAllBalances = async (req, res) => { + try { + const response = await adminService.getAllBalances(); + debug.info(`Admin All Balances 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 }); + } +}; + +const addNewAccount = async (req, res) => { + try { + const username = req.body.username; + const passphrase = req.body.passphrase; + // add to DB? + const alias = req.body.alias; + const start = req.body.start; + const response = await senseiAdmin.createNode(username, passphrase, alias, start) + + debug.info(`Add Account / Create Node 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 }); + } +} + +const keysend = async (req, res) => { + try { + const destPubkey = req.body.destPubkey; + const amtMsat = req.body.amtMsat; + const response = await senseiNodes.keysend(destPubkey, amtMsat); + debug.info(`Admin Keysend 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 = { getAllBalances, addNewAccount, keysend }; diff --git a/backend/api/admin/adminService.js b/backend/api/admin/adminService.js new file mode 100644 index 0000000..61896b1 --- /dev/null +++ b/backend/api/admin/adminService.js @@ -0,0 +1,21 @@ +const debug = require('../../utils/debug'); +const accounts = require('../../db/collection'); +const senseiAdmin = require('../../sensei/admin'); +const senseiNodes = require('../../sensei/nodes'); + +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; + nodes.push(node); + } + return { success: true, message: nodes }; + } catch (error) { + debug.error(error.stack); + throw new Error(error); + } +}; + +module.exports = { getAllBalances }; diff --git a/backend/api/admin/index.js b/backend/api/admin/index.js new file mode 100644 index 0000000..1c6b6a1 --- /dev/null +++ b/backend/api/admin/index.js @@ -0,0 +1,10 @@ +const express = require('express'); +const router = express(); + +const { getAllBalances, addNewAccount, keysend } = require('./adminController'); + +router.get('/balances', getAllBalances); +router.post('/add', addNewAccount); +router.post('/transfer', keysend); + +module.exports = router; diff --git a/backend/app.js b/backend/app.js new file mode 100644 index 0000000..224ee54 --- /dev/null +++ b/backend/app.js @@ -0,0 +1,31 @@ +const dotenv = require('dotenv'); +dotenv.config(); + +const PORT = process.env.PORT || 4000; +const cors = require('cors'); +const morgan = require('morgan'); +const express = require('express'); +const app = express(); + +app.use(morgan('dev')); +app.use(express.json({ limit: '50mb' })); +app.use(express.urlencoded({ limit: '50mb', extended: true })); +app.use(cors({ credentials: true, origin: true })); +app.use( + express.text({ + type: () => { + return { text: 'text' }; + }, + }) +); + +app.get('/', async (req, res) => { + res.send(`Health check! Server running on port ${PORT}!`); +}); + +const account = require('./api/account'); +const admin = require('./api/admin'); +app.use('/api/v1/account', account); +app.use('/api/v1/admin', admin); + +module.exports = { app, PORT }; diff --git a/backend/dataModel.json b/backend/dataModel.json new file mode 100644 index 0000000..8d228f9 --- /dev/null +++ b/backend/dataModel.json @@ -0,0 +1,37 @@ +{ + "d8d05dce-dbae-421b-9bdd-ea3ce75b7a77": { + "address": "691 John Wesley Dobbs Ave, Atlanta, GA 30312", + "occupied": true, + "licensePlate": "SC39133", + "start": 1648317620, + "expiration": 1648319420, + "duration": 1800, + "expired": true + }, + "da1c0d1b-1ecf-4fe0-9acd-dc3d49640f8f": { + "address": "79 5th St NW, Atlanta, GA 30308", + "occupied": false + }, + "e74d1791-1ef9-4a5a-b98f-0d7d67e533e1": { + "address": "817 W Peachtree St NE, Atlanta, GA 30308", + "occupied": true, + "licensePlate": "8BQN612", + "startTime": 1648321556, + "expiration": 1649014216, + "duration": 604800, + "expired": false + }, + "4420c88b-a0dd-470c-925c-c713a73b8f17": { + "address": "79 5th St NW, Atlanta, GA 30308", + "licensePlate": "7MSW817", + "occupied": true, + "startTime": 1648317607, + "expiration": 1648324807, + "duration": 7200, + "expired": true + }, + "f1056ff9-e0bd-47a0-adba-5e11670897e7": { + "address": "675 Ponce de Leon Ave NE, Atlanta, GA 30308", + "occupied": false + } +} diff --git a/backend/db/collection.js b/backend/db/collection.js index 1a9d283..2752fde 100644 --- a/backend/db/collection.js +++ b/backend/db/collection.js @@ -1,7 +1,8 @@ -const db = require("./db"); -const DB_COLLECTION = "families"; +const debug = require('../utils/debug'); +const db = require('./db'); +const DB_COLLECTION = 'families'; const collection = db.collection(DB_COLLECTION); -console.log(`Connected to collection ${db.projectId}/${DB_COLLECTION}`); +debug.info(`Connected to collection ${db.projectId}/${DB_COLLECTION}`); -export default collection; +module.exports = collection; \ No newline at end of file diff --git a/backend/db/db.js b/backend/db/db.js index 84aca95..4c714e9 100644 --- a/backend/db/db.js +++ b/backend/db/db.js @@ -1,22 +1,13 @@ -import { initializeApp } from "firebase/app"; -import { getFirestore, collection, getDocs } from "firebase/firestore"; - -const firebaseConfig = require("../service-account.json"); -console.log(firebaseConfig); - -const app = initializeApp(firebaseConfig); - -const db = getFirestore(app); +const utils = require('../utils/debug'); +const admin = require('firebase-admin'); +const GOOGLE_APPLICATION_CREDENTIALS = JSON.parse( + Buffer.from(process.env.GOOGLE_APPLICATION_CREDENTIALS, 'base64').toString( + 'utf-8' + ) +); +admin.initializeApp({ + credential: admin.credential.cert(GOOGLE_APPLICATION_CREDENTIALS), +}); +const db = admin.firestore(); utils.info(`Connection to GCP Project ${db.projectId} successful!`); -export default db; - -export const getFamily = async (familyName) => { - const querySnapshot = await getDocs(collection(db, "families")); - let res = null; - const docs = await querySnapshot.forEach((doc) => { - if (doc.data()["family-name"] === familyName) { - res = doc.data(); - } - }); - return res; -}; +module.exports = db; \ No newline at end of file diff --git a/backend/mailgun/mailgun.js b/backend/mailgun/mailgun.js new file mode 100644 index 0000000..ce984ce --- /dev/null +++ b/backend/mailgun/mailgun.js @@ -0,0 +1,30 @@ +const mailgun = require("mailgun-js"); +const DOMAIN = "sandboxb488ae9cba904e73936d81f35b2ee781.mailgun.org"; +const mg = mailgun({ + apiKey: process.env.MAILGUN_API_KEY, + domain: DOMAIN, +}); + +const sendInvite = async (newUser, email, familyName, inviteLink) => { + const data = { + from: `Family BTC Wallet { + 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, + }, + }); + + 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); + 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, + }, + }); + + 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); + 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); + 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); + 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); + 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); + 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); + 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); + 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); + 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); + return await res.json(); +}; + +module.exports = { + initSensei, + listNodes, + createNode, + startNode, + stopNode, + deleteNode, + nodeStatus, + startSensi, + login, + logout, + getConfig, + updateConfig, +}; diff --git a/backend/sensei/nodes.js b/backend/sensei/nodes.js new file mode 100644 index 0000000..b14c1a0 --- /dev/null +++ b/backend/sensei/nodes.js @@ -0,0 +1,90 @@ +const { apiCall } = require('../utils/apiCall'); + +const getUnusedAddress = async () => { + const { address } = await apiCall('/v1/node/wallet/address', 'GET') + return await address.json(); +} + +const getBalance = async () => { + const balance = await apiCall('/v1/node/wallet/balance', 'GET') + return await balance.json(); +} + +const getChannels = async ({ page, searchTerm, take }) => { + const { channels, pagination } = await apiCall(`/v1/node/channels?page=${page}&take=${take}&query=${searchTerm}`, 'GET') + return await channels.json(); +} + +const getPayments = async (page, take) => { + const { payments } = await apiCall(`/v1/node/payments?page=${page}&take=${take}`, 'GET') + return await payments.json(); +} + +const getInfo = async () => { + const { node_info } = await apiCall(`/v1/node/payments?page=${page}&take=${take}`, 'GET') + + return { + version: node_info.version, + nodePubkey: node_info.node_pubkey, + numChannels: node_info.num_channels, + numUsableChannels: node_info.num_usable_channels, + numPeers: node_info.num_peers, + localBalanceMsat: node_info.local_balance_msat, + }; +} + +const getPeers = async () => { + const { peers } = await apiCall('/v1/node/peers', 'GET') + return await peers.json(); +} + +const stopNode = async () => { + const res = await apiCall('/v1/node/stop', 'GET'); + return res.json(); +} + +const createInvoice = async (amountMillisats, description) => { + const res = await apiCall('/v1/node/invoices', 'POST', { amt_msat: amountMillisats, description }); + return await res.json(); +} + +const payInvoice = async (invoice) => { + const res = await apiCall('/v1/node/invoices/pay', 'POST', { invoice }); + return await res.json(); +} + +<<<<<<< HEAD +const keysend = async (destPubkey, amtMsat) => { + const res = await apiCall('/v1/node/invoices/pay', 'POST', { dest_pubkey: destPubkey, amt_msat: amtMsat }); + return await res.json(); +======= +const keySend = async (destPubkey, amtMsat) => { + const res = await fetch(`${BASE_URL}/v1/node/keysend`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify({ + dest_pubkey: destPubkey, + amt_msat: amtMsat + }) + } + ); +>>>>>>> e295aada2015b250764e2aa5e1db0e8d8a7f6d24 +} + + +module.exports = { + getUnusedAddress, + getBalance, + getChannels, + getPayments, + getInfo, + getPeers, + stopNode, + createInvoice, + payInvoice, + keySend, +} \ No newline at end of file diff --git a/backend/server.js b/backend/server.js new file mode 100644 index 0000000..fecb636 --- /dev/null +++ b/backend/server.js @@ -0,0 +1,8 @@ +require('dotenv').config(); + +const { app, PORT } = require('./app'); +const debug = require('./utils/debug'); + +app.listen(PORT, () => { + debug.info(`API Server listening on http://127.0.0.1:${PORT}`); +}); diff --git a/backend/utils/apiCall.js b/backend/utils/apiCall.js new file mode 100644 index 0000000..e82d622 --- /dev/null +++ b/backend/utils/apiCall.js @@ -0,0 +1,29 @@ +const fetch = require('node-fetch'); +const BASE_URL = process.env.BASE_URL; +const MACAROON = process.env.MACAROON; +const TOKEN = process.env.TOKEN; + +const apiCall = async (path, method, json = null) => { + if (json) { + return await fetch(BASE_URL + path, { + method: method, + headers: { + 'Content-Type': 'application/json', + Cookie: `macaroon=${MACAROON}; token=${TOKEN}`, + }, + credentials: 'include', + body: JSON.stringify(json), + }); + } else { + return await fetch(BASE_URL + path, { + method: method, + headers: { + 'Content-Type': 'application/json', + Cookie: `macaroon=${MACAROON}; token=${TOKEN}`, + }, + credentials: 'include', + }); + } +}; + +module.exports = { apiCall }; diff --git a/backend/utils/debug.js b/backend/utils/debug.js new file mode 100644 index 0000000..251c032 --- /dev/null +++ b/backend/utils/debug.js @@ -0,0 +1,20 @@ +const logger = require('./logger'); + +const info = (i) => { + logger.info(i); +}; + +const verbose = (v) => { + logger.verbose(v); +}; + +const error = (e) => { + logger.error(e); +}; + +module.exports = { + verbose, + info, + error, +}; + diff --git a/backend/utils/logger.js b/backend/utils/logger.js new file mode 100644 index 0000000..b2f2667 --- /dev/null +++ b/backend/utils/logger.js @@ -0,0 +1,19 @@ +const { createLogger, format, transports } = require('winston'); + +module.exports = createLogger({ + transports: [ + new transports.File({ + filename: 'log/server.log', + format: format.combine( + format.timestamp({ format: 'MMM-DD-YYYY HH:mm:ss' }), + format.align(), + format.printf( + (level) => `${level.level}: ${[level.timestamp]}: ${level.message}` + ) + ), + }), + new transports.Console({ + format: format.combine(format.colorize(), format.simple()), + }), + ], +}); \ No newline at end of file diff --git a/sample.env b/sample.env new file mode 100644 index 0000000..3f60363 --- /dev/null +++ b/sample.env @@ -0,0 +1,3 @@ +GOOGLE_APPLICATION_CREDENTIALS= +MAILGUN_API_KEY= +MAILGUN_DOMAIN= \ No newline at end of file