Mayank
4 years ago
commit
9ad14c3409
37 changed files with 4691 additions and 0 deletions
@ -0,0 +1,5 @@ |
|||
{ |
|||
"presets": [ |
|||
"@babel/preset-env" |
|||
] |
|||
} |
@ -0,0 +1,18 @@ |
|||
.dockerignore |
|||
node_modules |
|||
npm-debug.log |
|||
logs |
|||
README.md |
|||
.git |
|||
.gitignore |
|||
.env.default |
|||
.idea |
|||
Dockerfile |
|||
pre-commit |
|||
.eslintrc |
|||
test/ |
|||
.eslintignore |
|||
coverage |
|||
.nyc_output |
|||
*.env |
|||
db/ |
@ -0,0 +1 @@ |
|||
coverage |
@ -0,0 +1,276 @@ |
|||
{ |
|||
"root": true, |
|||
"parser": "babel-eslint", |
|||
"extends": "eslint:recommended", |
|||
"env": { |
|||
"es6": true, |
|||
"node": true |
|||
}, |
|||
"globals": {}, |
|||
"plugins": [], |
|||
"overrides": [ |
|||
{ |
|||
"files": ["*.integration.js","*.spec.js"], |
|||
"rules": { |
|||
"no-magic-numbers": "off" |
|||
} |
|||
}, |
|||
{ |
|||
"files": ["*.js"], |
|||
"rules": { |
|||
"prefer-arrow-callback": 0, |
|||
"no-process-env": 0, |
|||
"no-warning-comments": 0, |
|||
"prefer-template": 0, |
|||
"no-sync": 0 |
|||
} |
|||
} |
|||
], |
|||
"rules": { |
|||
//Possible Errors |
|||
"comma-dangle": 0, //disallow or enforce trailing commas |
|||
"no-cond-assign": 2, //disallow assignment in conditional expressions |
|||
"no-console": 2, //disallow use of console in the node environment |
|||
"no-constant-condition": 1, //disallow use of constant expressions in conditions |
|||
"no-control-regex": 2, //disallow control characters in regular expressions |
|||
"no-debugger": 2, //disallow use of debugger |
|||
"no-dupe-args": 2, //disallow duplicate arguments in functions |
|||
"no-dupe-keys": 2, //disallow duplicate keys when creating object literals |
|||
"no-duplicate-case": 2, //disallow a duplicate case label. |
|||
"no-empty-character-class": 2, //disallow the use of empty character classes in regular expressions |
|||
"no-empty": 2, //disallow empty statements |
|||
"no-ex-assign": 2, //disallow assigning to the exception in a catch block |
|||
"no-extra-boolean-cast": 2, //disallow double-negation boolean casts in a boolean context |
|||
"no-extra-parens": 2, //disallow unnecessary parentheses |
|||
"no-extra-semi": 2, //disallow unnecessary semicolons |
|||
"no-func-assign": 2, //disallow overwriting functions written as function declarations |
|||
"no-inner-declarations": 1, //disallow function or variable declarations in nested blocks |
|||
"no-invalid-regexp": 2, //disallow invalid regular expression strings in the RegExp constructor |
|||
"no-irregular-whitespace": 2, //disallow irregular whitespace outside of strings and comments |
|||
"no-negated-in-lhs": 2, //disallow negation of the left operand of an in expression |
|||
"no-obj-calls": 2, //disallow the use of object properties of the global object (Math and JSON) as functions |
|||
"no-prototype-builtins": 2, //Disallow use of Object.prototypes builtins directly |
|||
"no-regex-spaces": 2, //disallow multiple spaces in a regular expression literal |
|||
"no-sparse-arrays": 1, //disallow sparse arrays |
|||
"no-unexpected-multiline": 2, //Avoid code that looks like two expressions but is actually one |
|||
"no-unreachable": 2, //disallow unreachable statements after a return, throw, continue, or break statement |
|||
"no-unsafe-finally": 2, //disallow control flow statements in finally blocks |
|||
"use-isnan": 2, //disallow comparisons with the value NaN |
|||
"valid-jsdoc": 2, //Ensure JSDoc comments are valid |
|||
"valid-typeof": 2, //Ensure that the results of typeof are compared against a valid string |
|||
|
|||
//Best Practices |
|||
"accessor-pairs": 0, //Enforces getter/setter pairs in objects |
|||
"array-callback-return": 2, //Enforces return statements in callbacks of array"s methods |
|||
"block-scoped-var": 2, //treat var statements as if they were block scoped |
|||
"complexity": 1, //specify the maximum cyclomatic complexity allowed in a program |
|||
"consistent-return": 0, //require return statements to either always or never specify values |
|||
"curly": 2, //specify curly brace conventions for all control statements |
|||
"default-case": 2, //require default case in switch statements |
|||
"dot-location": [2, "property"], //enforces consistent newlines before or after dots |
|||
"dot-notation": 2, //encourages use of dot notation whenever possible |
|||
"eqeqeq": 2, //require the use of === and !== |
|||
"guard-for-in": 2, //make sure for-in loops have an if statement |
|||
"no-alert": 2, //disallow the use of alert, confirm, and prompt |
|||
"no-caller": 2, //disallow use of arguments.caller or arguments.callee |
|||
"no-case-declarations": 0, //disallow lexical declarations in case clauses |
|||
"no-div-regex": 2, //disallow division operators explicitly at beginning of regular expression |
|||
"no-else-return": 0, //disallow else after a return in an if |
|||
"no-empty-function": 2, //disallow use of empty functions |
|||
"no-empty-pattern": 2, //disallow use of empty destructuring patterns |
|||
"no-eq-null": 2, //disallow comparisons to null without a type-checking operator |
|||
"no-eval": 2, //disallow use of eval() |
|||
"no-extend-native": 0, //disallow adding to native types |
|||
"no-extra-bind": 1, //disallow unnecessary function binding |
|||
"no-extra-label": 2, //disallow unnecessary labels |
|||
"no-fallthrough": 2, //disallow fallthrough of case statements |
|||
"no-floating-decimal": 2, //disallow the use of leading or trailing decimal points in numeric literals |
|||
"no-implicit-coercion": 0, //disallow the type conversions with shorter notations |
|||
"no-implicit-globals": 0, //disallow var and named functions in global scope |
|||
"no-implied-eval": 2, //disallow use of eval()-like methods |
|||
"no-invalid-this": 2, //disallow this keywords outside of classes or class-like objects |
|||
"no-iterator": 2, //disallow usage of __iterator__ property |
|||
"no-labels": 2, //disallow use of labeled statements |
|||
"no-lone-blocks": 2, //disallow unnecessary nested blocks |
|||
"no-loop-func": 2, //disallow creation of functions within loops |
|||
"no-magic-numbers": [2, { "ignore": [0,1,-1] }], //disallow the use of magic numbers other than 0 or 1 |
|||
"no-multi-spaces": 2, //disallow use of multiple spaces |
|||
"no-multi-str": 0, //disallow use of multiline strings |
|||
"no-native-reassign": 2, //disallow reassignments of native objects |
|||
"no-new-func": 1, //disallow use of new operator for Function object |
|||
"no-new-wrappers": 2, //disallows creating new instances of String,Number, and Boolean |
|||
"no-new": 2, //disallow use of the new operator when not part of an assignment or comparison |
|||
"no-octal-escape": 0, //disallow use of octal escape sequences in string literals, such as var foo = "Copyright \251"; |
|||
"no-octal": 0, //disallow use of octal literals |
|||
"no-param-reassign": 2, //disallow reassignment of function parameters |
|||
"no-process-env": 2, //disallow use of process.env |
|||
"no-proto": 2, //disallow usage of __proto__ property |
|||
"no-redeclare": 2, //disallow declaring the same variable more than once |
|||
"no-return-assign": 2, //disallow use of assignment in return statement |
|||
"no-script-url": 2, //disallow use of javascript: urls. |
|||
"no-self-assign": 2, //disallow assignments where both sides are exactly the same |
|||
"no-self-compare": 2, //disallow comparisons where both sides are exactly the same |
|||
"no-sequences": 2, //disallow use of the comma operator |
|||
"no-throw-literal": 0, //restrict what can be thrown as an exception |
|||
"no-unmodified-loop-condition": 2, //disallow unmodified conditions of loops |
|||
"no-unused-expressions": 0, //disallow usage of expressions in statement position |
|||
"no-unused-labels": 2, //disallow unused labels |
|||
"no-useless-call": 2, //disallow unnecessary .call() and .apply() |
|||
"no-useless-concat": 2, //disallow unnecessary concatenation of literals or template literals |
|||
"no-useless-escape": 2, //disallow unnecessary escape characters |
|||
"no-void": 0, //disallow use of the void operator |
|||
"no-warning-comments": 1, //disallow usage of configurable warning terms in comments (e.g. TODO or FIXME) |
|||
"no-with": 2, //disallow use of the with statement |
|||
"radix": 2, //require use of the second argument for parseInt() |
|||
"vars-on-top": 0, //require declaration of all vars at the top of their containing scope |
|||
"wrap-iife": 2, //require immediate function invocation to be wrapped in parentheses |
|||
"yoda": 2, //require or disallow Yoda conditions |
|||
|
|||
//Strict Mode |
|||
"strict": 0, //controls location of Use Strict Directives |
|||
|
|||
//Variables |
|||
"init-declarations": 0, //enforce or disallow variable initializations at definition |
|||
"no-catch-shadow": 2, //disallow the catch clause parameter name being the same as a variable in the outer scope |
|||
"no-delete-var": 2, //disallow deletion of variables |
|||
"no-label-var": 2, //disallow labels that share a name with a variable |
|||
"no-restricted-globals": 0, //restrict usage of specified global variables |
|||
"no-shadow-restricted-names": 2, //disallow shadowing of names such as arguments |
|||
"no-shadow": [2, {"allow": ["err"]}], //disallow declaration of variables already declared in the outer scope |
|||
"no-undef-init": 2, //disallow use of undefined when initializing variables |
|||
"no-undef": 2, //disallow use of undeclared variables unless mentioned in a /*global */ block |
|||
"no-undefined": 0, //disallow use of undefined variable |
|||
"no-unused-vars": 2, //disallow declaration of variables that are not used in the code |
|||
"no-use-before-define": [2, { "functions": false }], //disallow use of variables before they are defined |
|||
|
|||
//Node.js and CommonJS |
|||
"callback-return": 2, //enforce return after a callback |
|||
"global-require": 0, //enforce require() on top-level module scope |
|||
"handle-callback-err": 2, //enforce error handling in callbacks |
|||
"no-mixed-requires": 2, //disallow mixing regular variable and require declarations |
|||
"no-new-require": 2, //disallow use of new operator with the require function |
|||
"no-path-concat": 2, //disallow string concatenation with __dirname and __filename |
|||
"no-process-exit": 2, //disallow process.exit() |
|||
"no-restricted-imports": 0, //restrict usage of specified node imports |
|||
"no-restricted-modules": 0, //restrict usage of specified node modules |
|||
"no-sync": 1, //disallow use of synchronous methods |
|||
|
|||
//Stylistic Issues |
|||
"array-bracket-spacing": [2, "never"], //enforce spacing inside array brackets |
|||
"block-spacing": 0, //disallow or enforce spaces inside of single line blocks |
|||
"brace-style": 2, //enforce one true brace style |
|||
"camelcase": 1, //require camel case names |
|||
"comma-spacing": [2, {"before": false, "after": true}], //enforce spacing before and after comma |
|||
"comma-style": 2, //enforce one true comma style |
|||
"computed-property-spacing": 2, //require or disallow padding inside computed properties |
|||
"consistent-this": 2, //enforce consistent naming when capturing the current execution context |
|||
"eol-last": 2, //enforce newline at the end of file, with no multiple empty lines |
|||
"func-names": 0, //require function expressions to have a name |
|||
"func-style": 0, //enforce use of function declarations or expressions |
|||
"id-blacklist": 0, //blacklist certain identifiers to prevent them being used |
|||
"id-length": [2, { //this option enforces minimum and maximum identifier lengths (variable names, property names etc.) |
|||
"min": 2, |
|||
"max": 25, |
|||
"exceptions": ["_"] |
|||
}], |
|||
"id-match": 0, //require identifiers to match the provided regular expression |
|||
"indent": ["error", 2], //specify tab or space width for your code |
|||
"jsx-quotes": 0, //specify whether double or single quotes should be used in JSX attributes |
|||
"key-spacing": 2, //enforce spacing between keys and values in object literal properties |
|||
"keyword-spacing": [2, { |
|||
"before": true, |
|||
"after": true |
|||
}], //enforce spacing before and after keywords |
|||
"linebreak-style": 2, //disallow mixed "LF" and "CRLF" as linebreaks |
|||
"lines-around-comment": ["error", { |
|||
"beforeLineComment": true, |
|||
"allowBlockStart": true |
|||
} |
|||
], //enforce empty lines around comments |
|||
"max-depth": 1, //specify the maximum depth that blocks can be nested |
|||
"max-len": [1, 120], //specify the maximum length of a line in your program |
|||
"max-lines": [1, 500], //enforce a maximum file length |
|||
"max-nested-callbacks": 2, //specify the maximum depth callbacks can be nested |
|||
"max-params": [1, 5], //limits the number of parameters that can be used in the function declaration. |
|||
"max-statements": [1, 50], //specify the maximum number of statement allowed in a function |
|||
"max-statements-per-line": 1, //enforce a maximum number of statements allowed per line |
|||
"new-cap": 0, //require a capital letter for constructors |
|||
"new-parens": 2, //disallow the omission of parentheses when invoking a constructor with no arguments |
|||
"newline-after-var": 0, //require or disallow an empty newline after variable declarations |
|||
"newline-before-return": 2, //require newline before return statement |
|||
"newline-per-chained-call": 0, //enforce newline after each call when chaining the calls |
|||
"no-array-constructor": 2, //disallow use of the Array constructor |
|||
"no-bitwise": 0, //disallow use of bitwise operators |
|||
"no-continue": 0, //disallow use of the continue statement |
|||
"no-inline-comments": 0, //disallow comments inline after code |
|||
"no-lonely-if": 2, //disallow if as the only statement in an else block |
|||
"no-mixed-operators": 0, //disallow mixes of different operators |
|||
"no-mixed-spaces-and-tabs": 2, //disallow mixed spaces and tabs for indentation |
|||
"no-multiple-empty-lines": 2, //disallow multiple empty lines |
|||
"no-negated-condition": 0, //disallow negated conditions |
|||
"no-nested-ternary": 2, //disallow nested ternary expressions |
|||
"no-new-object": 2, //disallow the use of the Object constructor |
|||
"no-plusplus": 0, //disallow use of unary operators, ++ and -- |
|||
"no-restricted-syntax": 0, //disallow use of certain syntax in code |
|||
"no-spaced-func": 2, //disallow space between function identifier and application |
|||
"no-ternary": 0, //disallow the use of ternary operators |
|||
"no-trailing-spaces": 2, //disallow trailing whitespace at the end of lines |
|||
"no-underscore-dangle": 0, //disallow dangling underscores in identifiers |
|||
"no-unneeded-ternary": 2, //disallow the use of ternary operators when a simpler alternative exists |
|||
"no-whitespace-before-property": 2, //disallow whitespace before properties |
|||
"object-curly-newline": 2, //enforce consistent line breaks inside braces |
|||
"object-curly-spacing": 2, //require or disallow padding inside curly braces |
|||
"object-property-newline": [2, { //enforce placing object properties on either one line or all on separate lines |
|||
"allowAllPropertiesOnSameLine": true |
|||
} |
|||
], |
|||
"one-var": [2, "never"], //require or disallow one variable declaration per function |
|||
"one-var-declaration-per-line": 2, //require or disallow an newline around variable declarations |
|||
"operator-assignment": 0, //require assignment operator shorthand where possible or prohibit it entirely |
|||
"operator-linebreak": [1, "before"], //enforce operators to be placed before or after line breaks |
|||
"padded-blocks": 0, //enforce padding within blocks |
|||
"quote-props": [2, "as-needed"], //require quotes around object literal property names |
|||
"quotes": [2, "single"], //specify whether backticks, double or single quotes should be used |
|||
"require-jsdoc": 0, //Require JSDoc comment |
|||
"semi-spacing": 2, //enforce spacing before and after semicolons |
|||
"sort-imports": 0, //sort import declarations within module |
|||
"semi": 2, //require or disallow use of semicolons instead of ASI |
|||
"sort-vars": 0, //sort variables within the same declaration block |
|||
"space-before-blocks": 2, //require or disallow a space before blocks |
|||
"space-before-function-paren": [2, "never"], //require or disallow a space before function opening parenthesis |
|||
"space-in-parens": 2, //require or disallow spaces inside parentheses |
|||
"space-infix-ops": 2, //require spaces around operators |
|||
"space-unary-ops": 2, //require or disallow spaces before/after unary operators |
|||
"spaced-comment": [2, "always", { "exceptions": ["*"] }], //require or disallow a space immediately following the // or /* in a comment |
|||
"unicode-bom": 0, //require or disallow the Unicode BOM |
|||
"wrap-regex": 0, //require regex literals to be wrapped in parentheses |
|||
|
|||
//ECMAScript 6 |
|||
"arrow-body-style": [2, "as-needed"], //require braces in arrow function body |
|||
"arrow-parens": [2, "as-needed"], //require parens in arrow function arguments |
|||
"arrow-spacing": 2, //require space before/after arrow function"s arrow |
|||
"constructor-super": 2, //verify calls of super() in constructors |
|||
"generator-star-spacing": 0, //enforce spacing around the * in generator functions |
|||
"no-class-assign": 2, //disallow modifying variables of class declarations |
|||
"no-confusing-arrow": 2, //disallow arrow functions where they could be confused with comparisons |
|||
"no-const-assign": 2, //disallow modifying variables that are declared using const |
|||
"no-dupe-class-members": 2, //disallow duplicate name in class members |
|||
"no-duplicate-imports": 2, //disallow duplicate module imports |
|||
"no-new-symbol": 2, //disallow use of the new operator with the Symbol object |
|||
"no-this-before-super": 2, //disallow use of this/super before calling super() in constructors. |
|||
"no-useless-computed-key": 2, //disallow unnecessary computed property keys in object literals |
|||
"no-useless-constructor": 2, //disallow unnecessary constructor |
|||
"no-useless-rename": 2, //disallow renaming import, export, and destructured assignments to the same name |
|||
"no-var": 0, //require let or const instead of var |
|||
"object-shorthand": 1, //require method and property shorthand syntax for object literals |
|||
"prefer-arrow-callback": 1, //suggest using arrow functions as callbacks |
|||
"prefer-const": 1, //suggest using const declaration for variables that are never modified after declared |
|||
"prefer-rest-params": 1, //suggest using the rest parameters instead of arguments |
|||
"prefer-spread": 1, //suggest using the spread operator instead of .apply(). |
|||
"prefer-template": 1, //suggest using template literals instead of strings concatenation |
|||
"require-yield": 0, //disallow generator functions that do not have yield |
|||
"rest-spread-spacing": ["error", "never"], //enforce spacing between rest and spread operators and their expressions |
|||
"template-curly-spacing": 2, //enforce spacing around embedded expressions of template strings |
|||
"yield-star-spacing": [2, "after"] //enforce spacing around the * in yield* expressions |
|||
} |
|||
} |
@ -0,0 +1,22 @@ |
|||
--- |
|||
name: Feature request |
|||
about: Suggest an idea for this project |
|||
title: '' |
|||
labels: '' |
|||
assignees: '' |
|||
|
|||
--- |
|||
|
|||
**Issue** |
|||
A clear and concise description of what the problem is. |
|||
|
|||
**Proposed Solution** |
|||
A clear and concise description of what you want to happen. |
|||
|
|||
**Dependencies** |
|||
1. Links to PRs |
|||
2. Links to Issues |
|||
3. Description of other dependencies |
|||
|
|||
**Additional context** |
|||
Add any other context or screenshots about the feature request here. |
@ -0,0 +1,54 @@ |
|||
name: Automatically Build image on tag |
|||
env: |
|||
DOCKER_CLI_EXPERIMENTAL: enabled |
|||
TAG_FMT: '^refs/tags/(((.?[0-9]+){3,4}))$' |
|||
|
|||
on: |
|||
push: |
|||
tags: [ '*' ] |
|||
|
|||
jobs: |
|||
build: |
|||
runs-on: ubuntu-18.04 |
|||
name: Build / Push Umbrel Manager on version tag |
|||
steps: |
|||
- name: Setup Environment |
|||
run: | |
|||
if ! echo "$GITHUB_REF" | grep -qE "$TAG_FMT"; then |
|||
echo "ERR: TAG must be in format: vX.Y.Z or X.Y.Z or vW.X.Y.Z or W.X.Y.Z" |
|||
exit 1 |
|||
fi |
|||
VERSION="$(echo "$GITHUB_REF" | sed -E "s|$TAG_FMT|\2|")" |
|||
|
|||
TAG="$(echo "$GITHUB_REF" | sed -E "s|$TAG_FMT|\1|")" |
|||
echo ::set-env name=TAG::"$TAG" |
|||
|
|||
- name: Show set environment variables |
|||
run: | |
|||
printf " TAG: %s\n" "$TAG" |
|||
- name: Login to Docker for building |
|||
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin |
|||
- name: Checkout project |
|||
uses: actions/checkout@v2 |
|||
- name: Setup Docker buildx Action |
|||
uses: crazy-max/ghaction-docker-buildx@v1 |
|||
id: buildx |
|||
with: |
|||
buildx-version: latest |
|||
qemu-version: latest |
|||
- name: Available platforms |
|||
run: echo ${{ steps.buildx.outputs.platforms }} |
|||
- name: Run Docker build X (against tag) |
|||
run: | |
|||
docker buildx build \ |
|||
--platform linux/amd64,linux/386,linux/arm/v7,linux/arm64 \ |
|||
-t getumbrel/manager:$TAG \ |
|||
--output "type=registry" \ |
|||
. |
|||
- name: Run Docker build X (against latest) |
|||
run: | |
|||
docker buildx build \ |
|||
--platform linux/amd64,linux/386,linux/arm/v7,linux/arm64 \ |
|||
-t getumbrel/manager:latest \ |
|||
--output "type=registry" \ |
|||
. |
@ -0,0 +1,14 @@ |
|||
node_modules/ |
|||
npm-debug.log |
|||
/.idea/ |
|||
/tls.cert |
|||
*.log |
|||
*.env |
|||
logs/ |
|||
package-lock.json |
|||
yarn.lock |
|||
*.bak |
|||
lb_settings.json |
|||
.nyc_output |
|||
coverage |
|||
db/ |
@ -0,0 +1,28 @@ |
|||
# specify the node base image with your desired version node:<version> |
|||
FROM node:8-slim |
|||
|
|||
# install tools |
|||
RUN apt-get update --no-install-recommends \ |
|||
&& apt-get install -y --no-install-recommends vim \ |
|||
&& apt-get install -y --no-install-recommends rsync \ |
|||
&& rm -rf /var/lib/apt/lists/* |
|||
|
|||
# Create app directory |
|||
WORKDIR /usr/src/app |
|||
|
|||
# Install app dependencies |
|||
# A wildcard is used to ensure both package.json AND package-lock.json are copied |
|||
# where available (npm@5+) |
|||
COPY package*.json ./ |
|||
|
|||
RUN npm install |
|||
# If you are building your code for production |
|||
# RUN npm install --only=production |
|||
|
|||
# Bundle app source |
|||
COPY . . |
|||
|
|||
RUN mkdir -p /root/.lnd |
|||
|
|||
EXPOSE 3005 |
|||
CMD [ "npm", "start" ] |
@ -0,0 +1,21 @@ |
|||
The MIT License |
|||
|
|||
Copyright (c) 2020 Umbrel. https://getumbrel.com/ |
|||
|
|||
Permission is hereby granted, free of charge, to any person obtaining a copy |
|||
of this software and associated documentation files (the "Software"), to deal |
|||
in the Software without restriction, including without limitation the rights |
|||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|||
copies of the Software, and to permit persons to whom the Software is |
|||
furnished to do so, subject to the following conditions: |
|||
|
|||
The above copyright notice and this permission notice shall be included in |
|||
all copies or substantial portions of the Software. |
|||
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
|||
THE SOFTWARE. |
@ -0,0 +1,3 @@ |
|||
# Umbrel Manager API |
|||
|
|||
⚙️ Work in progress. README will be updated with some documentation very soon! |
@ -0,0 +1,51 @@ |
|||
require('module-alias/register'); |
|||
require('module-alias').addPath('.'); |
|||
require('dotenv').config(); |
|||
|
|||
const express = require('express'); |
|||
const path = require('path'); |
|||
const morgan = require('morgan'); |
|||
const bodyParser = require('body-parser'); |
|||
const passport = require('passport'); |
|||
const cors = require('cors'); |
|||
|
|||
// Keep requestCorrelationId middleware as the first middleware. Otherwise we risk losing logs.
|
|||
const requestCorrelationMiddleware = require('middlewares/requestCorrelationId.js'); // eslint-disable-line id-length
|
|||
const camelCaseReqMiddleware = require('middlewares/camelCaseRequest.js').camelCaseRequest; |
|||
const onionOriginMiddleware = require('middlewares/onionOrigin.js'); |
|||
const corsOptions = require('middlewares/cors.js').corsOptions; |
|||
const errorHandleMiddleware = require('middlewares/errorHandling.js'); |
|||
require('middlewares/auth.js'); |
|||
|
|||
const logger = require('utils/logger.js'); |
|||
|
|||
const ping = require('routes/ping.js'); |
|||
const account = require('routes/v1/account.js'); |
|||
|
|||
const app = express(); |
|||
|
|||
// Handle Cors for Tor Browser 9.0.0 bug and options requests
|
|||
app.use(onionOriginMiddleware); |
|||
|
|||
// Handles Cors for normal requests
|
|||
app.use(cors(corsOptions)); |
|||
|
|||
app.use(bodyParser.json()); |
|||
app.use(bodyParser.urlencoded({ extended: true })); |
|||
app.use(express.static(path.join(__dirname, 'public'))); |
|||
app.use(passport.initialize()); |
|||
app.use(passport.session()); |
|||
|
|||
app.use(requestCorrelationMiddleware); |
|||
app.use(camelCaseReqMiddleware); |
|||
app.use(morgan(logger.morganConfiguration)); |
|||
|
|||
app.use('/ping', ping); |
|||
app.use('/v1/account', account); |
|||
|
|||
app.use(errorHandleMiddleware); |
|||
app.use((req, res) => { |
|||
res.status(404).json(); // eslint-disable-line no-magic-numbers
|
|||
}); |
|||
|
|||
module.exports = app; |
@ -0,0 +1,91 @@ |
|||
#!/usr/bin/env node |
|||
|
|||
/** |
|||
* Module dependencies. |
|||
*/ |
|||
|
|||
var app = require('../app'); |
|||
var debug = require('debug')('nodejs-regular-webapp2:server'); |
|||
var http = require('http'); |
|||
|
|||
/** |
|||
* Get port from environment and store in Express. |
|||
*/ |
|||
|
|||
var port = normalizePort(process.env.PORT || '3005'); |
|||
app.set('port', port); |
|||
|
|||
/** |
|||
* Create HTTP server. |
|||
*/ |
|||
|
|||
var server = http.createServer(app); |
|||
|
|||
/** |
|||
* Listen on provided port, on all network interfaces. |
|||
*/ |
|||
|
|||
server.listen(port); |
|||
server.on('error', onError); |
|||
server.on('listening', onListening); |
|||
|
|||
/** |
|||
* Normalize a port into a number, string, or false. |
|||
*/ |
|||
|
|||
function normalizePort(val) { |
|||
var port = parseInt(val, 10); |
|||
|
|||
if (isNaN(port)) { |
|||
// named pipe |
|||
return val; |
|||
} |
|||
|
|||
if (port >= 0) { |
|||
// port number |
|||
return port; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/** |
|||
* Event listener for HTTP server "error" event. |
|||
*/ |
|||
|
|||
function onError(error) { |
|||
if (error.syscall !== 'listen') { |
|||
throw error; |
|||
} |
|||
|
|||
var bind = typeof port === 'string' |
|||
? 'Pipe ' + port |
|||
: 'Port ' + port; |
|||
|
|||
// handle specific listen errors with friendly messages |
|||
switch (error.code) { |
|||
case 'EACCES': |
|||
console.error(bind + ' requires elevated privileges'); |
|||
process.exit(1); |
|||
break; |
|||
case 'EADDRINUSE': |
|||
console.error(bind + ' is already in use'); |
|||
process.exit(1); |
|||
break; |
|||
default: |
|||
throw error; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Event listener for HTTP server "listening" event. |
|||
*/ |
|||
|
|||
function onListening() { |
|||
var addr = server.address(); |
|||
var bind = typeof addr === 'string' |
|||
? 'pipe ' + addr |
|||
: 'port ' + addr.port; |
|||
debug('Listening on ' + bind); |
|||
console.log('Listening on ' + bind); |
|||
} |
@ -0,0 +1,224 @@ |
|||
const bcrypt = require('bcrypt'); |
|||
const iocane = require("iocane"); |
|||
const diskLogic = require('logic/disk.js'); |
|||
// const dockerComposeLogic = require('logic/docker-compose.js');
|
|||
// const lnapiService = require('services/lnapi.js');
|
|||
const bashService = require('services/bash.js'); |
|||
const NodeError = require('models/errors.js').NodeError; |
|||
const JWTHelper = require('utils/jwt.js'); |
|||
const constants = require('utils/const.js'); |
|||
const UUID = require('utils/UUID.js'); |
|||
|
|||
const saltRounds = 10; |
|||
const SYSTEM_USER = UUID.fetchBootUUID() || 'admin'; |
|||
|
|||
let devicePassword = ''; |
|||
let changePasswordStatus; |
|||
|
|||
resetChangePasswordStatus(); |
|||
|
|||
function resetChangePasswordStatus() { |
|||
changePasswordStatus = { percent: 0 }; |
|||
} |
|||
|
|||
async function sleepSeconds(seconds) { |
|||
return new Promise(resolve => { |
|||
setTimeout(resolve, seconds * constants.TIME.ONE_SECOND_IN_MILLIS); |
|||
}); |
|||
} |
|||
|
|||
// Caches the password.
|
|||
function cachePassword(password) { |
|||
devicePassword = password; |
|||
} |
|||
|
|||
// Gets the cached the password.
|
|||
function getCachedPassword() { |
|||
return devicePassword; |
|||
} |
|||
|
|||
// Change the device and lnd password.
|
|||
async function changePassword(currentPassword, newPassword, jwt) { |
|||
|
|||
// restart lnd
|
|||
resetChangePasswordStatus(); |
|||
changePasswordStatus.percent = 1; // eslint-disable-line no-magic-numbers
|
|||
// await dockerComposeLogic.dockerComposeStop({ service: constants.SERVICES.LND });
|
|||
changePasswordStatus.percent = 40; // eslint-disable-line no-magic-numbers
|
|||
// await dockerComposeLogic.dockerComposeUpSingleService({ service: constants.SERVICES.LND });
|
|||
|
|||
let complete = false; |
|||
let attempt = 0; |
|||
const MAX_ATTEMPTS = 20; |
|||
|
|||
do { |
|||
try { |
|||
attempt++; |
|||
|
|||
// call lnapi to change password
|
|||
changePasswordStatus.percent = 60 + attempt; // eslint-disable-line no-magic-numbers
|
|||
// await lnapiService.changePassword(currentPassword, newPassword, jwt);
|
|||
|
|||
// make new password file
|
|||
const credentials = hashCredentials(SYSTEM_USER, newPassword); |
|||
|
|||
// replace user file
|
|||
await diskLogic.deleteUserFile(); |
|||
await diskLogic.writeUserFile({ password: credentials.password }); |
|||
|
|||
// update ssh password
|
|||
await hashAccountPassword(newPassword); |
|||
|
|||
complete = true; |
|||
|
|||
// cache the password for later use
|
|||
cachePassword(newPassword); |
|||
|
|||
changePasswordStatus.percent = 100; |
|||
} catch (error) { |
|||
|
|||
// wait for lnd to boot up
|
|||
if (error.response.status === constants.STATUS_CODES.BAD_GATEWAY) { |
|||
await sleepSeconds(1); |
|||
|
|||
// user supplied incorrect credentials
|
|||
} else if (error.response.status === constants.STATUS_CODES.FORBIDDEN) { |
|||
changePasswordStatus.forbidden = true; |
|||
|
|||
// unknown error occurred
|
|||
} else { |
|||
changePasswordStatus.error = true; |
|||
changePasswordStatus.percent = 100; |
|||
|
|||
throw error; |
|||
} |
|||
} |
|||
} while (!complete && attempt < MAX_ATTEMPTS && !changePasswordStatus.unauthorized && !changePasswordStatus.error); |
|||
|
|||
if (!complete && attempt === MAX_ATTEMPTS) { |
|||
changePasswordStatus.error = true; |
|||
changePasswordStatus.percent = 100; |
|||
|
|||
throw new Error('Unable to change password. Lnd would not restart properly.'); |
|||
} |
|||
|
|||
} |
|||
|
|||
function getChangePasswordStatus() { |
|||
return changePasswordStatus; |
|||
} |
|||
|
|||
// Returns an object with the hashed credentials inside.
|
|||
function hashCredentials(username, password) { |
|||
const hash = bcrypt.hashSync(password, saltRounds); |
|||
|
|||
return { password: hash, username, plainTextPassword: password }; |
|||
} |
|||
|
|||
// Returns true if the user is registered otherwise false.
|
|||
async function isRegistered() { |
|||
try { |
|||
await diskLogic.readUserFile(); |
|||
|
|||
return { registered: true }; |
|||
} catch (error) { |
|||
return { registered: false }; |
|||
} |
|||
} |
|||
|
|||
// Log the user into the device. Caches the password if login is successful. Then returns jwt.
|
|||
async function login(user) { |
|||
try { |
|||
const jwt = await JWTHelper.generateJWT(user.username); |
|||
|
|||
// Cache plain text password
|
|||
// cachePassword(user.plainTextPassword);
|
|||
cachePassword(user.password); |
|||
|
|||
return { jwt: jwt }; |
|||
|
|||
} catch (error) { |
|||
console.log(error); |
|||
throw new NodeError('Unable to generate JWT'); |
|||
} |
|||
} |
|||
|
|||
async function seed(user) { |
|||
|
|||
//Decrypt mnemonic seed
|
|||
try { |
|||
const { seed } = await diskLogic.readUserFile(); |
|||
|
|||
const decryptedSeed = await iocane.createSession().decrypt(seed, user.plainTextPassword); |
|||
|
|||
return { seed: decryptedSeed.split(",") }; |
|||
|
|||
} catch (error) { |
|||
throw new NodeError('Unable to decrypt mnemonic seed'); |
|||
} |
|||
} |
|||
|
|||
// Registers the the user to the device. Returns an error if a user already exists.
|
|||
async function register(user, seed) { |
|||
if ((await isRegistered()).registered) { |
|||
throw new NodeError('User already exists', 400); // eslint-disable-line no-magic-numbers
|
|||
} |
|||
|
|||
//Encrypt mnemonic seed for storage
|
|||
let encryptedSeed; |
|||
try { |
|||
encryptedSeed = await iocane.createSession().encrypt(seed.join(), user.plainTextPassword); |
|||
} catch (error) { |
|||
throw new NodeError('Unable to encrypt mnemonic seed'); |
|||
} |
|||
|
|||
//save user
|
|||
try { |
|||
await diskLogic.writeUserFile({ name: user.name, password: user.password, seed: encryptedSeed }); |
|||
} catch (error) { |
|||
throw new NodeError('Unable to register user'); |
|||
} |
|||
|
|||
//generate JWt
|
|||
let jwt; |
|||
try { |
|||
jwt = await JWTHelper.generateJWT(user.username); |
|||
} catch (error) { |
|||
throw new NodeError('Unable to generate JWT'); |
|||
} |
|||
|
|||
//initialize lnd wallet
|
|||
try { |
|||
// await lnapiService.initializeWallet(user.plainTextPassword, seed, jwt);
|
|||
} catch (error) { |
|||
await diskLogic.deleteUserFile(); |
|||
throw new NodeError(error.response.data); |
|||
} |
|||
|
|||
//return token
|
|||
return { jwt: jwt }; |
|||
} |
|||
|
|||
// Generate and return a new jwt token.
|
|||
async function refresh(user) { |
|||
try { |
|||
const jwt = await JWTHelper.generateJWT(user.username); |
|||
|
|||
return { jwt: jwt }; |
|||
} catch (error) { |
|||
throw new NodeError('Unable to generate JWT'); |
|||
} |
|||
} |
|||
|
|||
|
|||
module.exports = { |
|||
changePassword, |
|||
getCachedPassword, |
|||
getChangePasswordStatus, |
|||
hashCredentials, |
|||
isRegistered, |
|||
seed, |
|||
login, |
|||
register, |
|||
refresh, |
|||
}; |
@ -0,0 +1,175 @@ |
|||
const constants = require('utils/const.js'); |
|||
const diskService = require('services/disk.js'); |
|||
|
|||
async function deleteUserFile() { |
|||
return await diskService.deleteFile(constants.USER_PASSWORD_FILE); |
|||
} |
|||
|
|||
async function deleteItemsInDir(directory) { |
|||
return await diskService.deleteItemsInDir(directory); |
|||
} |
|||
|
|||
async function deleteFoldersInDir(directory) { |
|||
await diskService.deleteFoldersInDir(directory); |
|||
} |
|||
|
|||
async function fileExists(path) { |
|||
return diskService.readJsonFile(path) |
|||
.then(() => Promise.resolve(true)) |
|||
.catch(() => Promise.resolve(false)); |
|||
} |
|||
|
|||
async function getBuildDetails(appsToLaunch) { |
|||
|
|||
const details = []; |
|||
|
|||
for (const applicationName of Object.keys(appsToLaunch)) { |
|||
const application = {}; |
|||
application.name = applicationName; |
|||
const path = constants.WORKING_DIRECTORY + '/' + application.name + '/' |
|||
+ appsToLaunch[application.name].version + '/'; |
|||
application.ymlPath = path + application.name + '.yml'; |
|||
application.digestsPath = path + 'digests.json'; |
|||
details.push(application); |
|||
} |
|||
|
|||
return details; |
|||
} |
|||
|
|||
async function listVersionsForApp(app) { |
|||
return await diskService.listDirsInDir(constants.WORKING_DIRECTORY + '/' + app); |
|||
} |
|||
|
|||
async function moveFoldersToDir(fromDir, toDir) { |
|||
await diskService.moveFoldersToDir(fromDir, toDir); |
|||
} |
|||
|
|||
async function writeAppVersionFile(application, json) { |
|||
return diskService.writeJsonFile(constants.WORKING_DIRECTORY + '/' + application, json); |
|||
} |
|||
|
|||
function readUserFile() { |
|||
return diskService.readJsonFile(constants.USER_PASSWORD_FILE); |
|||
} |
|||
|
|||
function readSettingsFile() { |
|||
return diskService.readJsonFile(constants.SETTINGS_FILE); |
|||
} |
|||
|
|||
function writeSettingsFile(json) { |
|||
return diskService.writeJsonFile(constants.SETTINGS_FILE, json); |
|||
} |
|||
|
|||
async function writeUserFile(json) { |
|||
return diskService.writeJsonFile(constants.USER_PASSWORD_FILE, json); |
|||
} |
|||
|
|||
function settingsFileExists() { |
|||
return diskService.readJsonFile(constants.SETTINGS_FILE) |
|||
.then(() => Promise.resolve(true)) |
|||
.catch(() => Promise.resolve(false)); |
|||
} |
|||
|
|||
function hiddenServiceFileExists() { |
|||
return readHiddenService() |
|||
.then(() => Promise.resolve(true)) |
|||
.catch(() => Promise.resolve(false)); |
|||
} |
|||
|
|||
async function readAppVersionFile(application) { |
|||
return diskService.readJsonFile(constants.WORKING_DIRECTORY + '/' + application); |
|||
} |
|||
|
|||
function readHiddenService() { |
|||
return diskService.readFile(constants.CASA_NODE_HIDDEN_SERVICE_FILE); |
|||
} |
|||
|
|||
function readJWTPrivateKeyFile() { |
|||
return diskService.readFile(constants.JWT_PRIVATE_KEY_FILE); |
|||
} |
|||
|
|||
function readJWTPublicKeyFile() { |
|||
return diskService.readFile(constants.JWT_PUBLIC_KEY_FILE); |
|||
} |
|||
|
|||
function writeJWTPrivateKeyFile(data) { |
|||
return diskService.writeKeyFile(constants.JWT_PRIVATE_KEY_FILE, data); |
|||
} |
|||
|
|||
function writeJWTPublicKeyFile(data) { |
|||
return diskService.writeKeyFile(constants.JWT_PUBLIC_KEY_FILE, data); |
|||
} |
|||
|
|||
// Send a signal to shutdown the Casa Node.
|
|||
async function shutdown() { |
|||
await diskService.writeFile(constants.SHUTDOWN_SIGNAL_FILE, 'true'); |
|||
} |
|||
|
|||
// Send a signal to relaunch the manager.
|
|||
async function relaunch() { |
|||
await diskService.writeFile(constants.RELAUNCH_SIGNAL_FILE, 'true'); |
|||
} |
|||
|
|||
// Read the contends of a file.
|
|||
async function readUtf8File(path) { |
|||
return await diskService.readUtf8File(path); |
|||
} |
|||
|
|||
// Read the contents of a file and return a json object.
|
|||
async function readJsonFile(path) { |
|||
return await diskService.readJsonFile(path); |
|||
} |
|||
|
|||
// Send a signal to perform a migration.
|
|||
async function migration() { |
|||
await diskService.writeFile(constants.MIGRATION_SIGNAL_FILE, 'true'); |
|||
} |
|||
|
|||
function readMigrationStatusFile() { |
|||
return diskService.readJsonFile(constants.MIGRATION_STATUS_FILE); |
|||
} |
|||
|
|||
async function writeMigrationStatusFile(json) { |
|||
return diskService.writeJsonFile(constants.MIGRATION_STATUS_FILE, json); |
|||
} |
|||
|
|||
// Send a signal to enable/disable SSH.
|
|||
async function enableSsh(state) { |
|||
await diskService.writeFile(constants.SSH_SIGNAL_FILE, state); |
|||
} |
|||
|
|||
function readSshSignalFile() { |
|||
return diskService.readFile(constants.SSH_SIGNAL_FILE); |
|||
} |
|||
|
|||
module.exports = { |
|||
deleteItemsInDir, |
|||
deleteUserFile, |
|||
deleteFoldersInDir, |
|||
moveFoldersToDir, |
|||
fileExists, |
|||
getBuildDetails, |
|||
listVersionsForApp, |
|||
readSettingsFile, |
|||
readUserFile, |
|||
writeAppVersionFile, |
|||
writeSettingsFile, |
|||
writeUserFile, |
|||
settingsFileExists, |
|||
hiddenServiceFileExists, |
|||
readAppVersionFile, |
|||
readHiddenService, |
|||
readJWTPrivateKeyFile, |
|||
readJWTPublicKeyFile, |
|||
writeJWTPrivateKeyFile, |
|||
writeJWTPublicKeyFile, |
|||
shutdown, |
|||
relaunch, |
|||
readUtf8File, |
|||
readJsonFile, |
|||
writeMigrationStatusFile, |
|||
readMigrationStatusFile, |
|||
migration, |
|||
enableSsh, |
|||
readSshSignalFile |
|||
}; |
@ -0,0 +1,166 @@ |
|||
const passport = require('passport'); |
|||
const passportJWT = require('passport-jwt'); |
|||
const passportHTTP = require('passport-http'); |
|||
const bcrypt = require('bcrypt'); |
|||
const diskLogic = require('logic/disk.js'); |
|||
const authLogic = require('logic/auth.js'); |
|||
const NodeError = require('models/errors.js').NodeError; |
|||
const UUID = require('utils/UUID.js'); |
|||
const rsa = require('node-rsa'); |
|||
|
|||
const JwtStrategy = passportJWT.Strategy; |
|||
const BasicStrategy = passportHTTP.BasicStrategy; |
|||
const ExtractJwt = passportJWT.ExtractJwt; |
|||
|
|||
const JWT_AUTH = 'jwt'; |
|||
const REGISTRATION_AUTH = 'register'; |
|||
const BASIC_AUTH = 'basic'; |
|||
|
|||
const SYSTEM_USER = UUID.fetchBootUUID() || 'admin'; |
|||
|
|||
async function generateJWTKeys() { |
|||
const key = new rsa({ b: 512 }); // eslint-disable-line id-length
|
|||
|
|||
const privateKey = key.exportKey('private'); |
|||
const publicKey = key.exportKey('public'); |
|||
|
|||
await diskLogic.writeJWTPrivateKeyFile(privateKey); |
|||
await diskLogic.writeJWTPublicKeyFile(publicKey); |
|||
} |
|||
|
|||
async function createJwtOptions() { |
|||
await generateJWTKeys(); |
|||
const pubKey = await diskLogic.readJWTPublicKeyFile(); |
|||
|
|||
return { |
|||
jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme('jwt'), |
|||
secretOrKey: pubKey, |
|||
algorithm: 'RS256' |
|||
}; |
|||
} |
|||
|
|||
passport.serializeUser(function (user, done) { |
|||
return done(null, SYSTEM_USER); |
|||
}); |
|||
|
|||
passport.use(BASIC_AUTH, new BasicStrategy(function (username, password, next) { |
|||
const user = { |
|||
username: SYSTEM_USER, |
|||
password, |
|||
plainTextPassword: password |
|||
}; |
|||
return next(null, user); |
|||
})); |
|||
|
|||
createJwtOptions().then(function (data) { |
|||
const jwtOptions = data; |
|||
|
|||
passport.use(JWT_AUTH, new JwtStrategy(jwtOptions, function (jwtPayload, done) { |
|||
return done(null, { username: SYSTEM_USER }); |
|||
})); |
|||
}); |
|||
|
|||
passport.use(REGISTRATION_AUTH, new BasicStrategy(function (username, password, next) { |
|||
const credentials = authLogic.hashCredentials(SYSTEM_USER, password); |
|||
|
|||
return next(null, credentials); |
|||
})); |
|||
|
|||
// Override the authorization header with password that is in the body of the request if basic auth was not supplied.
|
|||
function convertReqBodyToBasicAuth(req, res, next) { |
|||
if (req.body.password && !req.headers.authorization) { |
|||
req.headers.authorization = 'Basic ' + Buffer.from(SYSTEM_USER + ':' + req.body.password).toString('base64'); |
|||
} |
|||
|
|||
next(); |
|||
} |
|||
|
|||
function basic(req, res, next) { |
|||
passport.authenticate(BASIC_AUTH, { session: false }, function (error, user) { |
|||
|
|||
function handleCompare(equal) { |
|||
if (!equal) { |
|||
return next(new NodeError('Incorrect password', 401)); // eslint-disable-line no-magic-numbers
|
|||
} |
|||
req.logIn(user, function (err) { |
|||
if (err) { |
|||
return next(new NodeError('Unable to authenticate', 401)); // eslint-disable-line no-magic-numbers
|
|||
} |
|||
|
|||
return next(null, user); |
|||
}); |
|||
} |
|||
|
|||
if (error || user === false) { |
|||
return next(new NodeError('Invalid state', 401)); // eslint-disable-line no-magic-numbers
|
|||
} |
|||
|
|||
diskLogic.readUserFile() |
|||
.then(userData => { |
|||
const storedPassword = userData.password; |
|||
|
|||
bcrypt.compare(user.password, storedPassword) |
|||
.then(handleCompare) |
|||
.catch(next); |
|||
}) |
|||
.catch(() => next(new NodeError('No user registered', 401))); // eslint-disable-line no-magic-numbers
|
|||
})(req, res, next); |
|||
} |
|||
|
|||
function jwt(req, res, next) { |
|||
passport.authenticate(JWT_AUTH, { session: false }, function (error, user) { |
|||
if (error || user === false) { |
|||
return next(new NodeError('Invalid JWT', 401)); // eslint-disable-line no-magic-numbers
|
|||
} |
|||
req.logIn(user, function (err) { |
|||
if (err) { |
|||
return next(new NodeError('Unable to authenticate', 401)); // eslint-disable-line no-magic-numbers
|
|||
} |
|||
|
|||
return next(null, user); |
|||
}); |
|||
})(req, res, next); |
|||
} |
|||
|
|||
async function accountJWTProtected(req, res, next) { |
|||
const isRegistered = await authLogic.isRegistered(); |
|||
if (isRegistered.registered) { |
|||
passport.authenticate(JWT_AUTH, { session: false }, function (error, user) { |
|||
if (error || user === false) { |
|||
return next(new NodeError('Invalid JWT', 401)); // eslint-disable-line no-magic-numbers
|
|||
} |
|||
req.logIn(user, function (err) { |
|||
if (err) { |
|||
return next(new NodeError('Unable to authenticate', 401)); // eslint-disable-line no-magic-numbers
|
|||
} |
|||
|
|||
return next(null, user); |
|||
}); |
|||
})(req, res, next); |
|||
} else { |
|||
return next(null, 'not-registered'); |
|||
} |
|||
} |
|||
|
|||
function register(req, res, next) { |
|||
passport.authenticate(REGISTRATION_AUTH, { session: false }, function (error, user) { |
|||
if (error || user === false) { |
|||
return next(new NodeError('Invalid state', 401)); // eslint-disable-line no-magic-numbers
|
|||
} |
|||
req.logIn(user, function (err) { |
|||
if (err) { |
|||
return next(new NodeError('Unable to authenticate', 401)); // eslint-disable-line no-magic-numbers
|
|||
} |
|||
|
|||
return next(null, user); |
|||
}); |
|||
})(req, res, next); |
|||
} |
|||
|
|||
module.exports = { |
|||
basic, |
|||
convertReqBodyToBasicAuth, |
|||
jwt, |
|||
register, |
|||
accountJWTProtected, |
|||
}; |
@ -0,0 +1,12 @@ |
|||
const camelizeKeys = require('camelize-keys'); |
|||
|
|||
function camelCaseRequest(req, res, next) { |
|||
if (req && req.body) { |
|||
req.body = camelizeKeys(req.body, '_'); |
|||
} |
|||
next(); |
|||
} |
|||
|
|||
module.exports = { |
|||
camelCaseRequest, |
|||
}; |
@ -0,0 +1,25 @@ |
|||
const corsOptions = { |
|||
origin: (origin, callback) => { |
|||
const whitelist = [ |
|||
'http://localhost:3000', |
|||
'http://localhost:8080', |
|||
'http://umbrel.local', |
|||
process.env.DEVICE_HOST, |
|||
]; |
|||
|
|||
// Whitelist hidden service if exists.
|
|||
if (process.env.UMBREL_HIDDEN_SERVICE) { |
|||
whitelist.push(process.env.UMBREL_HIDDEN_SERVICE); |
|||
} |
|||
|
|||
if (whitelist.indexOf(origin) !== -1 || !origin) { |
|||
return callback(null, true); |
|||
} else { |
|||
return callback(new Error('Not allowed by CORS')); |
|||
} |
|||
} |
|||
}; |
|||
|
|||
module.exports = { |
|||
corsOptions, |
|||
}; |
@ -0,0 +1,16 @@ |
|||
/* eslint-disable no-unused-vars, no-magic-numbers */ |
|||
const logger = require('utils/logger.js'); |
|||
const LndError = require('models/errors.js').LndError; |
|||
|
|||
function handleError(error, req, res, next) { |
|||
|
|||
var statusCode = error.statusCode || 500; |
|||
var route = req.url || ''; |
|||
var message = error.message || ''; |
|||
|
|||
logger.error(message, route, error.stack); |
|||
|
|||
res.status(statusCode).json(message); |
|||
} |
|||
|
|||
module.exports = handleError; |
@ -0,0 +1,30 @@ |
|||
/* |
|||
Tor 9.0.0 does not automatically include the origin header. This causes cross origin errors. This middleware handles |
|||
this bug until Tor releases a bugfix. |
|||
|
|||
https://trac.torproject.org/projects/tor/ticket/32255
|
|||
*/ |
|||
function onionOrigin(req, res, next) { |
|||
|
|||
// Get just the onion address.
|
|||
let hiddenService; |
|||
if (process.env.UMBREL_HIDDEN_SERVICE |
|||
&& process.env.UMBREL_HIDDEN_SERVICE.startsWith('http://')) { |
|||
|
|||
hiddenService = process.env.UMBREL_HIDDEN_SERVICE.substring(7, // eslint-disable-line no-magic-numbers
|
|||
process.env.UMBREL_HIDDEN_SERVICE.length); |
|||
} |
|||
|
|||
// If a hidden service is known and the request has the Tor Browser 9.0.0 bug.
|
|||
if (hiddenService |
|||
&& req.headers.host.includes(hiddenService) |
|||
&& !req.headers.origin) { |
|||
|
|||
// Manually add hidden service (with http://) to the response header.
|
|||
res.setHeader('Access-Control-Allow-Origin', process.env.UMBREL_HIDDEN_SERVICE); |
|||
} |
|||
|
|||
next(); |
|||
} |
|||
|
|||
module.exports = onionOrigin; |
@ -0,0 +1,15 @@ |
|||
const UUID = require('utils/UUID.js'); |
|||
const constants = require('utils/const.js'); |
|||
const createNamespace = require('continuation-local-storage').createNamespace; |
|||
const apiRequest = createNamespace(constants.REQUEST_CORRELATION_NAMESPACE_KEY); |
|||
|
|||
function addCorrelationId(req, res, next) { |
|||
apiRequest.bindEmitter(req); |
|||
apiRequest.bindEmitter(res); |
|||
apiRequest.run(function () { |
|||
apiRequest.set(constants.REQUEST_CORRELATION_ID_KEY, UUID.create()); |
|||
next(); |
|||
}); |
|||
} |
|||
|
|||
module.exports = addCorrelationId; |
@ -0,0 +1,22 @@ |
|||
/* eslint-disable no-magic-numbers */ |
|||
function NodeError(message, statusCode) { |
|||
Error.captureStackTrace(this, this.constructor); |
|||
this.name = this.constructor.name; |
|||
this.message = message; |
|||
this.statusCode = statusCode; |
|||
} |
|||
require('util').inherits(NodeError, Error); |
|||
|
|||
function ValidationError(message, statusCode) { |
|||
Error.captureStackTrace(this, this.constructor); |
|||
this.name = this.constructor.name; |
|||
this.message = message; |
|||
this.statusCode = statusCode || 400; |
|||
} |
|||
require('util').inherits(ValidationError, Error); |
|||
|
|||
module.exports = { |
|||
NodeError, |
|||
ValidationError |
|||
}; |
|||
|
@ -0,0 +1,62 @@ |
|||
{ |
|||
"name": "umbrel-manager", |
|||
"version": "0.0.0", |
|||
"description": "Manager for Umbrel Node", |
|||
"author": "Umbrel", |
|||
"scripts": { |
|||
"lint": "eslint", |
|||
"start": "nodemon --exec node ./bin/www", |
|||
"test": "mocha --file test.setup 'test/**/*.js'", |
|||
"coverage": "nyc --all mocha --file test.setup 'test/**/*.js'", |
|||
"postcoverage": "codecov" |
|||
}, |
|||
"dependencies": { |
|||
"bcrypt": "^4.0.1", |
|||
"big.js": "^5.2.2", |
|||
"body-parser": "^1.18.2", |
|||
"camelize-keys": "^1.0.0", |
|||
"continuation-local-storage": "^3.2.1", |
|||
"cors": "^2.8.5", |
|||
"debug": "^2.6.1", |
|||
"dotenv": "^8.2.0", |
|||
"express": "^4.16.3", |
|||
"fs-extra": "^9.0.0", |
|||
"iocane": "^4.0.0", |
|||
"module-alias": "^2.1.0", |
|||
"morgan": "^1.9.0", |
|||
"node-rsa": "^1.0.8", |
|||
"npm": "^6.13.4", |
|||
"passport": "^0.4.0", |
|||
"passport-http": "^0.3.0", |
|||
"passport-jwt": "^4.0.0", |
|||
"request-promise": "^4.2.2", |
|||
"uuid": "^3.3.2", |
|||
"validator": "^9.2.0", |
|||
"winston": "^3.0.0-rc5", |
|||
"winston-daily-rotate-file": "^3.1.3" |
|||
}, |
|||
"devDependencies": { |
|||
"babel-eslint": "^8.2.6", |
|||
"chai": "^4.1.2", |
|||
"chai-http": "^4.2.0", |
|||
"codecov": "^3.6.5", |
|||
"eslint": "^5.3.0", |
|||
"mocha": "^5.2.0", |
|||
"nodemon": "^2.0.4", |
|||
"nyc": "13.0.1", |
|||
"proxyquire": "^2.0.1", |
|||
"sinon": "^6.1.4" |
|||
}, |
|||
"nyc": { |
|||
"exclude": [ |
|||
"test", |
|||
"test.setup.js" |
|||
], |
|||
"sourceMap": false, |
|||
"reporter": [ |
|||
"lcov", |
|||
"text-summary" |
|||
], |
|||
"cache": "false" |
|||
} |
|||
} |
File diff suppressed because it is too large
@ -0,0 +1,9 @@ |
|||
const express = require('express'); |
|||
const pjson = require('../package.json'); |
|||
const router = express.Router(); |
|||
|
|||
router.get('/', function (req, res) { |
|||
res.json({ version: 'umbrel-manager-' + pjson.version }); |
|||
}); |
|||
|
|||
module.exports = router; |
@ -0,0 +1,116 @@ |
|||
const express = require('express'); |
|||
const router = express.Router(); |
|||
|
|||
// const applicationLogic = require('logic/application.js');
|
|||
const authLogic = require('logic/auth.js'); |
|||
|
|||
const auth = require('middlewares/auth.js'); |
|||
// const changePasswordAuthHandler = require('middlewares/changePasswordAuthHandler.js');
|
|||
|
|||
const constants = require('utils/const.js'); |
|||
const safeHandler = require('utils/safeHandler'); |
|||
const validator = require('utils/validator.js'); |
|||
|
|||
// const COMPLETE = 100;
|
|||
|
|||
// // Endpoint to change your lnd password. Wallet must exist and be unlocked. This endpoint is authorized with basic auth
|
|||
// // or the property password from the body.
|
|||
// router.post('/changePassword', auth.convertReqBodyToBasicAuth, auth.basic, changePasswordAuthHandler,
|
|||
// safeHandler(async(req, res, next) => {
|
|||
|
|||
// // Use password from the body by default. Basic auth has issues handling special characters.
|
|||
// const currentPassword = req.body.password;
|
|||
// const newPassword = req.body.newPassword;
|
|||
|
|||
// const jwt = await authLogic.refresh(req.user);
|
|||
|
|||
// try {
|
|||
// validator.isString(currentPassword);
|
|||
// validator.isMinPasswordLength(currentPassword);
|
|||
// validator.isString(newPassword);
|
|||
// validator.isMinPasswordLength(newPassword);
|
|||
// } catch (error) {
|
|||
// return next(error);
|
|||
// }
|
|||
|
|||
// const status = await authLogic.getChangePasswordStatus();
|
|||
|
|||
// // return a conflict if a change password process is already running
|
|||
// if (status.percent > 0 && status.percent !== COMPLETE) {
|
|||
// return res.status(constants.STATUS_CODES.CONFLICT).json();
|
|||
// }
|
|||
|
|||
// // start change password process in the background and immediately return
|
|||
// authLogic.changePassword(currentPassword, newPassword, jwt.jwt);
|
|||
|
|||
// return res.status(constants.STATUS_CODES.ACCEPTED).json();
|
|||
// }));
|
|||
|
|||
// // Returns the current status of the change password process.
|
|||
// router.get('/changePassword/status', auth.jwt, safeHandler(async(req, res) => {
|
|||
// const status = await authLogic.getChangePasswordStatus();
|
|||
|
|||
// return res.status(constants.STATUS_CODES.OK).json(status);
|
|||
// }));
|
|||
|
|||
// Registered does not need auth. This is because the user may not be registered at the time and thus won't always have
|
|||
// // an auth token.
|
|||
// router.get('/registered', safeHandler((req, res) =>
|
|||
// authLogic.isRegistered()
|
|||
// .then(registered => res.json(registered))
|
|||
// ));
|
|||
|
|||
// Endpoint to register a password with the device. Wallet must not exist. This endpoint is authorized with basic auth
|
|||
// or the property password from the body.
|
|||
router.post('/register', auth.convertReqBodyToBasicAuth, auth.register, safeHandler(async (req, res, next) => { |
|||
|
|||
|
|||
// return res.json({ seed: req.body.seed });
|
|||
|
|||
const seed = req.body.seed; |
|||
|
|||
if (seed.length !== 24) { // eslint-disable-line no-magic-numbers
|
|||
throw new Error('Invalid seed length'); |
|||
} |
|||
|
|||
try { |
|||
validator.isString(req.body.name); |
|||
validator.isString(req.user.plainTextPassword); |
|||
validator.isMinPasswordLength(req.user.plainTextPassword); |
|||
} catch (error) { |
|||
return next(error); |
|||
} |
|||
|
|||
const user = { |
|||
name: req.body.name, |
|||
password: req.user.password, |
|||
plainTextPassword: req.user.plainTextPassword |
|||
}; |
|||
|
|||
const jwt = await authLogic.register(user, seed); |
|||
|
|||
return res.json(jwt); |
|||
})); |
|||
|
|||
router.post('/login', auth.convertReqBodyToBasicAuth, auth.basic, safeHandler(async (req, res) => { |
|||
const jwt = await authLogic.login(req.user); |
|||
|
|||
return res.json(jwt); |
|||
} |
|||
|
|||
)); |
|||
|
|||
router.post('/seed', auth.convertReqBodyToBasicAuth, auth.basic, safeHandler(async (req, res) => { |
|||
const seed = await authLogic.seed(req.user); |
|||
|
|||
return res.json(seed); |
|||
} |
|||
|
|||
)); |
|||
|
|||
// router.post('/refresh', auth.jwt, safeHandler((req, res) =>
|
|||
// applicationLogic.refresh(req.user)
|
|||
// .then(jwt => res.json(jwt))
|
|||
// ));
|
|||
|
|||
module.exports = router; |
@ -0,0 +1,56 @@ |
|||
const childProcess = require('child_process'); |
|||
|
|||
// Sets environment variables on container.
|
|||
// Env should not contain sensitive data, because environment variables are not secure.
|
|||
function extendProcessEnv(env) { |
|||
Object.keys(env).map(function (objectKey) { // eslint-disable-line array-callback-return
|
|||
process.env[objectKey] = env[objectKey]; |
|||
}); |
|||
} |
|||
|
|||
// Executes docker-compose command with common options
|
|||
const exec = (command, args, opts) => new Promise((resolve, reject) => { |
|||
const options = opts || {}; |
|||
|
|||
const cwd = options.cwd || null; |
|||
|
|||
if (options.env) { |
|||
extendProcessEnv(options.env); |
|||
} |
|||
|
|||
const childProc = childProcess.spawn(command, args, { cwd }); |
|||
|
|||
childProc.on('error', err => { |
|||
reject(err); |
|||
}); |
|||
|
|||
const result = { |
|||
err: '', |
|||
out: '' |
|||
}; |
|||
|
|||
childProc.stdout.on('data', chunk => { |
|||
result.out += chunk.toString(); |
|||
}); |
|||
|
|||
childProc.stderr.on('data', chunk => { |
|||
result.err += chunk.toString(); |
|||
}); |
|||
|
|||
childProc.on('close', code => { |
|||
if (code === 0) { |
|||
resolve(result); |
|||
} else { |
|||
reject(result.err); |
|||
} |
|||
}); |
|||
|
|||
if (options.log) { |
|||
childProc.stdout.pipe(process.stdout); |
|||
childProc.stderr.pipe(process.stderr); |
|||
} |
|||
}); |
|||
|
|||
module.exports = { |
|||
exec, |
|||
}; |
@ -0,0 +1,186 @@ |
|||
/** |
|||
* Generic disk functions. |
|||
*/ |
|||
|
|||
const logger = require('utils/logger'); |
|||
const fs = require('fs'); |
|||
const fse = require('fs-extra'); |
|||
const crypto = require('crypto'); |
|||
const uint32Bytes = 4; |
|||
|
|||
// Deletes a file from the filesystem
|
|||
function deleteFile(filePath) { |
|||
return new Promise((resolve, reject) => fs.unlink(filePath, (err, str) => { |
|||
if (err) { |
|||
reject(err); |
|||
} else { |
|||
resolve(str); |
|||
} |
|||
})); |
|||
} |
|||
|
|||
async function copyFolder(fromFile, toFile) { |
|||
return new Promise((resolve, reject) => fse.copy(fromFile, toFile, err => { |
|||
if (err) { |
|||
reject(err); |
|||
} else { |
|||
resolve(); |
|||
} |
|||
})); |
|||
} |
|||
|
|||
// Delete all items in a directory.
|
|||
async function deleteItemsInDir(path) { |
|||
|
|||
const contents = fs.readdirSync(path); |
|||
|
|||
for (const item of contents) { |
|||
|
|||
const curPath = path + '/' + item; |
|||
if (fs.statSync(curPath).isDirectory()) { |
|||
deleteFolderRecursive(curPath); |
|||
} else { |
|||
fs.unlinkSync(curPath); |
|||
} |
|||
} |
|||
} |
|||
|
|||
async function deleteFoldersInDir(path) { |
|||
|
|||
const contents = fs.readdirSync(path); |
|||
|
|||
for (const item of contents) { |
|||
if (fs.statSync(path + '/' + item).isDirectory()) { |
|||
deleteFolderRecursive(path + '/' + item); |
|||
} |
|||
} |
|||
} |
|||
|
|||
function deleteFolderRecursive(path) { |
|||
if (fs.existsSync(path)) { |
|||
const contents = fs.readdirSync(path); |
|||
|
|||
for (const file of contents) { |
|||
const curPath = path + '/' + file; |
|||
if (fs.statSync(curPath).isDirectory()) { |
|||
deleteFolderRecursive(curPath); |
|||
} else { |
|||
fs.unlinkSync(curPath); |
|||
} |
|||
} |
|||
fs.rmdirSync(path); |
|||
} |
|||
} |
|||
|
|||
async function listDirsInDir(dir) { |
|||
const contents = fs.readdirSync(dir); |
|||
|
|||
const dirs = []; |
|||
|
|||
for (const item of contents) { |
|||
if (fs.statSync(dir + '/' + item).isDirectory()) { |
|||
dirs.push(item); |
|||
} |
|||
} |
|||
|
|||
return dirs; |
|||
} |
|||
|
|||
async function moveFoldersToDir(fromDir, toDir) { |
|||
|
|||
const contents = fs.readdirSync(fromDir); |
|||
|
|||
for (const item of contents) { |
|||
if (item !== '.git' && fs.statSync(fromDir + '/' + item).isDirectory()) { |
|||
await copyFolder(fromDir + '/' + item, toDir + '/' + item); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Reads a file. Wraps fs.readFile into a native promise
|
|||
function readFile(filePath, encoding) { |
|||
return new Promise((resolve, reject) => fs.readFile(filePath, encoding, (err, str) => { |
|||
if (err) { |
|||
reject(err); |
|||
} else { |
|||
resolve(str); |
|||
} |
|||
})); |
|||
} |
|||
|
|||
// Reads a file as a utf8 string. Wraps fs.readFile into a native promise
|
|||
function readUtf8File(filePath) { |
|||
return readFile(filePath, 'utf8'); |
|||
} |
|||
|
|||
async function readJsonFile(filePath) { |
|||
return readUtf8File(filePath).then(JSON.parse); |
|||
} |
|||
|
|||
// Writes a string to a file. Wraps fs.writeFile into a native promise
|
|||
// This is _not_ concurrency safe, so don't export it without making it like writeJsonFile
|
|||
function writeFile(filePath, data, encoding) { |
|||
return new Promise((resolve, reject) => fs.writeFile(filePath, data, encoding, err => { |
|||
if (err) { |
|||
reject(err); |
|||
} else { |
|||
resolve(); |
|||
} |
|||
})); |
|||
} |
|||
|
|||
function writeJsonFile(filePath, obj) { |
|||
const tempFileName = `${filePath}.${crypto.randomBytes(uint32Bytes).readUInt32LE(0)}`; |
|||
|
|||
return writeFile(tempFileName, JSON.stringify(obj, null, 2), 'utf8') |
|||
.then(() => new Promise((resolve, reject) => fs.rename(tempFileName, filePath, err => { |
|||
if (err) { |
|||
reject(err); |
|||
} else { |
|||
resolve(); |
|||
} |
|||
}))) |
|||
.catch(err => { |
|||
if (err) { |
|||
fs.unlink(tempFileName, err => { |
|||
logger.warn('Error removing temporary file after error', 'disk', { err, tempFileName }); |
|||
}); |
|||
} |
|||
throw err; |
|||
}); |
|||
} |
|||
|
|||
function writeKeyFile(filePath, obj) { |
|||
const tempFileName = `${filePath}.${crypto.randomBytes(uint32Bytes).readUInt32LE(0)}`; |
|||
|
|||
return writeFile(tempFileName, obj, 'utf8') |
|||
.then(() => new Promise((resolve, reject) => fs.rename(tempFileName, filePath, err => { |
|||
if (err) { |
|||
reject(err); |
|||
} else { |
|||
resolve(); |
|||
} |
|||
}))) |
|||
.catch(err => { |
|||
if (err) { |
|||
fs.unlink(tempFileName, err => { |
|||
logger.warn('Error removing temporary file after error', 'disk', { err, tempFileName }); |
|||
}); |
|||
} |
|||
throw err; |
|||
}); |
|||
} |
|||
|
|||
module.exports = { |
|||
deleteItemsInDir, |
|||
deleteFile, |
|||
deleteFoldersInDir, |
|||
listDirsInDir, |
|||
moveFoldersToDir, |
|||
readFile, |
|||
readUtf8File, |
|||
readJsonFile, |
|||
writeJsonFile, |
|||
writeKeyFile, |
|||
writeFile, |
|||
}; |
@ -0,0 +1,9 @@ |
|||
{ |
|||
"extends": "../.eslintrc", |
|||
"env": { |
|||
"mocha": true |
|||
}, |
|||
"rules": { |
|||
"no-magic-numbers": "off" |
|||
} |
|||
} |
@ -0,0 +1,17 @@ |
|||
/* globals requester */ |
|||
|
|||
describe('ping', () => { |
|||
it('should respond on /ping GET', done => { |
|||
requester |
|||
.get('/ping') |
|||
.end((err, res) => { |
|||
if (err) { |
|||
done(err); |
|||
} |
|||
res.should.have.status(200); |
|||
res.should.be.json; |
|||
res.body.should.have.property('version'); |
|||
done(); |
|||
}); |
|||
}); |
|||
}); |
@ -0,0 +1,22 @@ |
|||
const chai = require('chai'); |
|||
const chaiHttp = require('chai-http'); |
|||
const server = require('../app.js'); |
|||
|
|||
chai.use(chaiHttp); |
|||
chai.should(); |
|||
|
|||
global.expect = chai.expect; |
|||
global.assert = chai.assert; |
|||
|
|||
before(() => { |
|||
global.requester = chai.request(server).keepOpen(); |
|||
}); |
|||
|
|||
global.reset = () => { |
|||
global.Lightning.reset(); |
|||
global.WalletUnlocker.reset(); |
|||
}; |
|||
|
|||
after(() => { |
|||
global.requester.close(); |
|||
}); |
@ -0,0 +1,24 @@ |
|||
const bashService = require('services/bash.js'); |
|||
const uuidv4 = require('uuid/v4'); |
|||
|
|||
function fetchBootUUID() { |
|||
bashService.exec('cat', ['/proc/sys/kernel/random/boot_id'], {}) |
|||
.then(uuid => Promise.resolve(uuid)) |
|||
.catch(() => Promise.resolve()); |
|||
} |
|||
|
|||
function fetchSerial() { |
|||
const commandOptions = ['/proc/cpuinfo', |
|||
'|', 'egrep', '"Serial"', |
|||
'|', 'awk', '\'{print $3}\'']; |
|||
|
|||
bashService.exec('cat', commandOptions, {}) |
|||
.then(serial => Promise.resolve(serial)) |
|||
.catch(() => Promise.resolve()); |
|||
} |
|||
|
|||
module.exports = { |
|||
create: uuidv4, |
|||
fetchBootUUID, |
|||
fetchSerial, |
|||
}; |
@ -0,0 +1,13 @@ |
|||
/* eslint-disable id-length */ |
|||
module.exports = { |
|||
REQUEST_CORRELATION_NAMESPACE_KEY: 'umbrel-manager-request', |
|||
USER_PASSWORD_FILE: process.env.USER_PASSWORD_FILE || '/home/umbrel/db/user.json', |
|||
JWT_PUBLIC_KEY_FILE: process.env.JWT_PUBLIC_KEY_FILE || '/home/umbrel/db/jwt/jwt.pem', |
|||
JWT_PRIVATE_KEY_FILE: process.env.JWT_PRIVATE_KEY_FILE || '/home/umbrel/db/jwt/jwt.key', |
|||
REQUEST_CORRELATION_ID_KEY: 'reqId', |
|||
STATUS_CODES: { |
|||
BAD_GATEWAY: 502, |
|||
FORBIDDEN: 403, |
|||
OK: 200, |
|||
}, |
|||
}; |
@ -0,0 +1,66 @@ |
|||
// Source from https://github.com/richardschneider/bitcoin-convert/
|
|||
const Big = require('big.js'); |
|||
const BTC = 1; |
|||
const SAT = 0.00000001; |
|||
|
|||
const units = { |
|||
btc: new Big(BTC), |
|||
sat: new Big(SAT), |
|||
}; |
|||
|
|||
function convert(from, fromUnit, toUnit, representation) { |
|||
const fromFactor = units[fromUnit]; |
|||
if (fromFactor === undefined) { |
|||
throw new Error(`'${fromUnit}' is not a bitcoin unit`); |
|||
} |
|||
const toFactor = units[toUnit]; |
|||
if (toFactor === undefined) { |
|||
throw new Error(`'${toUnit}' is not a bitcoin unit`); |
|||
} |
|||
|
|||
if (Number.isNaN(from)) { |
|||
if (!representation || representation === 'Number') { |
|||
return from; |
|||
} else if (representation === 'Big') { |
|||
return new Big(from); // throws BigError
|
|||
} else if (representation === 'String') { |
|||
return from.toString(); |
|||
} |
|||
throw new Error(`'${representation}' is not a valid representation`); |
|||
} |
|||
|
|||
const result = new Big(from).times(fromFactor).div(toFactor); |
|||
|
|||
if (!representation || representation === 'Number') { |
|||
return Number(result); |
|||
} else if (representation === 'Big') { |
|||
return result; |
|||
} else if (representation === 'String') { |
|||
return result.toString(); |
|||
} |
|||
|
|||
throw new Error(`'${representation}' is not a valid representation`); |
|||
} |
|||
|
|||
convert.units = function () { |
|||
return Object.keys(units); |
|||
}; |
|||
|
|||
convert.addUnit = function addUnit(unit, factor) { |
|||
const bigFactor = new Big(factor); |
|||
const existing = units[unit]; |
|||
if (existing && !existing.eq(bigFactor)) { |
|||
throw new Error(`'${unit}' already exists with a different conversion factor`); |
|||
} |
|||
units[unit] = bigFactor; |
|||
}; |
|||
|
|||
const predefinedUnits = convert.units(); |
|||
convert.removeUnit = function removeUnit(unit) { |
|||
if (predefinedUnits.indexOf(unit) >= 0) { |
|||
throw new Error(`'${unit}' is predefined and cannot be removed`); |
|||
} |
|||
delete units[unit]; |
|||
}; |
|||
|
|||
module.exports = convert; |
@ -0,0 +1,28 @@ |
|||
const jwt = require('jsonwebtoken'); |
|||
const diskLogic = require('logic/disk.js'); |
|||
|
|||
// Environmental variables are Strings, the expiry will be interpreted as milliseconds if not converted to int.
|
|||
// eslint-disable-next-line no-magic-numbers
|
|||
const expiresIn = process.env.JWT_EXPIRATION ? parseInt(process.env.JWT_EXPIRATION, 3600) : 3600; |
|||
|
|||
async function generateJWT(account) { |
|||
|
|||
const jwtPrivateKey = await diskLogic.readJWTPrivateKeyFile(); |
|||
|
|||
const jwtPubKey = await diskLogic.readJWTPublicKeyFile(); |
|||
|
|||
// eslint-disable-next-line object-shorthand
|
|||
const token = await jwt.sign({ id: account }, jwtPrivateKey, { expiresIn: expiresIn, algorithm: 'RS256' }); |
|||
|
|||
await jwt.verify(token, jwtPubKey, function (error) { |
|||
if (error) { |
|||
return Promise.reject(new Error('Error generating JWT token.')); |
|||
} |
|||
}); |
|||
|
|||
return token; |
|||
} |
|||
|
|||
module.exports = { |
|||
generateJWT, |
|||
}; |
@ -0,0 +1,133 @@ |
|||
require('winston-daily-rotate-file'); |
|||
const constants = require('utils/const.js'); |
|||
const fs = require('fs'); |
|||
const path = require('path'); |
|||
const winston = require('winston'); |
|||
const { format } = require('winston'); |
|||
const { combine, timestamp, printf } = format; |
|||
const getNamespace = require('continuation-local-storage').getNamespace; |
|||
|
|||
const LOCAL = 'local'; |
|||
const logDir = './logs'; |
|||
const ENV = process.env.NODE_ENV; |
|||
|
|||
if (!fs.existsSync(logDir)) { |
|||
fs.mkdirSync(logDir); |
|||
} |
|||
|
|||
const appendCorrelationId = format((info, opts) => { |
|||
var apiRequest = getNamespace(constants.REQUEST_CORRELATION_NAMESPACE_KEY); |
|||
if (apiRequest) { |
|||
info.internalCorrelationId = apiRequest.get(constants.REQUEST_CORRELATION_ID_KEY); |
|||
} |
|||
|
|||
return info; |
|||
}); |
|||
|
|||
const errorFileTransport = new winston.transports.DailyRotateFile({ |
|||
filename: path.join(logDir, 'error-%DATE%.log'), |
|||
datePattern: 'YYYY-MM-DD', |
|||
level: 'error', |
|||
maxSize: '10m', |
|||
maxFiles: '7d' |
|||
}); |
|||
|
|||
const apiFileTransport = new winston.transports.DailyRotateFile({ |
|||
filename: path.join(logDir, 'api-%DATE%.log'), |
|||
datePattern: 'YYYY-MM-DD', |
|||
maxSize: '10m', |
|||
maxFiles: '7d' |
|||
}); |
|||
|
|||
const localLogFormat = printf(info => { |
|||
var data = ''; |
|||
if (info.data) { |
|||
data = JSON.stringify({ data: info.data }); |
|||
} |
|||
|
|||
return `${info.timestamp} ${info.level.toUpperCase()}: ${info.internalCorrelationId} [${info._module}] ${info.message} ${data}`; |
|||
}); |
|||
|
|||
const localLoggerTransports = [ |
|||
errorFileTransport, |
|||
apiFileTransport, |
|||
]; |
|||
|
|||
if (ENV === 'development') { |
|||
localLoggerTransports.push(new winston.transports.Console()); |
|||
} |
|||
|
|||
winston.loggers.add(LOCAL, { |
|||
level: 'info', |
|||
format: combine( |
|||
timestamp(), |
|||
appendCorrelationId(), |
|||
localLogFormat |
|||
), |
|||
transports: localLoggerTransports |
|||
}); |
|||
|
|||
const morganConfiguration = { |
|||
stream: { |
|||
write: function (message) { |
|||
info(message, 'umbrel-manager'); |
|||
} |
|||
} |
|||
}; |
|||
|
|||
const localLogger = winston.loggers.get(LOCAL); |
|||
|
|||
function printToStandardOut(data) { |
|||
if (data) { |
|||
console.log(data); |
|||
} |
|||
} |
|||
|
|||
function error(message, _module, data) { |
|||
printToStandardOut(message); |
|||
printToStandardOut(_module); |
|||
printToStandardOut(data); |
|||
localLogger.error(message, { |
|||
_module: _module, |
|||
data: data |
|||
}); |
|||
} |
|||
|
|||
function warn(message, _module, data) { |
|||
printToStandardOut(message); |
|||
printToStandardOut(_module); |
|||
printToStandardOut(data); |
|||
localLogger.warn(message, { |
|||
_module: _module, |
|||
data: data |
|||
}); |
|||
} |
|||
|
|||
function info(message, _module, data) { |
|||
printToStandardOut(message); |
|||
printToStandardOut(_module); |
|||
printToStandardOut(data); |
|||
localLogger.info(message, { |
|||
_module: _module, |
|||
data: data |
|||
}); |
|||
} |
|||
|
|||
function debug(message, _module, data) { |
|||
printToStandardOut(message); |
|||
printToStandardOut(_module); |
|||
printToStandardOut(data); |
|||
localLogger.debug(message, { |
|||
_module: _module, |
|||
data: data |
|||
}); |
|||
} |
|||
|
|||
module.exports = { |
|||
error: error, |
|||
warn: warn, |
|||
info: info, |
|||
debug: debug, |
|||
morganConfiguration: morganConfiguration, |
|||
}; |
|||
|
@ -0,0 +1,15 @@ |
|||
// this safe handler is used to wrap our api methods
|
|||
// so that we always fallback and return an exception if there is an error
|
|||
// inside of an async function
|
|||
// Mostly copied from vault/server/utils/safeHandler.js
|
|||
function safeHandler(handler) { |
|||
return async (req, res, next) => { |
|||
try { |
|||
return await handler(req, res, next); |
|||
} catch (err) { |
|||
return next(err); |
|||
} |
|||
}; |
|||
} |
|||
|
|||
module.exports = safeHandler; |
@ -0,0 +1,86 @@ |
|||
const validator = require('validator'); |
|||
|
|||
const ValidationError = require('models/errors.js').ValidationError; |
|||
|
|||
// Max length is listed here,
|
|||
// https://github.com/lightningnetwork/lnd/blob/fd1f6a7bc46b1e50ff3879b8bd3876d347dbb73d/channeldb/invoices.go#L84
|
|||
const MAX_MEMO_LENGTH = 1024; |
|||
const MIN_PASSWORD_LENGTH = 12; |
|||
|
|||
function isAlphanumeric(string) { |
|||
|
|||
isDefined(string); |
|||
|
|||
if (!validator.isAlphanumeric(string)) { |
|||
throw new ValidationError('Must include only alpha numeric characters.'); |
|||
} |
|||
} |
|||
|
|||
function isAlphanumericAndSpaces(string) { |
|||
|
|||
isDefined(string); |
|||
|
|||
if (!validator.matches(string, '^[a-zA-Z0-9\\s]*$')) { |
|||
throw new ValidationError('Must include only alpha numeric characters and spaces.'); |
|||
} |
|||
} |
|||
|
|||
function isBoolean(value) { |
|||
if (value !== true && value !== false) { |
|||
throw new ValidationError('Must be true or false.'); |
|||
} |
|||
} |
|||
|
|||
function isDecimal(amount) { |
|||
if (!validator.isDecimal(amount)) { |
|||
throw new ValidationError('Must be decimal.'); |
|||
} |
|||
} |
|||
|
|||
function isDefined(object) { |
|||
if (object === undefined) { |
|||
throw new ValidationError('Must define variable.'); |
|||
} |
|||
} |
|||
|
|||
function isMinPasswordLength(password) { |
|||
if (password.length < MIN_PASSWORD_LENGTH) { |
|||
throw new ValidationError('Must be ' + MIN_PASSWORD_LENGTH + ' or more characters.'); |
|||
} |
|||
} |
|||
|
|||
function isPositiveInteger(amount) { |
|||
if (!validator.isInt(amount + '', { gt: 0 })) { |
|||
throw new ValidationError('Must be positive integer.'); |
|||
} |
|||
} |
|||
|
|||
function isPositiveIntegerOrZero(amount) { |
|||
if (!validator.isInt(amount + '', { gt: -1 })) { |
|||
throw new ValidationError('Must be positive integer.'); |
|||
} |
|||
} |
|||
|
|||
function isString(object) { |
|||
if (typeof object !== 'string') { |
|||
throw new ValidationError('Object must be of type string.'); |
|||
} |
|||
} |
|||
|
|||
function isValidMemoLength(string) { |
|||
if (Buffer.byteLength(string, 'utf8') > MAX_MEMO_LENGTH) { |
|||
throw new ValidationError('Must be less than ' + MAX_MEMO_LENGTH + ' bytes.'); |
|||
} |
|||
} |
|||
|
|||
module.exports = { |
|||
isAlphanumeric, |
|||
isAlphanumericAndSpaces, |
|||
isBoolean, |
|||
isDecimal, |
|||
isMinPasswordLength, |
|||
isPositiveInteger, |
|||
isPositiveIntegerOrZero, |
|||
isString, |
|||
isValidMemoLength, |
|||
}; |
Loading…
Reference in new issue