├── .dockerignore
├── .eslintignore
├── .eslintrc.json
├── .github
└── workflows
│ ├── get-version.js
│ ├── release-bot.yml
│ ├── release-prepare.sh
│ ├── release-success.sh
│ ├── release.yml
│ └── test.yml
├── .gitignore
├── .idea
├── .name
├── codeStyles
│ └── codeStyleConfig.xml
├── encodings.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── jsLibraryMappings.xml
├── jsLinters
│ └── eslint.xml
├── misc.xml
├── modules.xml
├── vcs.xml
├── watcherTasks.xml
└── webResources.xml
├── .npmignore
├── CHANGELOG.md
├── Dockerfile
├── Privacy.md
├── Privacy_en.md
├── README.md
├── Terms.md
├── Terms_en.md
├── bin
├── database
├── plugin
└── user
├── docker-compose.yml
├── helpers
├── config.js
├── database.js
├── errorResponse.js
├── httpRequestHandler.js
├── importer
│ ├── csv.js
│ ├── index.js
│ ├── mt-940.js
│ └── ofx.js
├── keychain.js
├── log.js
├── plugin
│ ├── index.js
│ ├── instance.js
│ ├── runner.js
│ └── tools.js
├── repository.js
├── server.js
├── socketRequestHandler.js
└── socketSession.js
├── logic
├── _.js
├── account.js
├── budget-guess.js
├── budget.js
├── category.js
├── component.js
├── document.js
├── import.js
├── index.js
├── payee.js
├── plugin-config.js
├── plugin-instance.js
├── plugin-store.js
├── plugin.js
├── portion.js
├── session.js
├── setting.js
├── summary.js
├── transaction.js
├── unit.js
└── user.js
├── migrations
├── 2017-10-01_04-00_kickstart.js
├── 2019-07-28_15-02_terms.js
├── 2019-07-30_14-57_ubud.js
├── 2019-08-03_19-00_payee-set-null.js
├── 2019-09-09_11-22_keychain-unlock-key.js
├── 2019-09-20_09-59_learnings.js
├── 2019-09-21_16-27_learnings.js
├── 2020-07-30_20-59_indices.js
└── 2020-08-09_12-04_recalculate-summaries.js
├── models
├── account.js
├── budget.js
├── category.js
├── document.js
├── index.js
├── learning.js
├── payee.js
├── plugin-config.js
├── plugin-instance.js
├── plugin-store.js
├── portion.js
├── session.js
├── setting.js
├── share.js
├── summary.js
├── transaction.js
├── unit.js
└── user.js
├── package-lock.json
├── package.json
├── release.config.js
├── server.js
└── test
└── migrations.js
/.dockerignore:
--------------------------------------------------------------------------------
1 | .git
2 | .github
3 | node_modules
4 | code-visualization
5 | .gitignore
6 | .npmignore
7 | .releaserc
8 | .docker-compose.yml
9 | *.md
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | code-visualization/**
2 | coverage/**
3 | node_modules/**
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "node": true,
4 | "es6": true,
5 | "mocha": true
6 | },
7 | "parserOptions": {
8 | "ecmaVersion": 2017,
9 | "sourceType": "module"
10 | },
11 | "plugins": [
12 | "security"
13 | ],
14 | "extends": [
15 | "eslint:recommended",
16 | "plugin:security/recommended",
17 | "plugin:node/recommended"
18 | ],
19 | "rules": {
20 | "indent": "error",
21 | "linebreak-style": "error",
22 | "no-console": "error",
23 | "no-extra-parens": [
24 | "error",
25 | "all",
26 | {
27 | "nestedBinaryExpressions": false
28 | }
29 | ],
30 | "no-loss-of-precision": "error",
31 | "no-unreachable-loop": "error",
32 | "node/callback-return": "warn",
33 | "node/exports-style": "error",
34 | "node/global-require": "off",
35 | "node/handle-callback-err": "error",
36 | "node/no-new-require": "error",
37 | "node/no-path-concat": "error",
38 | "node/no-process-env": "error",
39 | "node/no-process-exit": "error",
40 | "node/prefer-global/buffer": "error",
41 | "node/prefer-global/console": "error",
42 | "node/prefer-global/process": "error",
43 | "node/prefer-global/text-decoder": "error",
44 | "node/prefer-global/text-encoder": "error",
45 | "node/prefer-global/url": "error",
46 | "node/prefer-global/url-search-params": "error",
47 | "node/prefer-promises/dns": "error",
48 | "node/prefer-promises/fs": "error",
49 | "quotes": [
50 | "error",
51 | "single"
52 | ],
53 | "security/detect-object-injection": "off",
54 | "require-atomic-updates": "off",
55 | "semi": "error"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/.github/workflows/get-version.js:
--------------------------------------------------------------------------------
1 | (async () => {
2 |
3 | // https://github.com/semantic-release/semantic-release/issues/753#issuecomment-550977000
4 | const { default: semanticRelease } = await import('semantic-release');
5 | const result = await semanticRelease({dryRun: true});
6 |
7 |
8 | if (result) {
9 | const {writeFileSync} = require('fs');
10 | writeFileSync('./version', result.nextRelease.version);
11 | writeFileSync('./artifact/release.json', JSON.stringify(result.nextRelease, null, ' '));
12 | } else {
13 | process.exit(1);
14 | }
15 | })();
16 |
--------------------------------------------------------------------------------
/.github/workflows/release-bot.yml:
--------------------------------------------------------------------------------
1 | name: ReleaseBot
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches: ['develop']
7 | schedule:
8 | - cron: '25 8 * * 3'
9 |
10 | jobs:
11 | release-bot:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: ☁️ Checkout Project
15 | uses: actions/checkout@v3
16 | - name: ☁️ Checkout ReleaseBot
17 | uses: actions/checkout@v3
18 | with:
19 | repository: sebbo2002/release-bot
20 | path: ./.actions/release-bot
21 | - name: 📦 Install Dependencies
22 | run: npm ci
23 | working-directory: ./.actions/release-bot
24 | - name: 🤖 Run ReleaseBot
25 | uses: ./.actions/release-bot
26 | with:
27 | token: ${{ secrets.GITHUB_TOKEN }}
28 |
--------------------------------------------------------------------------------
/.github/workflows/release-prepare.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ASSETVERSION=$(cat ./version/release.json| jq .version -r)
4 |
5 | # CHeck if versions match
6 | if [ "$VERSION" != "$ASSETVERSION" ]; then
7 | echo "Error: Version in assets and semantic-release version mismatch!";
8 | echo " - Asset Version: $ASSETVERSION";
9 | echo " - semantic-release Version: $VERSION";
10 | exit 1;
11 | fi
12 |
13 | sentry-cli releases new -p server "${VERSION}"
14 |
--------------------------------------------------------------------------------
/.github/workflows/release-success.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | npx sentry-cli releases set-commits "${VERSION}" --auto
4 | npx sentry-cli releases finalize "${VERSION}"
5 |
6 | curl -X "POST" "https://beacon.ubud.club/webhooks/update-components/${NOTIFY_WEBHOOK_SECRET}"
7 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | push:
4 | branches:
5 | - develop
6 | - main
7 |
8 | jobs:
9 | generate-version:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v4
13 | - uses: actions/setup-node@v4
14 | with:
15 | cache: 'npm'
16 | node-version: '20'
17 | - run: npm ci
18 | - name: set version
19 | run: |
20 | mkdir ./artifact
21 | node ./.github/workflows/get-version.js
22 | echo "$(jq ".version = \"$(cat ./version)\"" package.json )" > ./artifact/package.json
23 | echo "$(jq ".version = \"$(cat ./version)\"" package-lock.json )" > ./artifact/package-lock.json
24 | rm -f ./version
25 | env:
26 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
27 | GH_TOKEN: ${{ secrets.GH_TOKEN }}
28 | - uses: actions/upload-artifact@v4
29 | with:
30 | name: version
31 | path: ./artifact
32 | if-no-files-found: error
33 |
34 | build-containers:
35 | runs-on: ubuntu-latest
36 | needs: [generate-version]
37 | steps:
38 | - uses: actions/checkout@v4
39 | - uses: actions/download-artifact@v4
40 | with:
41 | name: version
42 | path: version
43 | - name: 🔧 Set up QEMU
44 | uses: actions/download-artifact@v4
45 | - name: 🔧 Set up Buildx
46 | id: buildx
47 | uses: docker/setup-buildx-action@master
48 | - name: 🔐 Login to GitHub Container Registry
49 | uses: docker/login-action@v3
50 | with:
51 | registry: ghcr.io
52 | username: ${{ github.repository_owner }}
53 | password: ${{ secrets.GITHUB_TOKEN }}
54 | - name: 🔐 Login to DockerHub
55 | uses: docker/login-action@v3
56 | with:
57 | username: ${{ secrets.DOCKERHUB_USERNAME }}
58 | password: ${{ secrets.DOCKERHUB_PASSWORD }}
59 | - name: ℹ️ Set Build Variables
60 | id: buildVars
61 | run: |
62 | echo "date=$(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_OUTPUT
63 | echo "version=$(cat ./version/release.json| jq .version -r)" >> $GITHUB_OUTPUT
64 |
65 | export BRANCH=$(git rev-parse --abbrev-ref HEAD)
66 | echo "branch=${BRANCH}" >> $GITHUB_OUTPUT
67 |
68 | if [[ "${BRANCH}" == "main" ]]
69 | then
70 | echo "clientTag=latest" >> $GITHUB_OUTPUT
71 | echo "next=" >> $GITHUB_OUTPUT
72 | else
73 | echo "clientTag=next" >> $GITHUB_OUTPUT
74 | echo "next=1" >> $GITHUB_OUTPUT
75 | fi
76 |
77 | cat $GITHUB_OUTPUT
78 | mv -f ./version/package.json ./
79 | mv -f ./version/package-lock.json ./
80 | rm -rf ./version
81 | - name: 🪄 Build and push
82 | id: docker-build-gh
83 | uses: docker/build-push-action@v5
84 | with:
85 | context: .
86 | github-token: ${{ secrets.GITHUB_TOKEN }}
87 | builder: ${{ steps.buildx.outputs.name }}
88 | platforms: linux/amd64,linux/arm64,linux/i386
89 | pull: true
90 | push: true
91 | tags: |
92 | ghcr.io/${{ github.repository }}:${{ github.sha }}
93 | ghcr.io/${{ github.repository }}:cache-${{ hashFiles('package*.json') }}
94 | labels: |
95 | org.opencontainers.image.authors=${{ github.repository_owner }}
96 | org.opencontainers.image.created=${{ steps.buildVars.outputs.date }}
97 | org.opencontainers.image.ref.name=${{ github.ref }}
98 | org.opencontainers.image.revision=${{ github.sha }}
99 | org.opencontainers.image.source=https://github.com/${{ github.repository }}
100 | cache-from: |
101 | ghcr.io/${{ github.repository }}:cache-${{ hashFiles('package*.json') }}
102 | ghcr.io/${{ github.repository }}:next
103 | build-args: |
104 | NODE_ENV=production
105 | CLIENT_TAG=${{ steps.buildVars.outputs.clientTag }}
106 | NEXT=${{ steps.buildVars.outputs.next }}
107 | BUILD_DATE=${{ steps.buildVars.outputs.date }}
108 | VCS_REF=${{ github.sha }}
109 | VERSION=${{ steps.buildVars.outputs.version }}
110 | - name: 🔄 Push container to DockerHub
111 | id: docker-build-dh
112 | uses: docker/build-push-action@v5
113 | with:
114 | context: .
115 | github-token: ${{ secrets.GITHUB_TOKEN }}
116 | builder: ${{ steps.buildx.outputs.name }}
117 | platforms: linux/amd64,linux/arm64,linux/i386
118 | push: true
119 | tags: ubud/server:${{ github.sha }}
120 | labels: |
121 | org.opencontainers.image.authors=${{ github.repository_owner }}
122 | org.opencontainers.image.created=${{ steps.buildVars.outputs.date }}
123 | org.opencontainers.image.ref.name=${{ github.ref }}
124 | org.opencontainers.image.revision=${{ github.sha }}
125 | org.opencontainers.image.source=https://github.com/${{ github.repository }}
126 | cache-from: |
127 | ghcr.io/${{ github.repository }}:${{ github.sha }}
128 | ghcr.io/${{ github.repository }}:cache-${{ hashFiles('package*.json') }}
129 | cache-to: type=inline
130 | build-args: |
131 | NODE_ENV=production
132 | CLIENT_TAG=${{ steps.buildVars.outputs.clientTag }}
133 | NEXT=${{ steps.buildVars.outputs.next }}
134 | BUILD_DATE=${{ steps.buildVars.outputs.date }}
135 | VCS_REF=${{ github.sha }}
136 | VERSION=${{ steps.buildVars.outputs.version }}
137 |
138 |
139 | release:
140 | runs-on: ubuntu-latest
141 | needs: [build-containers]
142 | steps:
143 | - uses: actions/checkout@v4
144 | - uses: actions/setup-node@v4
145 | with:
146 | cache: 'npm'
147 | node-version: '20'
148 | - name: 🔧 Setup regclient
149 | run: |
150 | mkdir -p "$HOME/.local/bin"
151 | curl -L https://github.com/regclient/regclient/releases/latest/download/regctl-linux-amd64 > "$HOME/.local/bin/regctl"
152 | chmod +x "$HOME/.local/bin/regctl"
153 | echo "$HOME/.local/bin" >> $GITHUB_PATH
154 | - uses: actions/download-artifact@v4
155 | with:
156 | name: version
157 | path: version
158 | - name: install dependencies
159 | run: npm ci
160 | - name: install sentry cli
161 | run: npm install -g @sentry/cli
162 | - name: 🔐 Login to GitHub Container Registry
163 | uses: docker/login-action@v3
164 | with:
165 | registry: ghcr.io
166 | username: ${{ github.repository_owner }}
167 | password: ${{ secrets.GITHUB_TOKEN }}
168 | - name: 🔐 Login to DockerHub
169 | uses: docker/login-action@v3
170 | with:
171 | username: ${{ secrets.DOCKERHUB_USERNAME }}
172 | password: ${{ secrets.DOCKERHUB_PASSWORD }}
173 | - name: semantic-release
174 | run: BRANCH=${GITHUB_REF#refs/heads/} npx semantic-release
175 | env:
176 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
177 | SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
178 | SENTRY_ORG: ubud
179 | SENTRY_PROJECT: server
180 | SENTRY_URL: ${{ secrets.SENTRY_URL }}
181 | DOCKER_LOCAL_IMAGE_DH: ubud/server:${{ github.sha }}
182 | DOCKER_LOCAL_IMAGE_GH: ghcr.io/${{ github.repository }}:${{ github.sha }}
183 | NOTIFY_WEBHOOK_SECRET: ${{ secrets.NOTIFY_WEBHOOK_SECRET }}
184 | GH_TOKEN: ${{ secrets.GH_TOKEN }}
185 | MICROBADGER_WEBHOOK_SECRET: ${{ secrets.MICROBADGER_WEBHOOK_SECRET }}
186 | - name: update develop
187 | if: ${{ github.ref == 'ref/head/main' }}
188 | uses: everlytic/branch-merge@1.1.5
189 | with:
190 | github_token: ${{ secrets.GITHUB_TOKEN }}
191 | source_ref: 'main'
192 | target_branch: 'develop'
193 | commit_message_template: 'Merge branch {source_ref} into {target_branch} [skip ci]'
194 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 | on:
3 | pull_request:
4 | types: [opened, reopened]
5 | push:
6 | branches:
7 | - feature/**
8 | - hotfix/**
9 | - depfu/**
10 |
11 | jobs:
12 | prepare:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v3
16 | - name: set version
17 | run: |
18 | export OLDVERSION=$(npm view @ubud-app/server@next version)
19 | jq -M ".version=\"$OLDVERSION\"" package.json > package.new.json
20 | rm -f package.json
21 | mv package.new.json package.json
22 | - name: 🔐 Login to DockerHub
23 | uses: docker/login-action@v1
24 | with:
25 | username: ${{ secrets.DOCKERHUB_USERNAME }}
26 | password: ${{ secrets.DOCKERHUB_PASSWORD }}
27 | - name: pull dependencies
28 | run: |
29 | docker pull multiarch/qemu-user-static:register
30 | docker pull multiarch/alpine:x86_64-latest-stable
31 | docker pull ubud/server:next-x86_64-base || true
32 | docker pull ubud/server:${{ github.sha }}-test-container || true
33 | - name: register quemu user static
34 | run: docker run --rm --privileged multiarch/qemu-user-static:register --reset
35 | - name: build test docker container
36 | run: |
37 | docker build \
38 | --target build-container \
39 | --build-arg BASEIMAGE="multiarch/alpine:x86_64-latest-stable" \
40 | --build-arg NODE_ENV="develop" \
41 | --build-arg CLIENT_TAG="next" \
42 | --build-arg NEXT="1" \
43 | --cache-from ubud/server:${{ github.sha }}-test-container \
44 | --cache-from ubud/server:next-x86_64-base \
45 | -t "ubud/server:${{ github.sha }}-test-container" .
46 | - name: push
47 | run: docker push "ubud/server:${{ github.sha }}-test-container"
48 | check:
49 | runs-on: ubuntu-latest
50 | needs: [prepare]
51 | steps:
52 | - name: run checks
53 | run: docker run --rm ubud/server:${{ github.sha }}-test-container npm run check
54 | test-mysql:
55 | runs-on: ubuntu-latest
56 | needs: [prepare]
57 | steps:
58 | - name: run tests
59 | run: |
60 | docker run -d --rm --name "database" \
61 | -e "MYSQL_ROOT_PASSWORD=**********" \
62 | -e "MYSQL_USER=ubud" \
63 | -e "MYSQL_PASSWORD=**********" \
64 | -e "MYSQL_DATABASE=ubud" \
65 | mariadb:latest
66 | sleep 10
67 | docker run --rm --link database \
68 | -e "DATABASE=mysql://ubud:**********@database/ubud" \
69 | ubud/server:${{ github.sha }}-test-container npm run test
70 | docker stop database
71 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /code-visualization
3 |
--------------------------------------------------------------------------------
/.idea/.name:
--------------------------------------------------------------------------------
1 | ubud-server
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/jsLibraryMappings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/jsLinters/eslint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/watcherTasks.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/webResources.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /code-visualization
3 | /.idea
4 | /.github
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [0.6.1](https://github.com/ubud-app/server/compare/v0.6.0...v0.6.1) (2021-07-31)
2 |
3 |
4 | ### Bug Fixes
5 |
6 | * **TransactionLogic:** Don't remove lost transaction for single items ([d32d6ec](https://github.com/ubud-app/server/commit/d32d6ecc425adbb55fdcff1b267f4482e73f1337))
7 | * **TransactionLogic:** Fix auto-budget ([66d0f9b](https://github.com/ubud-app/server/commit/66d0f9bbca6336adfb799dc238ecd5e7f7b2dd2b))
8 | * **CSVImporter:** Fix importing wrong date format ([f0a93b1](https://github.com/ubud-app/server/commit/f0a93b12a067f1f269a7de5445005783c33d1df9))
9 | * **CSVImporter:** Fix importing wrong date format ([933ac78](https://github.com/ubud-app/server/commit/933ac78a963d0e55ca9c30765fb25ee10ff07493))
10 | * **CSV Import:** Handle column "Wertstellung" with DD.MM.YYYY values ([f72853b](https://github.com/ubud-app/server/commit/f72853b6c5af3d2a34f211fd4af86538514b998f))
11 | * **Dockerfile:** replace apk from "nodejs-npm" to "npm" ([a5e4809](https://github.com/ubud-app/server/commit/a5e4809354edb8cec1aa37058897461962e5b64d))
12 | * **Importer:** Trim long memos during import ([c919e93](https://github.com/ubud-app/server/commit/c919e93df367ca6f13803ceefd1999e6e12e02a2))
13 |
14 | # [0.6.0](https://github.com/ubud-app/server/compare/v0.5.0...v0.6.0) (2021-04-25)
15 |
16 |
17 | ### Bug Fixes
18 |
19 | * **CSV Import:** Fix year for DKB credit card imports ([57f8c91](https://github.com/ubud-app/server/commit/57f8c91193bea3f33f0185cc6c4c017dfe0d5445))
20 |
21 |
22 | ### Features
23 |
24 | * **CSV Importer:** Add columns for DKB credit cards ([bad692d](https://github.com/ubud-app/server/commit/bad692db6b20385018fbd5d58f0b808b7724ec71))
25 | * **CSV Importer:** Add support for dkb.de csv export ([e9a86d7](https://github.com/ubud-app/server/commit/e9a86d707f76171ecf626606382ff04bff65be0d))
26 | * **Transactions:** Guess budget for new, synced transactions ([1bb5ee2](https://github.com/ubud-app/server/commit/1bb5ee26edc8f571c9edcb605a1cfdd45d5f34ef))
27 |
28 | # [0.5.0](https://github.com/ubud-app/server/compare/v0.4.0...v0.5.0) (2021-04-13)
29 |
30 |
31 | ### Bug Fixes
32 |
33 | * **Log:** Fix scope.addTag is not a function ([0e98323](https://github.com/ubud-app/server/commit/0e983238bc89b3c5e73285d0dfab33e6c3306a73))
34 |
35 |
36 | ### Features
37 |
38 | * **PluginInstances:** add logging for plugin initialization for debugging purposes ([8dcce61](https://github.com/ubud-app/server/commit/8dcce61b23c3e799262b84458541b1d01e1553bb))
39 | * **bin/server:** Do not allow to create first user with that tool ([25dda15](https://github.com/ubud-app/server/commit/25dda15a18956357659a035ccdde0ec5f54b7b84))
40 |
41 | # [0.4.0](https://github.com/ubud-app/server/compare/v0.3.0...v0.4.0) (2020-12-09)
42 |
43 |
44 | ### Bug Fixes
45 |
46 | * **Dockerfile:** Client version for next channel ([fd7c604](https://github.com/ubud-app/server/commit/fd7c604c79e0f3364f7de8da583ce5ca76054cea))
47 | * **Transactions:** Correct transaction background sync ([25735e7](https://github.com/ubud-app/server/commit/25735e7c76e4f05ad8e6d926692b06e8013a60f7))
48 | * **CI:** Fix CI cache issue ([592aedc](https://github.com/ubud-app/server/commit/592aedcbfcadf05862f587f5840e871756b9167f))
49 | * **Dockerfile:** Fix client version ([6e77d04](https://github.com/ubud-app/server/commit/6e77d04cbcd0d543270c124207606659a792da68))
50 | * **Dockerfile:** Fix client version ([4a6e968](https://github.com/ubud-app/server/commit/4a6e96814d5f22af7f7211e614f5acb55222d1b1))
51 | * **Summaries:** Fix document balances ([45eab5e](https://github.com/ubud-app/server/commit/45eab5e7c29b633e39fa13eb5a7032fbfbe423f8))
52 | * **Accounts:** Handle pluginInstanceId = "null" correctly ([bc9c3ef](https://github.com/ubud-app/server/commit/bc9c3ef3f480e359e86ed126dcb923c1c879e58c))
53 | * **Server:** Serve client again ([de15d7d](https://github.com/ubud-app/server/commit/de15d7dc2e2bb4876652f9f0421cec78557472e0))
54 |
55 |
56 | ### Features
57 |
58 | * **Docker:** Add labels (http://label-schema.org) ([423c544](https://github.com/ubud-app/server/commit/423c544c44611d3a3742c30371a7768e4af6b081))
59 | * **Socket:** Use querystrings for collection filtering ([540f396](https://github.com/ubud-app/server/commit/540f39610a3b02227506815922dca7eca54edbc2))
60 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG BASEIMAGE=alpine:latest
2 | FROM $BASEIMAGE as build-container
3 |
4 | ARG CLIENT_TAG=latest
5 | ENV SENTRY_DSN=$SENTRY_DSN
6 |
7 | RUN apk add --no-cache --update \
8 | nodejs \
9 | npm \
10 | libstdc++ \
11 | make \
12 | gcc \
13 | g++ \
14 | python3 && \
15 | npm install -g --unsafe-perm npm
16 |
17 | COPY package*.json "/@ubud-app/server/"
18 | WORKDIR "/@ubud-app/server"
19 | RUN npm ci
20 |
21 | COPY . "/@ubud-app/server/"
22 | RUN npm i "@ubud-app/client@$CLIENT_TAG" --save-optional --no-audit
23 |
24 |
25 |
26 | FROM $BASEIMAGE
27 |
28 | ARG UID=1000
29 | ARG GID=1000
30 | ARG CLIENT_TAG=latest
31 | ARG NODE_ENV=production
32 | ARG NEXT
33 | ARG SENTRY_DSN
34 |
35 | # Build-time metadata as defined at http://label-schema.org
36 | ARG BUILD_DATE
37 | ARG VCS_REF
38 | ARG VERSION
39 | LABEL org.label-schema.build-date=$BUILD_DATE \
40 | org.label-schema.name="ubud app" \
41 | org.label-schema.description="A small, selfhosted software for personal budgeting." \
42 | org.label-schema.url="https://ubud.club" \
43 | org.label-schema.usage="https://github.com/ubud-app/server#-quick-start" \
44 | org.label-schema.vcs-ref=$VCS_REF \
45 | org.label-schema.vcs-url="https://github.com/ubud-app/server" \
46 | org.label-schema.vendor="Sebastian Pekarek" \
47 | org.label-schema.version=$VERSION \
48 | org.label-schema.schema-version="1.0"
49 |
50 | ENV NODE_ENV=$NODE_ENV
51 | ENV SENTRY_DSN=$SENTRY_DSN
52 | ENV NEXT=$NEXT
53 |
54 | RUN apk add --no-cache --update \
55 | nodejs \
56 | npm && \
57 | npm install -g --unsafe-perm npm && \
58 | addgroup -g $GID ubud && \
59 | adduser -u $UID -G ubud -s /bin/sh -D ubud
60 |
61 | COPY --from=build-container "/@ubud-app" "/@ubud-app"
62 |
63 | RUN ln -s "/@ubud-app/server/bin/database" "/usr/local/bin/ubud-db" && \
64 | ln -s "/@ubud-app/server/bin/plugin" "/usr/local/bin/ubud-plugin" && \
65 | ln -s "/@ubud-app/server/bin/user" "/usr/local/bin/ubud-user" && \
66 | ln -s "/@ubud-app/server/server.js" "/usr/local/bin/ubud-server" && \
67 | chown -R ubud:ubud /@ubud-app/server && \
68 | chown -R ubud:ubud /@ubud-app/server/node_modules && \
69 | chown -R ubud:ubud /@ubud-app/server/package.json && \
70 | chown -R ubud:ubud /@ubud-app/server/package-lock.json
71 |
72 | USER ubud
73 | WORKDIR "/@ubud-app/server"
74 | CMD ubud-server
75 |
--------------------------------------------------------------------------------
/Privacy.md:
--------------------------------------------------------------------------------
1 | # Datenschutzerklärung
2 | [**de**|[en](./Privacy_en.md)]
3 |
4 | ## Verantwortlicher für die Datenverarbeitung
5 | Diese Datenschutzhinweise gelten für die Datenverarbeitung durch Sebastian Pekarek, Lindenstr. 75 B, 10969 Berlin (nachfolgend Betreiber genannt).
6 |
7 | ## Datenverarbeitung
8 | Grundsätzlich findet keine Datenverarbeitung personenbezogener Daten durch den Betreiber statt.
9 |
10 | ### Beacon
11 | Aus statistischen Gründen werden in regelmäßigen Intervallen nicht-personenbezogene Daten an den Betreiber gemeldet. Diese Daten sind:
12 |
13 | - Instanz-ID (zufällige, eindeutige ID der Instanz)
14 | - Versionen installierter ubud-Komponenten (z.B. Server oder Web-Client)
15 | - node.js Version
16 | - npm Version
17 | - Prozessor-Typ
18 | - Betriebssystem-Typ
19 | - Anzahl User auf der Instanz
20 | - Anzahl Dokumente auf der Instanz
21 | - installierte Plugins
22 | - Name des npm-Moduls
23 | - Version
24 | - Anzahl angelegter Accounts (falls Account-Plugin)
25 | - Anzahl angelegter Ziele (falls Goal-Plugin)
26 |
27 | Bei der Verwendung der Plugin-Suche wird die Eingabe Klartext an den Betreiber übermittelt, um entsprechende Ergebisse ausliefern zu können.
28 |
29 | Der Server des Betreibers wird von Contabo GmbH, Aschauer Straße 32a, 81549 München gehostet. Eine entsprechende Vereinbarung zur Auftragsverarbeitung wurde mit Contabo geschlossen. Jeder Zugriff auf diesen Server erzeugt einen Eintrag im sogenannten Access-Log. Dabei wird die öffentliche IP-Adresse anonymisiert, sodass Zugriffe nicht nachträglich einer bestimmten Person zuzuordnen sind. Die Datenverarbeitung erfolgt auf der Rechtsgrundlage des Artikel 6 Abs. 1 S. 1 lit. f. Berechtigtes Interesse des Betreibers ist die Auswertung, insbesondere um die Anzahl der Nutzer und die Verbreitung der Software nachzuvollziehen.
30 |
31 | ### Sentry
32 | Ausnahme von dem obigen Grundsatz bildet die Nutzung von Sentry. Diese Software wird verwendet, um bisher unbekannte Fehler in den Software-Komponenten aufzuspüren. Die Erhebung und Speicherung der personenbezogen Daten erfolgt aufgrund der Rechtsgrundlage des Artikel 6 Abs. 1 S. 1 lit. b und f auf dem Server des Betreibers innerhalb der EU. Berechtigtes Interesse des Betreibers ist die Lösung von Fehlern in der Software. Die zu diesem Zwecke erhobenen und gespeicherten Daten werden nach spätestens 14 Tagen gelöscht.
33 |
34 | ## Betroffenenrechte
35 |
36 | ### 1. Auskunft
37 | Auf Wunsch erhalten Sie jederzeit unentgeltlich Auskunft über alle personenbezogenen Daten, die wir über Sie gespeichert haben.
38 |
39 | ### 2. Berichtigung, Löschung, Einschränkung der Verarbeitung (Sperrung), Widerspruch
40 | Sollten Sie mit der Speicherung Ihrer personenbezogenen Daten nicht mehr einverstanden oder sollten diese unrichtig geworden sein, werden wir auf eine entsprechende Weisung hin die Löschung oder Sperrung Ihrer Daten veranlassen oder die notwendigen Korrekturen vornehmen (soweit dies nach dem geltenden Recht möglich ist). Gleiches gilt, sofern wir Daten künftig nur noch einschränkend verarbeiten sollen.
41 |
42 | ### 3. Datenübertragbarkeit
43 | Auf Antrag stellen wir Ihnen Ihre Daten in einem gängigen, strukturierten und maschinenlesbaren Format bereit, so dass Sie die Daten auf Wunsch einem anderen Verantwortlichen übermitteln können.
44 |
45 | ### 4. Beschwerderecht
46 | Es besteht ein Beschwerderecht bei der zuständigen Aufsichtsbehörde:
47 | (https://www.bfdi.bund.de/DE/Infothek/Anschriften_Links/anschriften_links-node.html).
48 |
49 | ### 5. Widerrufsrecht bei Einwilligungen mit Wirkung für die Zukunft
50 | Erteilte Einwilligungen können Sie mit Wirkung für die Zukunft jederzeit widerrufen. Durch Ihren Widerruf wird die Rechtmäßigkeit der Verarbeitung bis zum Zeitpunkt des Widerrufs nicht berührt.
51 |
52 | ### 6. Einschränkungen
53 | Daten, bei denen wir nicht in der Lage sind die betroffene Person zu identifizieren, bspw. wenn diese zu Analysezwecken anonymisiert wurden, sind nicht von den vorstehenden Rechten umfasst. Auskunft, Löschung, Sperrung, Korrektur oder Übertragung an ein anderes Unternehmen sind in Bezug auf diese Daten ggf. möglich, wenn Sie uns zusätzliche Informationen, die uns eine Identifizierung erlauben, bereitstellen.
54 |
55 | ### 7. Ausübung Ihrer Betroffenenrechte
56 | Bei Fragen zur Verarbeitung Ihrer personenbezogenen Daten, bei Auskünften, Berichtigung, Sperrung, Widerspruch oder Löschung von Daten oder dem Wunsch der Übertragung der Daten an ein anderes Unternehmen, wenden Sie sich bitte an den Betreiber.
57 |
58 |
--------------------------------------------------------------------------------
/Privacy_en.md:
--------------------------------------------------------------------------------
1 | # Privacy Statement
2 | [[de](./Privacy.md)|**en**]
3 |
4 | ## Person responsible for data processing
5 | This data protection notice applies to data processing by Sebastian Pekarek, Lindenstr. 75 B, 10969 Berlin (hereinafter referred to as operator).
6 |
7 | ## Data processing
8 | In principle, no data processing of personal data takes place by the operator.
9 |
10 | ### Beacon
11 | For statistical reasons, non-personal data are reported to the operator at regular intervals. These data are:
12 |
13 | - Instance ID (random, unique ID of the instance)
14 | - Versions of installed ubud components (e.g. server or web client)
15 | - node.js version
16 | - npm version
17 | - Processor type
18 | - Operating system type
19 | - Number of users on the instance
20 | - Number of documents on the instance
21 | - installed plugins
22 | - Name of the npm module
23 | - version
24 | - Number of accounts created (if account plug-in)
25 | - Number of destinations created (if Goal plugin)
26 |
27 | When using the plugin search, the input plain text is transmitted to the operator in order to be able to deliver corresponding results.
28 |
29 | The operator's server is hosted by Contabo GmbH, Aschauer Straße 32a, 81549 Munich, Germany. A corresponding agreement on order processing was concluded with Contabo. Each access to this server generates an entry in the so-called access log. The public IP address is made anonymous, so that access cannot subsequently be assigned to a specific person. The data processing takes place on the legal basis of Article 6 Paragraph 1 S. 1 lit. f. The legitimate interest of the operator is the evaluation, in particular to track the number of users and the distribution of the software.
30 |
31 | ### Sentry
32 | The exception to the above principle is the use of Sentry. This software is used to detect previously unknown errors in the software components. The collection and storage of personal data takes place on the operator's server within the EU on the basis of the legal basis of Article 6 (1) (1) (b) and (f). The justified interest of the operator is the solution of errors in the software. The data collected and stored for this purpose will be deleted after 14 days at the latest.
33 |
34 | ## Rights concerned
35 |
36 | ### 1. information
37 | Upon request, you will receive information free of charge about all personal data that we have stored about you at any time.
38 |
39 | ### 2. correction, deletion, restriction of processing (blocking), objection
40 | Should you no longer agree with the storage of your personal data or should this have become incorrect, we will arrange for the deletion or blocking of your data or make the necessary corrections on the basis of a corresponding instruction (insofar as this is possible under applicable law). The same applies if we are only to process data in a restrictive manner in future.
41 |
42 | ### 3. data transferability
43 | Upon request, we will provide you with your data in a common, structured and machine-readable format, so that you can transfer the data to another responsible person on request.
44 |
45 | ### 4. right of appeal
46 | There is a right of appeal to the competent supervisory authority:
47 | (https://www.bfdi.bund.de/DE/Infothek/Anschriften_Links/anschriften_links-node.html_Links/anschriften_links-node.html).
48 |
49 | ### 5. right of revocation for consents with effect for the future
50 | You may revoke your consent at any time with effect for the future. Your revocation does not affect the lawfulness of the processing until the time of revocation.
51 |
52 | ### 6. restrictions
53 | Data for which we are unable to identify the data subject, e.g. if anonymized for analysis purposes, are not covered by the above rights. Information, deletion, blocking, correction or transfer to another company may be possible with respect to such data if you provide us with additional information that allows us to identify you.
54 |
55 | ### 7. exercising your rights as a data subject
56 | If you have any questions regarding the processing of your personal data, information, correction, blocking, objection or deletion of data or if you wish the data to be transferred to another company, please contact the operator.
57 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 |
5 |
6 | [](https://github.com/ubud-app/server/actions)
7 | [](https://www.npmjs.com/package/@ubud-app/server)
8 | [](https://www.npmjs.com/package/@ubud-app/server)
9 | [](https://hub.docker.com/r/ubud/server)
10 |
11 | ## 🚨 Warning
12 |
13 | This software is still in a very early stage (early preview). Errors can occur at any time. Therefore ubud should currently not be used productively, but only for testing purposes.
14 |
15 |
16 | ## 🧐 What's this?
17 |
18 | This repository contains the software ubud, a small private software for budgeting. ubud works according to the envelope method and can be extended with plugins, so that turnovers of accounts can be imported automatically. So that your data doesn't buzz around in some clouds, ubud is a self-hosted software. Install ubud on a [Raspberry Pi](https://www.raspberrypi.org/) or on any NAS with docker support.
19 |
20 |
21 | ## 🖼 Screenshot
22 |
23 | 
24 |
25 |
26 | ## 🎉 Features
27 |
28 | - self-hosted software, no private data in the cloud
29 | - web interface optimized for mobile devices
30 | - Budgeting via envelope method
31 | - Synchronization with banks possible with plugins
32 | - Multi-user capable
33 |
34 |
35 | ## 🐳 Quick Start
36 |
37 | The easiest way to test ubud is Docker. If you don't have Docker installed on your system yet, you can do this with [this guide](https://docs.docker.com/install/).
38 |
39 | You need a database where all the data is stored. Currently MySQL and MariaDB are supported. Docker images are currently available for ARM and AMD64.
40 |
41 | ```
42 | # Download docker-compose.yml
43 | wget https://raw.githubusercontent.com/ubud-app/server/develop/docker-compose.yml
44 |
45 | # Edit environment variables
46 | nano docker-compose.yml
47 |
48 | # Start ubud
49 | docker-compose up -d
50 |
51 | # Get initial login credentials
52 | docker logs -f $(docker-compose ps -q ubud) | docker-compose exec -T ubud \
53 | ./node_modules/bunyan/bin/bunyan -o short --color -l info
54 | ```
55 |
56 |
57 | ## 🔧 Configuration
58 | | Environment Variable | Default Value | Description |
59 | |:------- |:------------------- |:------------------ |
60 | |DATABASE|mysql://localhost/ubud|Database Connection URI|
61 | |SENTRY_DSN|-|Sentry DSN, overwrite to use a custom Sentry Instance|
62 | |PORT|8080|Port to listen on|
63 | |DEVELOP|0|Run in develop mode|
64 |
65 |
66 | ## 💬 Feedback & Help
67 |
68 | For the early preview there are no issues possible, because I want to get the software to work well with it. However, questions and feedback can still be passed on. Either via Twitter [@ubudapp](https://twitter.com/ubudapp) or in our [Slack-Channel](https://join.slack.com/t/ubud-app/shared_invite/enQtNzAzNTU0MjM2MzUzLTY5MGRiZDE5ZDAyMDc3NDZkNGZlOGQxMTc2ZjA1NzEwZDk5ODc5YTc4MTg5N2VlYzY0ODViODZkNmQ0YTQ0MDk).
69 |
70 |
71 | ## 🛠 Build a Plugin
72 |
73 | Plugins can be installed via the ubud user interface. These are written in node.js. There are three types of plugins, whereby one plugin can implement several types:
74 |
75 | - Account Plugin: Allows you to synchronize one or more accounts with ubud. Example: Plugin for a bank
76 | - Metadata Plugin: Enables metadata to be automatically added to a transaction. Example: Plugin that automatically splits the transaction between the different products for online shop orders.
77 | - Goal Plugin: Allows you to automatically add saving targets to a document. Example: Synchronize wish lists of an online shop
78 |
79 | The development of a plugin is currently still a bit hairy, since there is still no documentation and no tools to help. If you still don't want to wait, please feel free to contact us via Slack or Twitter.
80 |
81 |
82 | ## ⏱ Roadmap
83 |
84 | During the Early Preview you can find a very rough roadmap on [GitHub](https://github.com/orgs/ubud-app/projects/1).
85 |
86 |
87 | ## 👩⚖️ Legal Stuff
88 |
89 | - [General terms and conditions](https://github.com/ubud-app/server/blob/develop/Terms.md)
90 | - [Privacy Statement](https://github.com/ubud-app/server/blob/develop/Privacy.md)
91 |
--------------------------------------------------------------------------------
/Terms.md:
--------------------------------------------------------------------------------
1 | # Allgemeine Geschäftsbedingungen
2 | [**de**|[en](./Terms_en.md)]
3 |
4 | ## Rechtsverhältnis zwischen Nutzer und Betreiber der Software ubud
5 | Bei der Bereitstellung der Software ubud handelt es sich um eine unentgeltliche bloße Gefälligkeit. Es werden somit keine Rechte und Pflichten des Betreibers der Software ubud begründet. Der Nutzer hingegen erklärt sich bereit, die geltenden Gesetze zu beachten. Verbotene Nutzungsaktivitäten sind zu unterlassen.
6 |
7 | ## Haftungsausschluss
8 |
9 | ### Allgemein
10 | Der Betreiber schließt jegliche Haftung aus. Der Nutzer nimmt zur Kenntnis, dass er nach eigenem Ermessen und auf eigene Gefahr die Software herunterläd, auf seinem Eigentum installiert und verwendet.
11 |
12 | ### Plugins
13 | Bei der Nutzung der Software ubud kannst du Plugins installieren, um eine Anbindung an verschiedene Dritt-Dienste zu ermöglichen. Diese wurden von Dritten oder dem Nutzer entwickelt. Mit der Installation erklärt sich der Nutzer mit folgendem einverstanden:
14 |
15 | - Dienste von Drittanbietern werden nicht vom Betreiber der Software ubud überprüft, empfohlen oder kontrolliert.
16 | - Jegliche Nutzung eines Plugins von Drittanbietern erfolgt auf eigene Gefahr. Eine diesbezügliche Haftung des Betreibers der Software ubud gegenüber Dritten oder dem Nutzer besteht nicht.
17 | - Im Verhältnis Plugin-Entwickler und Nutzer gelten ausschließlich die AGB des Betreibers der Software ubud.
18 | - Einige Dienste von Drittanbietern können den Zugriff auf deine Daten anfordern oder verlangen. Es gilt die Datenschutzerklärung des Plugin-Entwicklers. Der Betreiber der Software ubud hat keine Kontrolle darüber, wie ein Drittanbieter deine Daten verwendet.
19 | - Plugins von Drittanbietern funktionieren möglicherweise nicht ordnungsgemäß.
20 | - In seltenen Fällen behält sich der Betreiber der Software ubud vor, bereits installierte Plugins zu deaktivieren. Dies geschiet nach seinem Ermessen, insbesondere bei Rechtsverstößen, Sicherheits- oder Datenschutzbedenken.
21 |
22 | ## Gewährleistungsausschluss
23 | Der Betreiber schließt jegliche Mängelhaftung aus. Demnach besteht auch kein Anspruch auf Support. Eine lückenlose und fehlerfreie Nutzung der Software ubud kann nicht gewährleistet werden.
24 |
25 | ## Ansprüche und Pflichten von Plugin-Entwicklern
26 | Durch die Bereitstellung eines Plugins werden keinerlei Ansprüche begründet. Weder schuldet der Nutzer noch der Betreiber der Software ubud ein Entgeld. Der Plugin-Entwickler hat keinen Anspruch auf Listung im Plugin-Verzeichnis.
27 |
28 | Dem Plugin-Entwickler ist es nicht gestattet gegenüber dem Nutzer eigene AGB zu stellen. Es gelten auch im Verhältnis Plugin-Entwickler und Nutzer die AGB des Betreibers der Software ubud.
29 |
30 | Plugin-Entwickler verpflichten sich zur Einhaltung der geltenden gesetzlichen Dateschutzbestimmungen sowie aller sonstigen Gesetze.
31 |
32 | ## Urheberrecht
33 | Die Software ist urheberrechtlich geschützt. Kopien sind nur zum Zwecke von Sicherungskopien erlaubt. Die Veröffentlichung wird ausdrücklich untersagt.
34 |
35 | ## Gerichtsstand und Rechtswahl
36 | Als Gerichtsstand wird das Amtsgericht Tempelhof/Kreuzberg vereinbart.
37 |
38 | Es findet deutsches Recht Anwendung.
39 |
40 | ## Änderung dieser AGB
41 | Änderungen dieser AGB werden dem Nutzer spätestens 30 Tage vor Wirksamwerdens in Textform auf der grafischen Benutzeroberfläche dargestellt. Zusätzlich können die AGB auf der Website der Software ubud (https://ubud.club) abgerufen werden. Die Zustimmung des Nutzers gilt als erteilt, wenn eine Nutzung über die 30-tägige Frist erfolgt. Ein Widerspruch ist aus technischen Gründen nicht möglich. Sofern der Nutzer mit der AGB-Änderung nicht einverstanden ist, verbleibt aus technischen Gründen nur die Deinstallation der Software.
42 |
43 | ## Übersetzung
44 | Diese Bedingungen wurden ursprünglich in deutscher Sprache verfasst. Wir haben das Recht, diese Bedingungen in andere Sprachen übersetzen. Im Falle eines Widerspruchs zwischen einer übersetzten Version dieser Bedingungen und der deutschen Version, hat die deutsche Version Vorrang.
45 |
--------------------------------------------------------------------------------
/Terms_en.md:
--------------------------------------------------------------------------------
1 | # General terms and conditions
2 | [[de](./Terms.md)|**en**]
3 |
4 | ## Legal relationship between user and operator of the software ubud
5 | The provision of the software ubud is a free mere favour. Thus, no rights and obligations of the operator of the software ubud are established. The user, on the other hand, agrees to comply with the applicable laws. Prohibited usage activities are to be omitted.
6 |
7 | ## Disclaimer
8 |
9 | ### General
10 | The operator excludes any liability. The user acknowledges that he downloads, installs and uses the software at his own discretion and risk.
11 |
12 | ### Plugins
13 | When using the ubud software, you can install plugins to enable connection to various third-party services. These were developed by third parties or the user. With the installation the user agrees with the following:
14 |
15 | - Third-party plugins and services are not checked, recommended or controlled by the operator of the ubud software.
16 | - Any use of a third-party plug-in is at your own risk. The operator of the ubud software is not liable to third parties or the user in this respect.
17 | - In the relationship between plugin developer and user, only the general terms and conditions of the operator of the software ubud apply.
18 | - Some third-party services may request or demand access to your data. The privacy policy of the plugin developer applies. The operator of the software ubud has no control over how a third party uses your data.
19 | - Third-party plug-ins may not work properly.
20 | - In rare cases, the operator of the ubud software reserves the right to disable plug-ins that are already installed. This is done at its discretion, especially in the event of legal violations, security or privacy concerns.
21 |
22 | ## Disclaimer of Warranty
23 | The operator excludes any liability for defects. Accordingly, there is also no claim to support. A complete and error-free use of the software ubud cannot be guaranteed.
24 |
25 | ## Requirements and duties of plugin developers
26 | The provision of a plugin does not give rise to any claims whatsoever. Neither the user nor the operator of the software owes ubud a fee. The plugin developer has no claim to listing in the plugin directory.
27 |
28 | The plugin developer is not allowed to provide the user with his own terms and conditions. The general terms and conditions of the operator of the software ubud also apply in the relationship between the plugin developer and the user.
29 |
30 | Plugin developers are obliged to comply with the applicable legal data protection regulations and all other laws.
31 |
32 | ## Copyright
33 | The software is protected by copyright. Copies are only allowed for backup purposes. Publication is expressly prohibited.
34 |
35 | ## Place of jurisdiction and choice of law
36 | The place of jurisdiction shall be Tempelhof/Kreuzberg Local Court.
37 |
38 | German law shall apply.
39 |
40 | ## Amendment of these General Terms and Conditions
41 | Amendments to these GTC shall be presented to the user in text form on the graphical user interface at least 30 days before they take effect. In addition, the GTC can be downloaded from the ubud software website (https://ubud.club). The user's consent shall be deemed to have been granted if use is made over the 30-day period. An objection is not possible for technical reasons. If the user does not agree with the change to the GTC, only the uninstallation of the software remains for technical reasons.
42 |
43 | ## Translation
44 | These Terms and Conditions were originally written in German. We have the right to translate these terms into other languages. In the event of a conflict between a translated version of these Terms and the German version, the German version shall prevail.
45 |
--------------------------------------------------------------------------------
/bin/database:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | 'use strict';
3 |
4 | const commander = require('commander');
5 | const LogHelper = require('../helpers/log');
6 | const ConfigHelper = require('../helpers/config');
7 | const DatabaseHelper = require('../helpers/database');
8 |
9 | const log = new LogHelper('bin/database');
10 | const migrator = DatabaseHelper.getMigrator();
11 |
12 |
13 | function run (promise, type) {
14 | promise.then(function (migrations) {
15 | if (type === 'read' && migrations.length) {
16 | log.info('Result: \n - %s', migrations.length, migrations.map(m => m.file).join('\n - '));
17 | }
18 | else if (type === 'read' && !migrations.length) {
19 | log.info('Done, no migrations found.');
20 | }
21 | else if (type === 'reset') {
22 | log.info('Done, database reset complete.');
23 | }
24 | else if (type === 'write' && migrations.length) {
25 | log.info('Done, executed %s migrations.\n - %s', migrations.length, migrations.map(m => m.file).join('\n - '));
26 | }
27 | else {
28 | log.info('Done, no migrations executed.');
29 | }
30 |
31 | process.exit(0);
32 | }).catch(function (error) {
33 | log.error(error);
34 | process.exit(1);
35 | });
36 | }
37 |
38 | commander.version(ConfigHelper.getVersion(), '-V, --version');
39 |
40 | commander
41 | .command('up [id]')
42 | .description('Run pending migrations till ')
43 | .action(function (id) {
44 | if (id !== true) {
45 | run(migrator.up({
46 | to: id
47 | }), 'write');
48 | } else {
49 | run(migrator.up(), 'write');
50 | }
51 | });
52 |
53 | commander
54 | .command('down [id]')
55 | .description('Run pending migrations down to ')
56 | .action(function (id) {
57 | if (id !== true) {
58 | run(migrator.down({
59 | to: id
60 | }), 'write');
61 | }
62 | });
63 |
64 | commander
65 | .command('pending')
66 | .description('List pending migrations')
67 | .action(function () {
68 | run(migrator.pending(), 'read');
69 | });
70 |
71 | commander
72 | .command('list')
73 | .description('List executed migrations')
74 | .action(function () {
75 | run(migrator.executed(), 'read');
76 | });
77 |
78 | commander
79 | .command('reset')
80 | .description('Reset database')
81 | .action(function () {
82 | run(DatabaseHelper.reset(), 'reset');
83 | });
84 |
85 | commander.parse(process.argv);
--------------------------------------------------------------------------------
/bin/plugin:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | 'use strict';
3 |
4 | const commander = require('commander');
5 | const LogHelper = require('../helpers/log');
6 | const ConfigHelper = require('../helpers/config');
7 | const PluginHelper = require('../helpers/plugin');
8 | const DatabaseHelper = require('../helpers/database');
9 |
10 | const log = new LogHelper('bin/plugin');
11 |
12 | async function run(action) {
13 | try {
14 | await action();
15 | process.exit(0);
16 | }
17 | catch(error) {
18 | log.error(error);
19 | process.exit(1);
20 | }
21 | }
22 |
23 | commander.version(ConfigHelper.getVersion(), '-V, --version');
24 |
25 | commander
26 | .command('list')
27 | .alias('ls')
28 | .description('List all installed plugin instances and their status…')
29 | .action(function () {
30 | run(async function() {
31 | const Table = require('cli-table');
32 | const table = new Table({
33 | head: ['ID', 'Document', 'Plugin']
34 | });
35 |
36 | const plugins = await DatabaseHelper.get('plugin-instance').findAll({
37 | include: [{
38 | model: DatabaseHelper.get('document')
39 | }]
40 | });
41 | plugins.forEach(plugin => {
42 | table.push([
43 | plugin.id,
44 | plugin.document.name,
45 | plugin.type
46 | ]);
47 | });
48 |
49 | log.info(table.toString());
50 | });
51 | });
52 |
53 | commander
54 | .command('install [document] [plugin]')
55 | .alias('i')
56 | .description('Install a plugin, where document is a valid document id and plugin is a `npm install` parameter (name, tarball, etc.)')
57 | .action(function (documentId, plugin) {
58 | run(async function() {
59 | const document = await DatabaseHelper.get('document').findByPk(documentId);
60 | if(!document) {
61 | throw new Error('Document not found, is the id correct?');
62 | }
63 |
64 | await PluginHelper.installPlugin(plugin, document, {dontLoad: true});
65 | log.info('Plugin installed successfully. Please restart ubud-server to apply changes.');
66 | });
67 | });
68 |
69 | commander.parse(process.argv);
--------------------------------------------------------------------------------
/bin/user:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | 'use strict';
3 |
4 | const commander = require('commander');
5 | const LogHelper = require('../helpers/log');
6 | const ConfigHelper = require('../helpers/config');
7 | const DatabaseHelper = require('../helpers/database');
8 | const ErrorResponse = require('../helpers/errorResponse');
9 |
10 | const log = new LogHelper('bin/user');
11 |
12 | async function run (action) {
13 | try {
14 | await action();
15 | process.exit(0);
16 | }
17 | catch (error) {
18 | if (error instanceof ErrorResponse) {
19 | log.error(error.toString());
20 | }
21 | else {
22 | log.error(error);
23 | }
24 | process.exit(1);
25 | }
26 | }
27 |
28 | commander.version(ConfigHelper.getVersion(), '-V, --version');
29 |
30 | commander
31 | .command('list')
32 | .alias('ls')
33 | .description('List all users and their permissions')
34 | .action(function () {
35 | run(async function () {
36 | const Table = require('cli-table');
37 | const table = new Table({
38 | head: ['User ID', 'Email', 'Is Admin', 'Document ID', 'Document Name']
39 | });
40 |
41 | const users = await DatabaseHelper.get('user').findAll({
42 | include: [{
43 | model: DatabaseHelper.get('document')
44 | }]
45 | });
46 | users.forEach(user => {
47 | const documents = user.documents.map(d => [d.id.substr(0, 18), d.name]);
48 | const firstDocument = documents.shift() || ['-', '-'];
49 |
50 | table.push([
51 | user.id.substr(0, 18),
52 | user.email,
53 | user.isAdmin ? 'Yes' : 'No',
54 | firstDocument[0],
55 | firstDocument[1]
56 | ]);
57 |
58 | documents.forEach(d => {
59 | table.push([
60 | '',
61 | '',
62 | '',
63 | d[0],
64 | d[1]
65 | ]);
66 | });
67 | });
68 |
69 | log.info(table.toString());
70 | });
71 | });
72 |
73 |
74 | commander
75 | .command('add [email]')
76 | .description('Adds a new user to the system with the given email address.')
77 | .action(function (email) {
78 | run(async function () {
79 | const UserLogic = require('../logic/user');
80 | const count = await DatabaseHelper.get('user').count();
81 | if (count === 0) {
82 | log.error(new Error(
83 | 'Open your browser and login with the credentials which you can find in the logs. ' +
84 | 'Use this tool only to create additional users.'
85 | ));
86 | return;
87 | }
88 |
89 | const {model, secrets} = await UserLogic.create({email}, {session: {user: {isAdmin: true}}});
90 |
91 | log.info('Created User:\nID: %s\nPassword: %s', model.id, secrets.password);
92 | });
93 | });
94 |
95 | commander.parse(process.argv);
96 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.7"
2 | services:
3 | database:
4 | image: "mariadb:latest"
5 | restart: "always"
6 | environment:
7 | MYSQL_ROOT_PASSWORD: "**********"
8 | MYSQL_USER: "ubud"
9 | MYSQL_PASSWORD: "**********"
10 | MYSQL_DATABASE: "ubud"
11 | # volumes:
12 | # - /path/to/persist:/var/lib/mysql
13 |
14 | ubud:
15 | image: "ubud/server:next"
16 | restart: "always"
17 | environment:
18 | DATABASE: "mysql://ubud:**********@database/ubud"
19 | depends_on:
20 | - "database"
21 | expose:
22 | - "8080"
23 | ports:
24 | - "127.0.0.1:8080:8080"
25 |
--------------------------------------------------------------------------------
/helpers/config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | /* eslint-disable node/no-process-env */
3 |
4 | const path = require('path');
5 | const fs = require('fs');
6 |
7 | let version;
8 | let database;
9 | let sentryDSN;
10 | let ui;
11 |
12 |
13 | // get version number
14 | try {
15 | const packageJsonPath = path.resolve(__dirname, '..', 'package.json');
16 |
17 | // eslint-disable-next-line security/detect-non-literal-fs-filename
18 | const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8');
19 | version = JSON.parse(packageJsonContent).version;
20 | }
21 | catch (err) {
22 | version = null;
23 | }
24 |
25 | // Database Connection URI
26 | database = process.env['DATABASE'];
27 | if (!database) {
28 | database = 'mysql://localhost/ubud';
29 | }
30 |
31 | // Sentry DSN
32 | sentryDSN = process.env['SENTRY_DSN'];
33 | if(sentryDSN === undefined) {
34 | sentryDSN = 'https://13a04aa0257b4b739ceec9c73d745d2d@glitch.sebbo.net/6';
35 | }
36 |
37 | // Client / UI
38 | try {
39 |
40 | ui = require('@ubud-app/client'); // eslint-disable-line node/global-require, node/no-missing-require
41 | ui.static = path.resolve(ui.static);
42 |
43 | const packageJsonPath = require.resolve('@ubud-app/client/package.json');
44 | const stats = fs.statSync(packageJsonPath); // eslint-disable-line security/detect-non-literal-fs-filename
45 | ui.timestamp = stats.mtime;
46 |
47 | // eslint-disable-next-line security/detect-non-literal-fs-filename
48 | const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8');
49 | ui.version = JSON.parse(packageJsonContent).version;
50 | }
51 | catch(err) {
52 | // do nothing
53 | }
54 |
55 |
56 | /**
57 | * ConfigHelper
58 | *
59 | * @module helpers/config
60 | * @class ConfigHelper
61 | */
62 | class ConfigHelper {
63 | /**
64 | * Returns the app's version number from package.json
65 | * @returns {String}
66 | */
67 | static getVersion() {
68 | return version;
69 | }
70 |
71 | /**
72 | * Returns the port the app server should bind to
73 | * @returns {Number}
74 | */
75 | static getPort() {
76 | return parseInt(process.env.PORT) || 8080;
77 | }
78 |
79 | /**
80 | * Returns the database connection URI set via the
81 | * `DATABASE` environment variable.
82 | * @returns {String}
83 | */
84 | static getDatabaseURI() {
85 | return database;
86 | }
87 |
88 | /**
89 | * Returns the Sentry DSN (Data Source Name) used to
90 | * remotely submit error messages.
91 | * @returns {String|null}
92 | */
93 | static getSentryDSN() {
94 | return sentryDSN || null;
95 | }
96 |
97 | /**
98 | * True, if app runs in development mode. Set `DEVELOP`
99 | * environment variable, to do so.
100 | * @returns {boolean}
101 | */
102 | static isDev() {
103 | return !!process.env.DEVELOP;
104 | }
105 |
106 | /**
107 | * Returns client paths if client is installed,
108 | * otherwise null.
109 | * @returns {object|null}
110 | */
111 | static getClient() {
112 | return ui;
113 | }
114 |
115 | /**
116 | * True, if app runs next channel instead of latest,
117 | * applies not for plugins.
118 | * @returns {boolean}
119 | */
120 | static isNext() {
121 | return !!process.env.NEXT;
122 | }
123 | }
124 |
125 | module.exports = ConfigHelper;
126 |
--------------------------------------------------------------------------------
/helpers/errorResponse.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | class ErrorResponseHelper extends Error {
4 | /**
5 | * ErrorResponse
6 | *
7 | * @param {number} [status] HTTP Status Code
8 | * @param {string|Error} message
9 | * @param {object} [options]
10 | * @param {Object} [options.attributes] Example: {name: 'Too long!'}
11 | * @param {string} [options.reference}
12 | * @param {Object} [options.extra] Example: {termsUrl: 'https://…'}
13 | */
14 | constructor(status, message, options) {
15 | if (!message) {
16 | message = status;
17 | status = 500;
18 | }
19 |
20 | super(message);
21 | this.name = 'ErrorResponse';
22 | this.status = status;
23 | this.options = options || {};
24 | this.options.attributes = options && options.attributes ? options.attributes : {};
25 | this.options.extra = options && options.extra ? options.extra : {};
26 | }
27 |
28 | toJSON () {
29 | return Object.assign({
30 | error: this.status,
31 | message: this.message
32 | }, this.options);
33 | }
34 | }
35 |
36 | module.exports = ErrorResponseHelper;
--------------------------------------------------------------------------------
/helpers/httpRequestHandler.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const ErrorResponse = require('./errorResponse');
4 | const DatabaseHelper = require('./database');
5 | const LogHelper = require('./log');
6 | const log = new LogHelper('HTTPRequestHandler');
7 |
8 |
9 | /**
10 | * HTTPRequestHandlerHelper
11 | *
12 | * @module helpers/httpRequestHandler
13 | * @class HTTPRequestHandlerHelper
14 | */
15 | class HTTPRequestHandler {
16 | /**
17 | * @param {Object} options
18 | * @param {Logic} [options.Logic]
19 | * @param {String} [options.route]
20 | * @param {Object} [options.req]
21 | * @param {Object} [options.res]
22 | */
23 | constructor (options) {
24 | this.Logic = options.Logic;
25 | this.route = options.route;
26 | this.req = options.req;
27 | this.res = options.res;
28 | }
29 |
30 | /**
31 | * Handle the request.
32 | * Requires all options in constructor to be set.
33 | *
34 | * @returns {HTTPRequestHandler}
35 | */
36 | run () {
37 | const res = this.res;
38 |
39 | this.checkSession()
40 | .then(session => this.runLogic(session))
41 | .then(response => this.success(response), error => this.error(error))
42 | .catch(function (err) {
43 | log.error(err);
44 |
45 | try {
46 | res.sendStatus(500);
47 | }
48 | catch (sendErr) {
49 | log.debug(sendErr);
50 | }
51 | });
52 |
53 | return this;
54 | }
55 |
56 | /**
57 | * Checks the Session
58 | * @returns {Promise}
59 | */
60 | async checkSession () {
61 | const auth = require('basic-auth');
62 | const bcrypt = require('bcryptjs');
63 | const moment = require('moment');
64 |
65 | const Logic = this.Logic;
66 | const route = this.route;
67 |
68 | const req = this.req;
69 | let credentials;
70 | try {
71 | credentials = auth(req);
72 | }
73 | catch (err) {
74 | throw new ErrorResponse(401, 'Not able to parse your `Authorization` header…');
75 | }
76 |
77 | if (!credentials) {
78 | throw new ErrorResponse(401, '`Authorization` header missing…');
79 | }
80 | if (!credentials.name) {
81 | throw new ErrorResponse(401, 'Error in `Authorization` header: username / session id empty…');
82 | }
83 | if (!credentials.pass) {
84 | throw new ErrorResponse(401, 'Error in `Authorization` header: password / session secret empty…');
85 | }
86 |
87 | if (Logic.getModelName() === 'session' && route === 'create' && credentials.name.length !== 36) {
88 | return new Promise(function (cb) {
89 | cb(credentials);
90 | });
91 | }
92 |
93 | const session = await DatabaseHelper.get('session').findOne({
94 | where: {
95 | id: credentials.name
96 | },
97 | include: [{
98 | model: DatabaseHelper.get('user')
99 | }]
100 | });
101 | if (!session) {
102 | throw new ErrorResponse(401, 'Not able to authorize: Is session id and secret correct?');
103 | }
104 |
105 | const isSessionCorrect = await bcrypt.compare(credentials.pass, session.secret);
106 | if (!isSessionCorrect) {
107 | throw new ErrorResponse(401, 'Not able to authorize: Is session id and secret correct?');
108 | }
109 |
110 | if (session.mobilePairing && (Logic.getModelName() !== 'session' || req.params[0] !== session.id)) {
111 | throw new ErrorResponse(401, 'Not able to authorize: This is a session for the mobile auth flow only.');
112 | }
113 |
114 | const RepositoryHelper = require('../helpers/repository');
115 | const terms = await RepositoryHelper.getTerms();
116 | if (
117 | session.user.acceptedTermVersion === null ||
118 | session.user.acceptedTermVersion < terms.version - 1 ||
119 | session.user.acceptedTermVersion === terms.version - 1 && moment().isSameOrAfter(terms.validFrom)
120 | ) {
121 | throw new ErrorResponse(401, 'Not able to login: User has not accept the current terms!', {
122 | attributes: {
123 | acceptedTerms: 'Is required to be set to the current term version.'
124 | }
125 | });
126 | }
127 |
128 | return session;
129 | }
130 |
131 | /**
132 | * Runs the logic (Logic.get etc.) for
133 | * the given request
134 | *
135 | * @param {Model} session
136 | * @returns {Promise}
137 | */
138 | runLogic (session) {
139 | const Logic = this.Logic;
140 | const method = 'serve' + this.route.substr(0, 1).toUpperCase() + this.route.substr(1);
141 | const options = {
142 | id: this.req.params[0] || null,
143 | body: this.req.body || {},
144 | params: this.req.query,
145 | session: session,
146 | httpRequest: this.req
147 | };
148 |
149 | return Logic[method](options).catch(e => {
150 | throw e;
151 | });
152 | }
153 |
154 | /**
155 | * Yeah! We need a success response…
156 | * @param {Object|Array} result
157 | */
158 | success (result) {
159 | const res = this.res;
160 |
161 | if (!result) {
162 | res.sendStatus(204);
163 | }
164 | else {
165 | res.send(result);
166 | }
167 | }
168 |
169 | /**
170 | * Oups. We need a error response…
171 | * @param {Error} err
172 | */
173 | error (err) {
174 | const res = this.res;
175 |
176 | if (err instanceof Error && !(err instanceof ErrorResponse)) {
177 | err = new ErrorResponse(500, err, {
178 | reference: log.error(err).id
179 | });
180 | }
181 | if (err instanceof ErrorResponse) {
182 | res.status(err.status).send(err.toJSON());
183 | return;
184 | }
185 |
186 | throw err;
187 | }
188 | }
189 |
190 |
191 | module.exports = HTTPRequestHandler;
192 |
--------------------------------------------------------------------------------
/helpers/importer/csv.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const moment = require('moment');
5 | const TransactionLogic = require('../../logic/transaction');
6 |
7 | const csv2transactionMap = {
8 | time: [
9 | ['Belegdatum', ['DD.MM.YYYY', 'DD.MM.YY']],
10 | ['Buchungstag', ['DD-MM-YY', 'DD.MM.YYYY']],
11 | ['Wertstellung', ['DD-MM-YY', 'DD.MM.YYYY']],
12 | ['Datum', ['DD-MM-YYYY', 'DD.MM.YYYY', 'DD-MM-YY', 'YYYY-MM-DD']],
13 | ['Valutadatum', 'DD-MM-YY']
14 | ],
15 | pluginsOwnPayeeId: [
16 | ['Beguenstigter/Zahlungspflichtiger'],
17 | ['Name'],
18 | ['Transaktionsbeschreibung'],
19 | ['Empfänger'],
20 | ['Auftraggeber / Begünstigter'],
21 | ['Beschreibung']
22 | ],
23 | memo: [
24 | ['Verwendungszweck'],
25 | ['Transaktionsbeschreibung'],
26 | ['Beschreibung']
27 | ],
28 | amount: [
29 | ['Betrag'],
30 | ['Buchungsbetrag'],
31 | ['Betrag (EUR)']
32 | ]
33 | };
34 |
35 |
36 | /**
37 | * CSVImporter
38 | *
39 | * @module helpers/importer/csv
40 | * @class CSVImporter
41 | */
42 | class CSVImporter {
43 | static async check (file) {
44 | return file.mime === 'text/csv' || path.extname(file.name).toLowerCase() === '.csv';
45 | }
46 |
47 | static async parse (file) {
48 | const TransactionModel = TransactionLogic.getModel();
49 | const data = file.data.toString('latin1').split('\n\n').pop();
50 |
51 | const { default: neatCsv } = await import('neat-csv');
52 | let csv = await neatCsv(data, {separator: ';'});
53 | if(Object.keys(csv[0]).length < 2) {
54 | csv = await neatCsv(data, {separator: ','});
55 | }
56 |
57 | return csv.map(row => {
58 | const model = TransactionModel.build();
59 |
60 | Object.entries(csv2transactionMap).forEach(([attr, def]) => {
61 | def.forEach(([possibleColumn, momentFormat]) => {
62 | if (model[attr]) {
63 | return;
64 | }
65 |
66 | if (row[possibleColumn] && attr === 'time') {
67 | const momentFormats = Array.isArray(momentFormat) ?
68 | momentFormat.filter(f => f.length === row[possibleColumn].length) :
69 | [momentFormat];
70 |
71 | const time = moment(row[possibleColumn], momentFormats);
72 | if (time && time.isValid()) {
73 | model[attr] = time.toJSON();
74 | }
75 | }
76 | else if (row[possibleColumn] && attr === 'amount') {
77 | const amount = parseInt(row[possibleColumn].replace(/,|\./g, ''), 10);
78 | if (!isNaN(amount) && amount !== 0) {
79 | model[attr] = amount;
80 | }
81 | if (amount === 0) {
82 | return;
83 | }
84 | }
85 | else if (typeof row[possibleColumn] === 'string') {
86 | model[attr] = row[possibleColumn].trim();
87 | }
88 | });
89 |
90 | // Some banks add transactions with an amount of
91 | // 0 to send messages to their customers…
92 | if (model[attr] === undefined && attr !== 'amount') {
93 | throw new Error(
94 | 'Unable to import CSV: no value found for `' + attr + '`, parsed this data: ' +
95 | JSON.stringify(row, null, ' ')
96 | );
97 | }
98 | });
99 |
100 | if(!model.amount) {
101 | return null;
102 | }
103 |
104 | return model;
105 | }).filter(Boolean);
106 | }
107 | }
108 |
109 |
110 | module.exports = CSVImporter;
111 |
--------------------------------------------------------------------------------
/helpers/importer/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 |
4 | /**
5 | * ImporterHelper
6 | *
7 | * @module helpers/importer
8 | * @class ImporterHelper
9 | */
10 | class ImporterHelper {
11 | static initialize () {
12 | if(this.initialized) {
13 | return;
14 | }
15 |
16 | this.importers = [
17 | require('./csv'),
18 | require('./mt-940'),
19 | require('./ofx')
20 | ];
21 |
22 | this.initialized = true;
23 | }
24 |
25 | /**
26 | * Parse the given file
27 | * @param {Model} account AccountModel
28 | * @param {object} file
29 | * @param {String} file.name
30 | * @param {Buffer} file.data
31 | * @param {String} file.mime
32 | * @returns {Promise>}
33 | */
34 | static async parse (account, file) {
35 | this.initialize();
36 |
37 | let myImporter;
38 | for(const importerId in this.importers) {
39 | const importer = this.importers[importerId];
40 | const usable = await importer.check(file);
41 |
42 | if(usable) {
43 | myImporter = importer;
44 | break;
45 | }
46 | }
47 | if(!myImporter) {
48 | throw new Error('Unable to import file `' + file.name + '`: no parser found for file');
49 | }
50 |
51 | const transactions = await myImporter.parse(file);
52 | return transactions.map(t => {
53 | t.accountId = account.id;
54 |
55 | t.memo = t.memo || null;
56 | if(t.memo && t.memo.length > 512) {
57 | t.memo = t.memo.substr(0, 511) + '…';
58 | }
59 |
60 | return t;
61 | });
62 | }
63 | }
64 |
65 |
66 | module.exports = ImporterHelper;
67 |
--------------------------------------------------------------------------------
/helpers/importer/mt-940.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const mt940 = require('mt940-js');
5 | const moment = require('moment');
6 | const TransactionLogic = require('../../logic/transaction');
7 |
8 |
9 | /**
10 | * MT940Importer
11 | *
12 | * @module helpers/importer/mt-940
13 | * @class MT940Importer
14 | */
15 | class MT940Importer {
16 | static async check (file) {
17 | return path.extname(file.name).toLowerCase() === '.sta';
18 | }
19 |
20 | static async parse (file) {
21 | const TransactionModel = TransactionLogic.getModel();
22 | const transactions = [];
23 | const data = await mt940.read(file.data);
24 |
25 | data.forEach(item => {
26 | item.transactions.forEach(transaction => {
27 | transactions.push(
28 | TransactionModel.build({
29 | time: moment(transaction.valueDate, 'YYYY-MM-DD').toJSON(),
30 | memo: transaction.description,
31 | amount: parseInt(transaction.amount.toString().replace(/,|\./, ''), 10) * (transaction.isExpense ? -1 : 1),
32 | pluginsOwnPayeeId: null
33 | })
34 | );
35 | });
36 | });
37 |
38 | return transactions;
39 | }
40 | }
41 |
42 |
43 | module.exports = MT940Importer;
44 |
--------------------------------------------------------------------------------
/helpers/importer/ofx.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const moment = require('moment');
5 | const TransactionLogic = require('../../logic/transaction');
6 |
7 |
8 | /**
9 | * OFXImporter
10 | *
11 | * @module helpers/importer/ofx
12 | * @class OFXImporter
13 | */
14 | class OFXImporter {
15 | static async check (file) {
16 | try {
17 | // eslint-disable-next-line node/no-missing-require
18 | require('ofx');
19 | }
20 | catch(err) {
21 | return false;
22 | }
23 |
24 | return path.extname(file.name).toLowerCase() === '.ofx';
25 | }
26 |
27 | static async parse (file) {
28 | // ofx is available, as check checks for it…
29 | // eslint-disable-next-line node/no-missing-require
30 | const ofx = require('ofx');
31 | const TransactionModel = TransactionLogic.getModel();
32 |
33 | const transactions = [];
34 | const data = await ofx.parse(file.data.toString());
35 |
36 | const transactionList = this.findTransactionList(data);
37 | if(!transactionList || !transactionList.STMTTRN) {
38 | throw new Error('Unable to import OFX file: Transaction List not found!');
39 | }
40 |
41 | transactionList.STMTTRN.forEach(transaction => {
42 | transactions.push(
43 | TransactionModel.build({
44 | time: moment(transaction.DTPOSTED, 'YYYYMMDD').toJSON(),
45 | memo: transaction.MEMO,
46 | amount: parseInt(transaction.TRNAMT.replace(/,|\./, ''), 10),
47 | pluginsOwnPayeeId: transaction.NAME
48 | })
49 | );
50 | });
51 |
52 | return transactions;
53 | }
54 |
55 | static findTransactionList (data) {
56 | if (typeof data === 'object' && data.BANKTRANLIST) {
57 | return data.BANKTRANLIST;
58 | }
59 | else if (typeof data === 'object') {
60 | return Object.values(data)
61 | .map(child => this.findTransactionList(child))
62 | .find(list => list);
63 | }
64 | else {
65 | return null;
66 | }
67 | }
68 | }
69 |
70 |
71 | module.exports = OFXImporter;
72 |
--------------------------------------------------------------------------------
/helpers/keychain.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const crypto = require('crypto');
4 | const EventEmitter = require('events');
5 |
6 | const DatabaseHelper = require('../helpers/database');
7 | const LogHelper = require('./log');
8 | const log = new LogHelper('KeychainHelper');
9 |
10 | let keychainKey = null;
11 | const events = new EventEmitter();
12 | const algorithm = 'aes-256-ctr';
13 |
14 |
15 | /**
16 | * KeychainHelper
17 | *
18 | * @module helpers/keychain
19 | * @class KeychainHelper
20 | */
21 | class KeychainHelper {
22 |
23 | /**
24 | * Returns true, if the keychain is currently unlocked.
25 | *
26 | * @returns {boolean}
27 | */
28 | static isUnlocked () {
29 | return !!keychainKey;
30 | }
31 |
32 | /**
33 | * Returns true, if the keychain is currently locked.
34 | *
35 | * @returns {boolean}
36 | */
37 | static isLocked () {
38 | return !keychainKey;
39 | }
40 |
41 | /**
42 | * Returns true, if keychain is set up.
43 | * @returns {Promise}
44 | */
45 | static async isSetUp () {
46 | const numberOfUsersWithKeychainKey = await DatabaseHelper.get('user')
47 | .count({
48 | where: {
49 | keychainKey: {
50 | [DatabaseHelper.op('not')]: null
51 | }
52 | }
53 | });
54 |
55 | return !!numberOfUsersWithKeychainKey;
56 | }
57 |
58 | /**
59 | * Returns a promise which resolves when the keychain
60 | * gets unlocked. Instantly resolves, if the database
61 | * is already in unlocked state.
62 | *
63 | * @returns {Promise}
64 | */
65 | static async waitTillUnlocked () {
66 | if (this.isUnlocked()) {
67 | return Promise.resolve();
68 | }
69 |
70 | return new Promise(resolve => {
71 | this.events().once('unlocked', () => resolve());
72 | });
73 | }
74 |
75 | /**
76 | * Returns the EventEmitter instance which reflects
77 | * all keychain events for this server instance.
78 | *
79 | * @returns {EventEmitter}
80 | * @instance
81 | */
82 | static events () {
83 | return events;
84 | }
85 |
86 | /**
87 | * Encrypts the given, json-serializable object and returns
88 | * the encrypted string. Only possible if keychain is unlocked.
89 | * otherwise will throw an error.
90 | *
91 | * @param {string|number|null|object} data
92 | * @returns {String}
93 | */
94 | static async encrypt (data) {
95 | if (this.isLocked()) {
96 | throw new Error('Impossible to encrypt given data: keychain is locked!');
97 | }
98 |
99 | let string;
100 | try {
101 | string = JSON.stringify(data);
102 | }
103 | catch (error) {
104 | throw new Error(`Impossible to encrypt given data: object is not serializable (${error.toString()})`);
105 | }
106 |
107 | return KeychainHelper._encrypt(string, keychainKey);
108 | }
109 |
110 | /**
111 | * Encrypts the given string and returns the encrypted one.
112 | *
113 | * @param {String} input
114 | * @param {String|Hash} key
115 | * @returns {Promise}
116 | * @private
117 | */
118 | static async _encrypt (input, key) {
119 | const secret = crypto.createHash('sha256').update(key).digest();
120 | const iv = crypto.randomBytes(16);
121 |
122 | const cipher = crypto.createCipheriv(algorithm, secret, iv);
123 |
124 | let encrypted = cipher.update(input, 'utf8', 'hex');
125 | encrypted += cipher.final('hex');
126 |
127 | return iv.toString('hex') + '!' + encrypted;
128 | }
129 |
130 | /**
131 | * Decrypts the given string and returns the object.
132 | * Only possible if keychain is unlocked, otherwise
133 | * will throw an error.
134 | *
135 | * @param {String} input
136 | * @returns {String|Number|null|Object}
137 | */
138 | static async decrypt (input) {
139 | if (this.isLocked()) {
140 | throw new Error('Impossible to decrypt given data: keychain is locked!');
141 | }
142 |
143 | const dec = await KeychainHelper._decrypt(input, keychainKey);
144 | return JSON.parse(dec);
145 | }
146 |
147 | /**
148 | * Decrypts the given string and returns the original one.
149 | *
150 | * @param {String} input
151 | * @param {String|Buffer} key
152 | * @returns {Promise}
153 | * @private
154 | */
155 | static async _decrypt (input, key) {
156 | const secret = crypto.createHash('sha256').update(key).digest();
157 | const inputParts = input.split('!', 2);
158 | const iv = Buffer.from(inputParts[0], 'hex');
159 |
160 | const decipher = crypto.createDecipheriv(algorithm, secret, iv);
161 |
162 | let dec = decipher.update(inputParts[1], 'hex', 'utf8');
163 | dec += decipher.final('utf8');
164 |
165 | return dec;
166 | }
167 |
168 | /**
169 | * Try to unlock and initialize the keychain for the given user.
170 | * Password should be checked already. Woun't throw any errors in
171 | * case this is not possible unless`options.force` is set to true.
172 | *
173 | * @param {Sequelize.Model} userModel – UserModel of the user trying to unlock keychain
174 | * @param {String} password – Cleartext password of the user
175 | * @param {Object} [options]
176 | * @param {Boolean} [options.force] – Force unlock. Will throw an error, if unlock is not possible.
177 | * @param {Boolean} [options.dontSave] – If set to true, unlock() wount save model changes in database, you need to
178 | * do this yourself afterwards if this is set.
179 | */
180 | static async unlock (userModel, password, options = {}) {
181 | if (!userModel.isAdmin && options.force) {
182 | throw new Error('Unable to unlock keychain: You are not an admin!');
183 | }
184 | if (!userModel.isAdmin) {
185 | return;
186 | }
187 |
188 | // initialize keychain
189 | if (this.isLocked() && !userModel.keychainKey) {
190 | const isSetUp = await this.isSetUp();
191 | if (!isSetUp) {
192 | const key = crypto.randomBytes(256);
193 | await KeychainHelper.migrateDatabase(key);
194 |
195 | keychainKey = key;
196 | events.emit('unlocked');
197 |
198 | log.info('Keychain set up with new, random secret. Now in unlocked state.');
199 | }
200 | }
201 |
202 | // setup keychainKey for user
203 | if (!userModel.keychainKey && this.isUnlocked()) {
204 | userModel.keychainKey = await KeychainHelper._encrypt(
205 | keychainKey.toString('hex'),
206 | password
207 | );
208 |
209 | if (!options.dontSave) {
210 | await userModel.save();
211 | }
212 |
213 | log.info(`Updated keychain key of user ${userModel.id}. User is now able to unlock keychain.`);
214 | }
215 |
216 | // throw error if unlock is not possible
217 | if (!userModel.keychainKey && this.isLocked() && options.force) {
218 | throw new Error('Unable to unlock keychain: Unlock not possible with this user, unlock with another admin user.');
219 | }
220 |
221 | // do unlock
222 | if (userModel.keychainKey && this.isLocked()) {
223 | const keychainKeyString = await KeychainHelper._decrypt(userModel.keychainKey, password);
224 | keychainKey = Buffer.from(keychainKeyString, 'hex');
225 |
226 | events.emit('unlocked');
227 | log.info(`Keychain unlocked by user ${userModel.id}.`);
228 | }
229 | }
230 |
231 | static async migrateDatabase (key) {
232 | const types = ['plugin-config', 'plugin-store'];
233 | for (let i in types) {
234 | const type = types[i];
235 | const pluginConfigs = await DatabaseHelper.get(type).findAll({
236 | attributes: ['id', 'value']
237 | });
238 |
239 | for (let j in pluginConfigs) {
240 | const model = pluginConfigs[j];
241 | model.value = await KeychainHelper._encrypt(model.value, key);
242 |
243 | await model.save();
244 | }
245 | }
246 | }
247 | }
248 |
249 |
250 | module.exports = KeychainHelper;
--------------------------------------------------------------------------------
/helpers/log.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const bunyan = require('bunyan');
4 | const Sentry = require('@sentry/node');
5 | const _ = require('underscore');
6 | const os = require('os');
7 | const util = require('util');
8 | const http = require('http');
9 | const {v4: uuid} = require('uuid');
10 | const ConfigHelper = require('./config');
11 |
12 | // bunyan logger
13 | const logger = bunyan.createLogger({
14 | name: 'ubud-server',
15 | level: 'trace',
16 | serializers: {req: bunyan.stdSerializers.req}
17 | });
18 |
19 | const globalTags = {};
20 |
21 | // add global tags
22 | globalTags.nodejs = process.version;
23 | globalTags.version = ConfigHelper.getVersion();
24 |
25 |
26 | // initialize sentry instance
27 | if (ConfigHelper.getSentryDSN()) {
28 | Sentry.init({
29 | dsn: ConfigHelper.getSentryDSN(),
30 | release: ConfigHelper.getVersion()
31 | });
32 | }
33 |
34 |
35 | /**
36 | * LogHelper
37 | *
38 | * @module helpers/log
39 | * @class LogHelper
40 | */
41 | class LogHelper {
42 | constructor(module, options) {
43 | this.module = module;
44 | this.options = options || {};
45 | }
46 |
47 |
48 | _log(s) {
49 | const t = {};
50 | let i;
51 |
52 | s = _.extend({}, {
53 | id: 0,
54 | error: null,
55 | time: null,
56 | level: 'log',
57 | module: this.module || null,
58 | param_message: null,
59 | report: false,
60 | request: null,
61 | user: null,
62 | extra: {},
63 | options: {},
64 | callback: null
65 | }, s);
66 | s.options = s.options || {};
67 |
68 | // add Time
69 | s.time = new Date().toGMTString();
70 |
71 | // add custom Tags
72 | _.extend(s.extra, globalTags);
73 |
74 | // add request
75 | if (s.options && s.options.request && s.options.request instanceof http.IncomingMessage) {
76 | s.request = s.options.request;
77 | }
78 |
79 | // add user
80 | if (s.options && s.options.user) {
81 | s.user = s.options.user;
82 | }
83 |
84 | // add path infos
85 | if (s.options && s.options.request) {
86 | s.pathname = s.options.request._parsedUrl.pathname;
87 | }
88 |
89 |
90 | // analyse arguments
91 | if (!s.error && (!s.args || s.args.length === 0)) {
92 | return null;
93 | }
94 | for (i in s.args) {
95 | if (Object.prototype.hasOwnProperty.call(s.args, i)) {
96 | t.argument = s.args[i];
97 | i = parseInt(i, 10);
98 |
99 | if (i === 0 && _.isString(t.argument)) {
100 | s.error = t.argument;
101 | t.isString = 1;
102 | t.variables = [t.argument];
103 | }
104 | else if (i === 0) {
105 | s.error = t.argument;
106 | }
107 | else if (t.argument instanceof http.IncomingMessage) {
108 | s.options.request = t.argument;
109 | }
110 | else if (parseInt(i, 10) === s.args.length - 1 && _.isObject(t.argument)) {
111 | _.extend(s.extra, t.argument);
112 | if (t.isString) {
113 | t.variables.push(t.argument);
114 | }
115 | }
116 | else if (t.isString) {
117 | t.variables.push(t.argument);
118 | }
119 | }
120 | }
121 |
122 |
123 | // generate id
124 | s.id = uuid().substr(0, 32).toUpperCase();
125 |
126 | // replace variables
127 | if (t.isString) {
128 | s.param_message = s.error;
129 | s.error = util.format.apply(null, t.variables);
130 | }
131 |
132 | // sentry
133 | if (s.report && !ConfigHelper.isDev() && ConfigHelper.getSentryDSN()) {
134 | Sentry.withScope(scope => {
135 | scope.setExtra('machine', os.hostname() + ':' + ConfigHelper.getPort());
136 | scope.setTag('module', s.module);
137 | scope.setTag('id', s.id);
138 | scope.setTag('level', s.level);
139 | scope.setUser(s.user);
140 |
141 | Sentry[s.level === 'error' ? 'captureException' : 'captureMessage'](s.error);
142 | scope.clear();
143 | });
144 | }
145 |
146 | // json log
147 | if(
148 | process.mainModule && process.mainModule.filename &&
149 | (
150 | process.mainModule.filename.substr(-13) === '/bin/database' ||
151 | process.mainModule.filename.substr(-11) === '/bin/plugin' ||
152 | process.mainModule.filename.substr(-9) === '/bin/user'
153 | )
154 | ) {
155 | const map = {fatal: 'error', error: 'error', warning: 'warn', info: 'info', debug: 'log'};
156 |
157 | if(s.module !== 'Database') {
158 | console[map[s.level]](s.error); // eslint-disable-line no-console
159 | }
160 | }else {
161 | logger[s.level === 'warning' ? 'warn' : s.level](_.extend({}, s.extra, {
162 | id: s.id,
163 | module: s.module,
164 | username: s.user,
165 | machine: os.hostname() + ':' + ConfigHelper.getPort(),
166 | pathname: s.pathname,
167 | req: s.request
168 | }), s.error);
169 | }
170 |
171 | // Exception
172 | if (s.level === 'fatal' && s.error instanceof Error) {
173 | throw s.error;
174 | }
175 |
176 | return s;
177 | }
178 |
179 |
180 | fatal() {
181 | let myLog = this._log({
182 | args: arguments,
183 | level: 'fatal',
184 | report: true
185 | });
186 |
187 | return myLog;
188 | }
189 |
190 | error() {
191 | return this._log({
192 | args: arguments,
193 | level: 'error',
194 | report: true
195 | });
196 | }
197 |
198 | warn() {
199 | return this._log({
200 | args: arguments,
201 | level: 'warning',
202 | report: true
203 | });
204 | }
205 |
206 | info() {
207 | return this._log({
208 | args: arguments,
209 | level: 'info',
210 | report: false
211 | });
212 | }
213 |
214 | debug() {
215 | return this._log({
216 | args: arguments,
217 | level: 'debug',
218 | report: false
219 | });
220 | }
221 |
222 | log() {
223 | return this._log({
224 | args: arguments,
225 | level: 'debug',
226 | report: false
227 | });
228 | }
229 |
230 | context(method, cb) {
231 | let returned,
232 | error;
233 |
234 | try {
235 | returned = method();
236 | }
237 | catch (err) {
238 | error = this._log({
239 | error: err,
240 | level: 'error',
241 | report: true
242 | });
243 | }
244 |
245 | if (_.isFunction(cb)) {
246 | cb(error || null, returned);
247 | return returned;
248 | }
249 |
250 | return returned;
251 | }
252 |
253 | wrap(method, cb) {
254 | const l = this;
255 | return function () {
256 | return l.context(method, cb);
257 | };
258 | }
259 | }
260 |
261 | // process exit log
262 | if (!ConfigHelper.isDev()) {
263 | process.on('uncaughtException', function (err) {
264 | const log = new LogHelper('LogHelper');
265 |
266 | log.fatal({
267 | error: err,
268 | level: 'error',
269 | module: 'root',
270 | report: true
271 | });
272 | });
273 | }
274 |
275 | module.exports = LogHelper;
276 |
--------------------------------------------------------------------------------
/helpers/plugin/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const EventEmitter = require('events');
4 | const LogHelper = require('../log');
5 | const DatabaseHelper = require('../database');
6 | const PluginInstance = require('./instance');
7 | const log = new LogHelper('PluginHelper');
8 |
9 |
10 | const pluginEvents = new EventEmitter();
11 | let initialized = false;
12 | let plugins = [];
13 |
14 |
15 | /**
16 | * PluginHelper
17 | *
18 | * @module helpers/plugin
19 | * @class PluginHelper
20 | */
21 | class PluginHelper {
22 | static async initialize () {
23 | if (initialized) {
24 | return;
25 | }
26 |
27 | let models;
28 | initialized = true;
29 |
30 | try {
31 | models = await DatabaseHelper.get('plugin-instance').findAll();
32 | }
33 | catch (err) {
34 | log.fatal('Unable to fetch used plugins: %s', err);
35 | throw err;
36 | }
37 |
38 | plugins = models.map(plugin => new PluginInstance(plugin, pluginEvents));
39 | }
40 |
41 | static async listPlugins () {
42 | return plugins;
43 | }
44 |
45 | static async getPlugin (id) {
46 | return plugins.find(plugin => plugin.id() === id) || null;
47 | }
48 |
49 | /**
50 | * installPlugin()
51 | *
52 | * Installs the given plugin for the selected document. For type, all parameters
53 | * specified for `npm install` are valid (see https://docs.npmjs.com/cli/install).
54 | *
55 | *
56 | * ### Sequence
57 | *
58 | * - run npm install
59 | * - Fails: error
60 | *
61 | * - check plugin basics
62 | * - Fails: uninstall plugin + error
63 | *
64 | * - add plugin to database
65 | * - Fails: error
66 | *
67 | * - add plugin to ram db
68 | *
69 | * - check plugin configuration
70 | * - Fails: uninstall plugin + error
71 | * - Valid: go to ready state
72 | * - Invalid: go to waiting for configuration state
73 | *
74 | * @param {string} type Plugin type, for example "@ubud-app/plugin-n26" or "~/my-plugin"
75 | * @param {Sequelize.Model} document
76 | * @param {object} [options]
77 | * @param {boolean} [options.dontLoad] Don't load plugin instance. Method will return null then.
78 | * @returns {Promise.}
79 | */
80 | static async installPlugin (type, document, options) {
81 | options = options || {};
82 |
83 | /*
84 | * npm install
85 | */
86 | type = await this._runPackageInstall(type);
87 | log.debug('%s: installed successfully', type);
88 |
89 |
90 | /*
91 | * run plugin checks
92 | */
93 | try {
94 | await PluginInstance.check(type);
95 | log.debug('%s: checks passed', type);
96 | }
97 | catch (err) {
98 | const count = await DatabaseHelper.get('plugin-instance').count({
99 | where: {type}
100 | });
101 |
102 | if (!count) {
103 |
104 | // remove plugin again
105 | try {
106 | await this._runPackageRemove(type);
107 | log.debug('%s: removed successfully', type);
108 | }
109 | catch (err) {
110 | log.warn('%s: unable to remove plugin: %s', type, err);
111 | }
112 | }
113 |
114 | throw err;
115 | }
116 |
117 |
118 | /*
119 | * add instance to database
120 | */
121 | const model = await DatabaseHelper.get('plugin-instance').create({type, documentId: document.id});
122 | if (options.dontLoad) {
123 | return null;
124 | }
125 |
126 | const instance = new PluginInstance(model, pluginEvents);
127 | plugins.push(instance);
128 |
129 | return instance;
130 | }
131 |
132 |
133 | /**
134 | * removePlugin()
135 | *
136 | * @param {PluginInstance} instance
137 | * @returns {Promise.}
138 | */
139 | static async removePlugin (instance) {
140 | // stop plugin
141 | await instance.destroy();
142 |
143 | // destroy database model
144 | await instance.model().destroy();
145 |
146 | // remove plugin from index
147 | const i = plugins.indexOf(instance);
148 | if (i !== -1) {
149 | plugins.splice(i, 1);
150 | }
151 |
152 | // get package usages by other plugin instances
153 | const usages = await DatabaseHelper.get('plugin-instance').count({
154 | where: {
155 | type: instance.type()
156 | }
157 | });
158 |
159 | // remove package if not used anymore
160 | if (!usages) {
161 | await this._runPackageRemove(instance.type());
162 | }
163 | }
164 |
165 |
166 | /**
167 | * Returns the event object used to transmit all
168 | * plugin events to our sockets…
169 | *
170 | * @returns {EventEmitter}
171 | */
172 | static events () {
173 | return pluginEvents;
174 | }
175 |
176 |
177 | static async _runPackageInstall (type) {
178 | const path = require('path');
179 | let modulePath = PluginHelper._packageDirectory(type);
180 | let res;
181 |
182 | if (!modulePath) {
183 | try {
184 | res = await this._runPackageRunQueue(['npm', 'install', '--ignore-scripts', '--production', type]);
185 | }
186 | catch (err) {
187 | log.error(err);
188 | throw new Error('Unable to install required package via npm`: ' + err.string);
189 | }
190 |
191 | modulePath = PluginHelper._packageDirectory(type);
192 | }
193 |
194 | modulePath = path.dirname(require.resolve(type + '/package.json'));
195 | if (!modulePath) {
196 | throw new Error('Unable to find plugin after installation');
197 | }
198 |
199 | log.debug('Plugin installation done, path is ' + modulePath);
200 | Object.keys(require.cache)
201 | .filter(key => key.substr(0, modulePath.length) === modulePath)
202 | .forEach(key => {
203 | log.debug('Invalidated cached module: ' + key);
204 | delete require.cache[key];
205 | });
206 |
207 | if (res) {
208 | const id = res.split('\n').find(l => l.trim().substr(0, 1) === '+');
209 | if (!id) {
210 | throw new Error(`Plugin installed, but unable to get plugin name. Output was \`${res}\``);
211 | }
212 |
213 | return id.substr(2, id.lastIndexOf('@') - 2).trim();
214 | }
215 |
216 | return type;
217 | }
218 |
219 | static _packageDirectory (type) {
220 | const path = require('path');
221 |
222 | try {
223 | const modulePath = path.dirname(require.resolve(type + '/package.json'));
224 | return modulePath;
225 | }
226 | catch (err) {
227 | return null;
228 | }
229 | }
230 |
231 | static async _runPackageRemove (type) {
232 | await this._runPackageRunQueue(['npm', 'remove', type]);
233 | }
234 |
235 | static async _runPackageRunQueue (command) {
236 | const id = command.join(' ');
237 | log.debug('_runPackageRunQueue: %s: add', id);
238 |
239 | this._runPackageRunQueue.q = this._runPackageRunQueue.q || [];
240 | this._runPackageRunQueue.e = this._runPackageRunQueue.e || new EventEmitter();
241 |
242 | // already in queue?
243 | const i = this._runPackageRunQueue.q.find(i => i[0] === id);
244 | if (i) {
245 | log.debug('_runPackageRunQueue: %s: already in queue', id);
246 | return i[2];
247 | }
248 |
249 | // add to queue
250 | const item = [
251 | id,
252 | command,
253 | new Promise(resolve => {
254 | const l = _id => {
255 | if (_id === id) {
256 | this._runPackageRunQueue.e.off('start', l);
257 | resolve();
258 | }
259 | };
260 | this._runPackageRunQueue.e.on('start', l);
261 | }).then(() => {
262 | log.debug('_runPackageRunQueue: %s: running', id);
263 |
264 | const exec = require('promised-exec');
265 | const escape = require('shell-escape');
266 |
267 | return exec(escape(command));
268 | }).finally(() => {
269 | log.debug('_runPackageRunQueue: %s: done', id);
270 | const i = this._runPackageRunQueue.q.indexOf(item);
271 | if (i >= 0) {
272 | log.debug('_runPackageRunQueue: %s: remove queue item', id);
273 | this._runPackageRunQueue.q.splice(i, 1);
274 | }
275 | if (this._runPackageRunQueue.q.length > 0) {
276 | setTimeout(() => {
277 | log.debug('_runPackageRunQueue: %s: start next item: %s', id, this._runPackageRunQueue.q[0][0]);
278 | this._runPackageRunQueue.e.emit('start', this._runPackageRunQueue.q[0][0]);
279 | }, 5000);
280 | }
281 | })
282 | ];
283 | this._runPackageRunQueue.q.push(item);
284 | log.debug('_runPackageRunQueue: %s: added, now %i items in queue', id, this._runPackageRunQueue.q.length);
285 |
286 | if (this._runPackageRunQueue.q.length === 1) {
287 | log.debug('_runPackageRunQueue: %s: kickstart queue', id);
288 | this._runPackageRunQueue.e.emit('start', this._runPackageRunQueue.q[0][0]);
289 | }
290 |
291 | return item[2];
292 | }
293 | }
294 |
295 |
296 | module.exports = PluginHelper;
297 |
--------------------------------------------------------------------------------
/helpers/plugin/runner.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /* eslint-disable node/no-process-exit, no-process-exit, no-console */
4 |
5 | const PluginTools = require('./tools.js');
6 | module.exports = PluginTools;
7 |
8 | /**
9 | * PluginRunner
10 | *
11 | * @class PluginRunner
12 | */
13 | class PluginRunner {
14 | static async initialize () {
15 | process.title = 'ubud-plugin';
16 |
17 | const job = await this.getJobDescription();
18 | process.title = 'ubud-plugin (' + job.type + ')';
19 | PluginTools._runner = this;
20 | PluginTools._config = job.config;
21 |
22 | try {
23 | job.plugin = require(job.type); // eslint-disable-line security/detect-non-literal-require
24 | }
25 | catch(err) {
26 | err.message = 'Unable to execute plugin: ' + err.message;
27 | throw err;
28 | }
29 |
30 | if (job.method === 'check') {
31 | return this.check(job);
32 | }
33 | else if (job.method === 'getSupported') {
34 | return this.getSupported(job);
35 | }
36 | else if (job.method === 'getConfig') {
37 | return this.getConfig();
38 | }
39 | else if (job.method === 'validateConfig') {
40 | return this.validateConfig(job);
41 | }
42 | else if (job.method === 'getAccounts') {
43 | return this.getAccounts(job);
44 | }
45 | else if (job.method === 'getTransactions') {
46 | return this.getTransactions(job);
47 | }
48 | else if (job.method === 'getMetadata') {
49 | return this.getMetadata(job);
50 | }
51 | else if (job.method === 'getGoals') {
52 | return this.getGoals(job);
53 | }
54 | else {
55 | throw new Error('Unimplemented method: `' + job.method + '`');
56 | }
57 | }
58 |
59 | /**
60 | * Returns a promise which will resolve with the job description
61 | * when received.
62 | *
63 | * @returns {Promise.