Merge pull request #17 from meienberger/tests/auth

Release
This commit is contained in:
Nicolas Meienberger 2022-05-12 19:50:58 +00:00 committed by GitHub
commit bf2192a624
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 2714 additions and 4749 deletions

49
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,49 @@
name: Tipi CI
on:
push:
env:
ROOT_FOLDER: /test
JWT_SECRET: "secret"
jobs:
ci:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 16
- uses: pnpm/action-setup@v2.0.1
name: Install pnpm
id: pnpm-install
with:
version: 7
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
- name: Run tests
run: pnpm -r lint
- name: Run tests
run: pnpm -r test

32
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,32 @@
name: Build & Deploy
on:
push:
branches:
- 'release/**'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Create Tag
id: create_tag
uses: jaywcjlove/create-tag-action@v1.1.5
with:
token: ${{ secrets.GITHUB_TOKEN }}
package-path: ./package.json
- name: Create Release
id: create_release
uses: actions/create-release@latest
if: steps.create_tag.outputs.successful
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.create_tag.outputs.version }}
release_name: ${{ steps.create_tag.outputs.version }}
draft: false
prerelease: false

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
.pnpm-debug.log
.env
.env*
node_modules/

5
.husky/pre-commit Executable file
View file

@ -0,0 +1,5 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
pnpm -r test
pnpm -r lint

View file

@ -1,5 +1,6 @@
{
"name": "File Browser",
"available": true,
"port": 8096,
"id": "filebrowser",
"description": "Reliable and Performant File Management Desktop Sync and File Sharing",

View file

@ -1,5 +1,6 @@
{
"name": "FreshRSS",
"available": true,
"port": 8086,
"id": "freshrss",
"description": "FreshRSS is a self-hosted RSS feed aggregator like Leed or Kriss Feed.\nIt is lightweight, easy to work with, powerful, and customizable.\n\nIt is a multi-user application with an anonymous reading mode. It supports custom tags. There is an API for (mobile) clients, and a Command-Line Interface.\n\nThanks to the WebSub standard (formerly PubSubHubbub), FreshRSS is able to receive instant push notifications from compatible sources, such as Mastodon, Friendica, WordPress, Blogger, FeedBurner, etc.\n\nFreshRSS natively supports basic Web scraping, based on XPath, for Web sites not providing any RSS / Atom feed.\n\nFinally, it supports extensions for further tuning.",

View file

@ -1,5 +1,6 @@
{
"name": "Invidious",
"available": true,
"port": 8095,
"id": "invidious",
"description": "",

View file

@ -1,5 +1,6 @@
{
"name": "Jackett",
"available": true,
"port": 8097,
"id": "jackett",
"description": "Jackett works as a proxy server: it translates queries from apps (Sonarr, Radarr, SickRage, CouchPotato, Mylar3, Lidarr, DuckieTV, qBittorrent, Nefarious etc.) into tracker-site-specific http queries, parses the html or json response, and then sends results back to the requesting software. This allows for getting recent uploads (like RSS) and performing searches.",
@ -7,7 +8,5 @@
"author": "",
"source": "https://github.com/Jackett/Jackett",
"image": "https://avatars.githubusercontent.com/u/15383019?s=200&v=4",
"form_fields": {
}
"form_fields": {}
}

View file

@ -1,5 +1,6 @@
{
"name": "Jellyfin",
"available": true,
"port": 8091,
"id": "jellyfin",
"description": "",

View file

@ -1,5 +1,6 @@
{
"name": "Joplin Server",
"available": true,
"port": 8099,
"id": "joplin",
"description": "",

View file

@ -1,5 +1,6 @@
{
"name": "n8n",
"available": true,
"port": 8094,
"id": "n8n",
"description": "n8n is an extendable workflow automation tool. With a fair-code distribution model, n8n will always have visible source code, be available to self-host, and allow you to add your own custom functions, logic and apps. n8n's node-based approach makes it highly versatile, enabling you to connect anything to everything.",

View file

@ -1,5 +1,6 @@
{
"name": "Nextcloud",
"available": true,
"port": 8083,
"id": "nextcloud",
"description": "Nextcloud is a self-hosted, open source, and fully-featured cloud storage solution for your personal files, office documents, and photos.",

View file

@ -1,23 +1,24 @@
{
"name": "PiHole",
"port": 8081,
"requirements": {
"ports": [53]
},
"id": "pihole",
"description": "",
"short_desc": "",
"author": "",
"source": "",
"image": "https://avatars.githubusercontent.com/u/16827203?s=200&v=4",
"form_fields": {
"password": {
"type": "password",
"label": "Password",
"max": 50,
"min": 3,
"required": true,
"env_variable": "APP_PASSWORD"
}
"name": "PiHole",
"available": true,
"port": 8081,
"requirements": {
"ports": [53]
},
"id": "pihole",
"description": "",
"short_desc": "",
"author": "",
"source": "",
"image": "https://avatars.githubusercontent.com/u/16827203?s=200&v=4",
"form_fields": {
"password": {
"type": "password",
"label": "Password",
"max": 50,
"min": 3,
"required": true,
"env_variable": "APP_PASSWORD"
}
}
}
}

View file

@ -1,5 +1,6 @@
{
"name": "Radarr",
"available": true,
"port": 8088,
"id": "radarr",
"description": "",

View file

@ -1,5 +1,6 @@
{
"name": "Sonarr",
"available": true,
"port": 8098,
"id": "sonarr",
"description": "",
@ -7,7 +8,5 @@
"author": "",
"source": "",
"image": "https://avatars.githubusercontent.com/u/1082903?s=200&v=4",
"form_fields": {
}
"form_fields": {}
}

View file

@ -1,5 +1,6 @@
{
"name": "Syncthing",
"available": true,
"port": 8090,
"id": "syncthing",
"description": "Syncthing is a peer-to-peer continuous file synchronization program. It synchronizes files between two or more computers in real time, safely protected from prying eyes. Your data is your data alone and you deserve to choose where it is stored, whether it is shared with some third party, and how it's transmitted over the internet.\n\nInstall the Syncthing app on your Umbrel and pair it with the Syncthing app on your phone or computer for a self hosted peer-to-peer backup solution.",

View file

@ -1,5 +1,6 @@
{
"name": "Tailscale",
"available": true,
"port": 8093,
"id": "tailscale",
"description": "",

View file

@ -1,5 +1,6 @@
{
"name": "Transmission",
"available": true,
"port": 8089,
"requirements": {
"ports": [51413]

View file

@ -1,5 +1,6 @@
{
"name": "Wireguard",
"available": true,
"port": 8082,
"requirements": {
"ports": [51820]

View file

@ -1,4 +0,0 @@
# Script to clean up the setup
./scripts/stop.sh
sudo rm -rf app-data/**

41
package.json Normal file
View file

@ -0,0 +1,41 @@
{
"name": "runtipi",
"version": "0.0.1",
"description": "A homeserver for everyone",
"scripts": {
"prepare": "husky install"
},
"dependencies": {
"eslint": "^8.15.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-config-next": "^12.1.4",
"eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-node": "^0.3.4",
"eslint-import-resolver-typescript": "^2.4.0",
"eslint-module-utils": "^2.7.3",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.29.1",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-scope": "^7.1.1",
"eslint-utils": "^3.0.0",
"eslint-visitor-keys": "^3.3.0",
"prettier": "^2.6.2",
"prettier-linter-helpers": "^1.0.0"
},
"devDependencies": {
"husky": "^8.0.1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/meienberger/runtipi.git"
},
"author": "",
"license": "GNU General Public License v3.0",
"bugs": {
"url": "https://github.com/meienberger/runtipi/issues"
},
"homepage": "https://github.com/meienberger/runtipi#readme"
}

View file

@ -30,12 +30,15 @@
"zustand": "^3.7.2"
},
"devDependencies": {
"@babel/core": "^7.0.0",
"@types/js-cookie": "^3.0.2",
"@types/node": "17.0.31",
"@types/react": "18.0.8",
"@types/react-dom": "18.0.3",
"@types/validator": "^13.7.2",
"@typescript-eslint/eslint-plugin": "^5.18.0",
"@typescript-eslint/parser": "^5.0.0",
"eslint-plugin-import": "^2.25.3",
"autoprefixer": "^10.4.4",
"eslint": "8.12.0",
"eslint-config-airbnb-typescript": "^17.0.0",

View file

@ -1,3 +1,3 @@
node_modules/
dist/
*.cjs
*.cjs

View file

@ -1,5 +1,5 @@
module.exports = {
env: { node: true },
env: { node: true, jest: true },
extends: ['airbnb-typescript', 'eslint:recommended', 'plugin:import/typescript'],
parser: '@typescript-eslint/parser',
parserOptions: {

View file

@ -0,0 +1,9 @@
const childProcess: { execFile: typeof execFile } = jest.genMockFromModule('child_process');
const execFile = (path: string, args: string[], thing: any, callback: Function) => {
callback();
};
childProcess.execFile = execFile;
module.exports = childProcess;

View file

@ -0,0 +1,86 @@
import path from 'path';
const fs: {
__createMockFiles: typeof createMockFiles;
readFileSync: typeof readFileSync;
existsSync: typeof existsSync;
writeFileSync: typeof writeFileSync;
mkdirSync: typeof mkdirSync;
rmSync: typeof rmSync;
readdirSync: typeof readdirSync;
copyFileSync: typeof copyFileSync;
} = jest.genMockFromModule('fs');
let mockFiles = Object.create(null);
const createMockFiles = (newMockFiles: Record<string, string>) => {
mockFiles = Object.create(null);
// Create folder tree
for (const file in newMockFiles) {
const dir = path.dirname(file);
if (!mockFiles[dir]) {
mockFiles[dir] = [];
}
mockFiles[dir].push(path.basename(file));
mockFiles[file] = newMockFiles[file];
}
};
const readFileSync = (p: string) => {
return mockFiles[p];
};
const existsSync = (p: string) => {
return mockFiles[p] !== undefined;
};
const writeFileSync = (p: string, data: any) => {
mockFiles[p] = data;
};
const mkdirSync = (p: string) => {
mockFiles[p] = Object.create(null);
};
const rmSync = (p: string, options: { recursive: boolean }) => {
if (options.recursive) {
delete mockFiles[p];
} else {
delete mockFiles[p][Object.keys(mockFiles[p])[0]];
}
};
const readdirSync = (p: string) => {
const files: string[] = [];
const depth = p.split('/').length;
Object.keys(mockFiles).forEach((file) => {
if (file.startsWith(p)) {
const fileDepth = file.split('/').length;
if (fileDepth === depth + 1) {
files.push(file.split('/').pop() || '');
}
}
});
return files;
};
const copyFileSync = (source: string, destination: string) => {
mockFiles[destination] = mockFiles[source];
};
fs.readdirSync = readdirSync;
fs.existsSync = existsSync;
fs.readFileSync = readFileSync;
fs.writeFileSync = writeFileSync;
fs.mkdirSync = mkdirSync;
fs.rmSync = rmSync;
fs.copyFileSync = copyFileSync;
fs.__createMockFiles = createMockFiles;
module.exports = fs;

View file

@ -0,0 +1,7 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/__tests__/**/*.test.ts'],
setupFiles: ['<rootDir>/tests/dotenv-config.ts'],
};

View file

@ -2,12 +2,16 @@
"name": "system-api",
"version": "1.0.0",
"description": "",
"main": "src/server.ts",
"exports": "./dist/server.js",
"type": "module",
"engines": {
"node": ">=14.16"
},
"scripts": {
"clean": "rimraf dist",
"lint": "eslint . --ext .ts",
"test": "echo \"Error: no test specified\" && exit 1",
"test": "jest",
"test:watch": "jest --watch",
"build-prod": "esbuild --bundle src/server.ts --outdir=dist --allow-overwrite --sourcemap --platform=node --minify --analyze=verbose --external:./node_modules/* --format=esm",
"build:watch": "esbuild --bundle src/server.ts --outdir=dist --allow-overwrite --sourcemap --platform=node --external:./node_modules/* --format=esm --watch",
"start:dev": "NODE_ENV=development nodemon --trace-deprecation --trace-warnings --watch dist dist/server.js",
@ -17,6 +21,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"argon2": "^0.28.5",
"bcrypt": "^5.0.1",
"compression": "^1.7.4",
"cookie-parser": "^1.4.6",
@ -24,8 +29,9 @@
"dotenv": "^16.0.0",
"express": "^4.17.3",
"helmet": "^5.0.2",
"internal-ip": "^7.0.0",
"internal-ip": "^6.0.0",
"jsonwebtoken": "^8.5.1",
"mock-fs": "^5.1.2",
"node-port-scanner": "^3.0.1",
"p-iteration": "^1.1.8",
"passport": "^0.5.2",
@ -41,23 +47,26 @@
"@types/cookie-parser": "^1.4.3",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.13",
"@types/jest": "^27.5.0",
"@types/jsonwebtoken": "^8.5.8",
"@types/mock-fs": "^4.13.1",
"@types/passport": "^1.0.7",
"@types/passport-http-bearer": "^1.0.37",
"@types/tcp-port-used": "^1.0.1",
"@types/validator": "^13.7.2",
"@typescript-eslint/eslint-plugin": "^5.18.0",
"@typescript-eslint/parser": "^5.22.0",
"concurrently": "^7.1.0",
"esbuild": "^0.14.32",
"eslint": "^8.13.0",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-config-hardcore": "^24.5.0",
"eslint-config-prettier": "^8.5.0",
"eslint-config-react": "^1.1.7",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.29.4",
"eslint-plugin-unicorn": "^42.0.0",
"jest": "^28.1.0",
"nodemon": "^2.0.15",
"prettier": "2.6.2"
"prettier": "2.6.2",
"ts-jest": "^28.0.2",
"typescript": "4.6.4"
}
}

View file

@ -19,6 +19,7 @@ export type Maybe<T> = T | null | undefined;
export interface AppConfig {
id: string;
available: boolean;
port: number;
name: string;
requirements?: {

View file

@ -0,0 +1,258 @@
import AppsService from '../apps.service';
import fs from 'fs';
import config from '../../../config';
import { AppConfig, FieldTypes } from '../../../config/types';
import childProcess from 'child_process';
jest.mock('fs');
jest.mock('child_process');
beforeEach(() => {
jest.resetModules();
jest.resetAllMocks();
});
const testApp: Partial<AppConfig> = {
id: 'test-app',
port: 3000,
available: true,
form_fields: {
test: {
type: FieldTypes.text,
label: 'Test field',
required: true,
env_variable: 'TEST_FIELD',
},
test2: {
type: FieldTypes.text,
label: 'Test field 2',
required: false,
env_variable: 'TEST_FIELD_2',
},
},
};
const testApp2: Partial<AppConfig> = {
available: true,
id: 'test-app2',
};
const MOCK_FILE_EMPTY = {
[`${config.ROOT_FOLDER}/apps/test-app/config.json`]: JSON.stringify(testApp),
[`${config.ROOT_FOLDER}/.env`]: 'TEST=test',
[`${config.ROOT_FOLDER}/state/apps.json`]: '{"installed": ""}',
};
const MOCK_FILE_INSTALLED = {
[`${config.ROOT_FOLDER}/apps/test-app/config.json`]: JSON.stringify(testApp),
[`${config.ROOT_FOLDER}/apps/test-app2/config.json`]: JSON.stringify(testApp2),
[`${config.ROOT_FOLDER}/.env`]: 'TEST=test',
[`${config.ROOT_FOLDER}/state/apps.json`]: '{"installed": "test-app"}',
[`${config.ROOT_FOLDER}/app-data/test-app`]: '',
[`${config.ROOT_FOLDER}/app-data/test-app/app.env`]: 'TEST=test\nAPP_PORT=3000\nTEST_FIELD=test',
};
describe('Install app', () => {
beforeEach(() => {
// @ts-ignore
fs.__createMockFiles(MOCK_FILE_EMPTY);
});
it('Should correctly generate env file for app', async () => {
await AppsService.installApp('test-app', { test: 'test' });
const envFile = fs.readFileSync(`${config.ROOT_FOLDER}/app-data/test-app/app.env`).toString();
expect(envFile.trim()).toBe('TEST=test\nAPP_PORT=3000\nTEST_FIELD=test');
});
it('Should add app to state file', async () => {
await AppsService.installApp('test-app', { test: 'test' });
const stateFile = JSON.parse(fs.readFileSync(`${config.ROOT_FOLDER}/state/apps.json`).toString());
expect(stateFile.installed).toBe(' test-app');
});
it('Should correctly run app script', async () => {
const spy = jest.spyOn(childProcess, 'execFile');
await AppsService.installApp('test-app', { test: 'test' });
expect(spy.mock.lastCall).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['install', 'test-app'], {}, expect.any(Function)]);
spy.mockRestore();
});
it('Should start app if already installed', async () => {
const spy = jest.spyOn(childProcess, 'execFile');
await AppsService.installApp('test-app', { test: 'test' });
await AppsService.installApp('test-app', { test: 'test' });
expect(spy.mock.calls.length).toBe(2);
expect(spy.mock.calls[0]).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['install', 'test-app'], {}, expect.any(Function)]);
expect(spy.mock.calls[1]).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['start', 'test-app'], {}, expect.any(Function)]);
spy.mockRestore();
});
it('Should throw if required form fields are missing', async () => {
await expect(AppsService.installApp('test-app', {})).rejects.toThrowError('Variable test is required');
});
});
describe('Uninstall app', () => {
beforeEach(() => {
// @ts-ignore
fs.__createMockFiles(MOCK_FILE_INSTALLED);
});
it('Should correctly remove app from state file', async () => {
await AppsService.uninstallApp('test-app');
const stateFile = JSON.parse(fs.readFileSync(`${config.ROOT_FOLDER}/state/apps.json`).toString());
expect(stateFile.installed).toBe('');
});
it('Should correctly run app script', async () => {
const spy = jest.spyOn(childProcess, 'execFile');
await AppsService.uninstallApp('test-app');
expect(spy.mock.lastCall).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['uninstall', 'test-app'], {}, expect.any(Function)]);
spy.mockRestore();
});
it('Should throw if app is not installed', async () => {
await expect(AppsService.uninstallApp('test-app-2')).rejects.toThrowError('App test-app-2 not installed');
});
});
describe('Start app', () => {
beforeEach(() => {
// @ts-ignore
fs.__createMockFiles(MOCK_FILE_INSTALLED);
});
it('Should correctly run app script', async () => {
const spy = jest.spyOn(childProcess, 'execFile');
await AppsService.startApp('test-app');
expect(spy.mock.lastCall).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['start', 'test-app'], {}, expect.any(Function)]);
spy.mockRestore();
});
it('Should throw if app is not installed', async () => {
await expect(AppsService.startApp('test-app-2')).rejects.toThrowError('App test-app-2 not installed');
});
it('Should restart if app is already running', async () => {
const spy = jest.spyOn(childProcess, 'execFile');
await AppsService.startApp('test-app');
expect(spy.mock.calls.length).toBe(1);
await AppsService.startApp('test-app');
expect(spy.mock.calls.length).toBe(2);
spy.mockRestore();
});
it('Should throw if app is not installed', async () => {
await expect(AppsService.startApp('test-app-2')).rejects.toThrowError('App test-app-2 not installed');
});
it('Regenerate env file', async () => {
fs.writeFile(`${config.ROOT_FOLDER}/app-data/test-app/app.env`, 'TEST=test\nAPP_PORT=3000', () => {});
await AppsService.startApp('test-app');
const envFile = fs.readFileSync(`${config.ROOT_FOLDER}/app-data/test-app/app.env`).toString();
expect(envFile.trim()).toBe('TEST=test\nAPP_PORT=3000\nTEST_FIELD=test');
});
});
describe('Stop app', () => {
beforeEach(() => {
// @ts-ignore
fs.__createMockFiles(MOCK_FILE_INSTALLED);
});
it('Should correctly run app script', async () => {
const spy = jest.spyOn(childProcess, 'execFile');
await AppsService.stopApp('test-app');
expect(spy.mock.lastCall).toEqual([`${config.ROOT_FOLDER}/scripts/app.sh`, ['stop', 'test-app'], {}, expect.any(Function)]);
});
it('Should throw if app is not installed', async () => {
await expect(AppsService.stopApp('test-app-2')).rejects.toThrowError('App test-app-2 not installed');
});
});
describe('Update app config', () => {
beforeEach(() => {
// @ts-ignore
fs.__createMockFiles(MOCK_FILE_INSTALLED);
});
it('Should correctly update app config', async () => {
await AppsService.updateAppConfig('test-app', { test: 'test', test2: 'test2' });
const envFile = fs.readFileSync(`${config.ROOT_FOLDER}/app-data/test-app/app.env`).toString();
expect(envFile.trim()).toBe('TEST=test\nAPP_PORT=3000\nTEST_FIELD=test\nTEST_FIELD_2=test2');
});
it('Should throw if app is not installed', async () => {
await expect(AppsService.updateAppConfig('test-app-2', { test: 'test' })).rejects.toThrowError('App test-app-2 not installed');
});
it('Should throw if required form fields are missing', async () => {
await expect(AppsService.updateAppConfig('test-app', {})).rejects.toThrowError('Variable test is required');
});
});
describe('Get app config', () => {
beforeEach(() => {
// @ts-ignore
fs.__createMockFiles(MOCK_FILE_INSTALLED);
});
it('Should correctly get app config', async () => {
const appconfig = await AppsService.getAppInfo('test-app');
expect(appconfig).toEqual({ ...testApp, installed: true, status: 'stopped' });
});
it('Should have installed false if app is not installed', async () => {
const appconfig = await AppsService.getAppInfo('test-app2');
expect(appconfig).toEqual({ ...testApp2, installed: false, status: 'stopped' });
});
});
describe('List apps', () => {
beforeEach(() => {
// @ts-ignore
fs.__createMockFiles(MOCK_FILE_INSTALLED);
});
it('Should correctly list apps', async () => {
const apps = await AppsService.listApps();
expect(apps).toEqual([
{ ...testApp, installed: true, status: 'stopped' },
{ ...testApp2, installed: false, status: 'stopped' },
]);
expect(apps.length).toBe(2);
expect(apps[0].id).toBe('test-app');
expect(apps[1].id).toBe('test-app2');
});
});

View file

@ -1,34 +1,7 @@
import { NextFunction, Request, Response } from 'express';
import si from 'systeminformation';
import { appNames } from '../../config/apps';
import AppsService from './apps.service';
import { AppConfig } from '../../config/types';
import { createFolder, fileExists, readJsonFile, writeFile, readFile } from '../fs/fs.helpers';
import { checkAppExists, checkAppRequirements, checkEnvFile, ensureAppState, getInitalFormValues, runAppScript } from './apps.helpers';
type AppsState = { installed: string };
const getStateFile = (): AppsState => {
return readJsonFile('/state/apps.json');
};
const generateEnvFile = (appName: string, form: Record<string, string>) => {
const configFile: AppConfig = readJsonFile(`/apps/${appName}/config.json`);
const baseEnvFile = readFile('/.env').toString();
let envFile = `${baseEnvFile}\nAPP_PORT=${configFile.port}\n`;
Object.keys(configFile.form_fields).forEach((key) => {
const value = form[key];
if (value) {
const envVar = configFile.form_fields[key].env_variable;
envFile += `${envVar}=${value}\n`;
} else if (configFile.form_fields[key].required) {
throw new Error(`Variable ${key} is required`);
}
});
writeFile(`/app-data/${appName}/app.env`, envFile);
};
import { getInitalFormValues } from './apps.helpers';
const uninstallApp = async (req: Request, res: Response, next: NextFunction) => {
try {
@ -38,11 +11,7 @@ const uninstallApp = async (req: Request, res: Response, next: NextFunction) =>
throw new Error('App name is required');
}
checkAppExists(appName);
ensureAppState(appName, false);
// Run script
await runAppScript(['uninstall', appName]);
await AppsService.uninstallApp(appName);
res.status(200).json({ message: 'App uninstalled successfully' });
} catch (e) {
@ -58,9 +27,7 @@ const stopApp = async (req: Request, res: Response, next: NextFunction) => {
throw new Error('App name is required');
}
checkAppExists(appName);
// Run script
await runAppScript(['stop', appName]);
await AppsService.stopApp(appName);
res.status(200).json({ message: 'App stopped successfully' });
} catch (e) {
@ -77,8 +44,7 @@ const updateAppConfig = async (req: Request, res: Response, next: NextFunction)
throw new Error('App name is required');
}
checkAppExists(appName);
generateEnvFile(appName, form);
AppsService.updateAppConfig(appName, form);
res.status(200).json({ message: 'App updated successfully' });
} catch (e) {
@ -94,15 +60,9 @@ const getAppInfo = async (req: Request, res: Response<AppConfig>, next: NextFunc
throw new Error('App name is required');
}
const dockerContainers = await si.dockerContainers();
const configFile: AppConfig = readJsonFile(`/apps/${id}/config.json`);
const appInfo = await AppsService.getAppInfo(id);
const state = getStateFile();
const installed: string[] = state.installed.split(' ').filter(Boolean);
configFile.installed = installed.includes(id);
configFile.status = (dockerContainers.find((container) => container.name === `${id}`)?.state as 'running') || 'stopped';
res.status(200).json(configFile);
res.status(200).json(appInfo);
} catch (e) {
next(e);
}
@ -110,25 +70,7 @@ const getAppInfo = async (req: Request, res: Response<AppConfig>, next: NextFunc
const listApps = async (req: Request, res: Response, next: NextFunction) => {
try {
const apps = appNames
.map((app) => {
try {
return readJsonFile(`/apps/${app}/config.json`);
} catch {
return null;
}
})
.filter(Boolean);
const dockerContainers = await si.dockerContainers();
const state = getStateFile();
const installed: string[] = state.installed.split(' ').filter(Boolean);
apps.forEach((app) => {
app.installed = installed.includes(app.id);
app.status = dockerContainers.find((container) => container.name === `${app.id}`)?.state || 'stopped';
});
const apps = await AppsService.listApps();
res.status(200).json(apps);
} catch (e) {
@ -138,23 +80,13 @@ const listApps = async (req: Request, res: Response, next: NextFunction) => {
const startApp = async (req: Request, res: Response, next: NextFunction) => {
try {
const { id: appName } = req.params;
const { id } = req.params;
if (!appName) {
if (!id) {
throw new Error('App name is required');
}
checkAppExists(appName);
checkEnvFile(appName);
// Regenerate env file
const form = getInitalFormValues(appName);
generateEnvFile(appName, form);
// Run script
await runAppScript(['start', appName]);
ensureAppState(appName, true);
await AppsService.startApp(id);
res.status(200).json({ message: 'App started successfully' });
} catch (e) {
@ -171,35 +103,7 @@ const installApp = async (req: Request, res: Response, next: NextFunction) => {
throw new Error('App name is required');
}
const appIsAvailable = appNames.includes(id);
if (!appIsAvailable) {
throw new Error(`App ${id} not available`);
}
const appExists = fileExists(`/app-data/${id}`);
if (appExists) {
await startApp(req, res, next);
} else {
const appIsValid = await checkAppRequirements(id);
if (!appIsValid) {
throw new Error(`App ${id} requirements not met`);
}
// Create app folder
createFolder(`/app-data/${id}`);
// Create env file
generateEnvFile(id, form);
ensureAppState(id, true);
// Run script
await runAppScript(['install', id]);
res.status(200).json({ message: 'App installed successfully' });
}
await AppsService.installApp(id, form);
} catch (e) {
next(e);
}

View file

@ -1,8 +1,10 @@
import portUsed from 'tcp-port-used';
import p from 'p-iteration';
import { AppConfig } from '../../config/types';
import { fileExists, readFile, readJsonFile, runScript, writeFile } from '../fs/fs.helpers';
import { internalIpV4 } from 'internal-ip';
import { fileExists, readdirSync, readFile, readJsonFile, runScript, writeFile } from '../fs/fs.helpers';
import InternalIp from 'internal-ip';
type AppsState = { installed: string };
export const checkAppRequirements = async (appName: string) => {
let valid = true;
@ -10,7 +12,7 @@ export const checkAppRequirements = async (appName: string) => {
if (configFile.requirements?.ports) {
await p.forEachSeries(configFile.requirements.ports, async (port: number) => {
const ip = await internalIpV4();
const ip = await InternalIp.v4();
const used = await portUsed.check(port, ip);
if (used) valid = false;
@ -94,8 +96,49 @@ export const ensureAppState = (appName: string, installed: boolean) => {
}
} else {
if (state.installed.indexOf(appName) !== -1) {
state.installed = state.installed.replace(` ${appName}`, '');
state.installed = state.installed.replace(`${appName}`, '');
writeFile('/state/apps.json', JSON.stringify(state));
}
}
};
export const generateEnvFile = (appName: string, form: Record<string, string>) => {
const configFile: AppConfig = readJsonFile(`/apps/${appName}/config.json`);
const baseEnvFile = readFile('/.env').toString();
let envFile = `${baseEnvFile}\nAPP_PORT=${configFile.port}\n`;
Object.keys(configFile.form_fields).forEach((key) => {
const value = form[key];
if (value) {
const envVar = configFile.form_fields[key].env_variable;
envFile += `${envVar}=${value}\n`;
} else if (configFile.form_fields[key].required) {
throw new Error(`Variable ${key} is required`);
}
});
writeFile(`/app-data/${appName}/app.env`, envFile);
};
export const getStateFile = (): AppsState => {
return readJsonFile('/state/apps.json');
};
export const getAvailableApps = (): string[] => {
const apps: string[] = [];
const appsDir = readdirSync('/apps');
appsDir.forEach((app) => {
if (fileExists(`/apps/${app}/config.json`)) {
const configFile: AppConfig = readJsonFile(`/apps/${app}/config.json`);
if (configFile.available) {
apps.push(app);
}
}
});
return apps;
};

View file

@ -0,0 +1,101 @@
import si from 'systeminformation';
import { AppConfig } from '../../config/types';
import { createFolder, fileExists, readJsonFile } from '../fs/fs.helpers';
import { checkAppExists, checkAppRequirements, checkEnvFile, ensureAppState, generateEnvFile, getAvailableApps, getInitalFormValues, getStateFile, runAppScript } from './apps.helpers';
const startApp = async (appName: string): Promise<void> => {
checkAppExists(appName);
checkEnvFile(appName);
// Regenerate env file
const form = getInitalFormValues(appName);
generateEnvFile(appName, form);
// Run script
await runAppScript(['start', appName]);
ensureAppState(appName, true);
};
const installApp = async (id: string, form: Record<string, string>): Promise<void> => {
const appExists = fileExists(`/app-data/${id}`);
if (appExists) {
await startApp(id);
} else {
const appIsValid = await checkAppRequirements(id);
if (!appIsValid) {
throw new Error(`App ${id} requirements not met`);
}
// Create app folder
createFolder(`/app-data/${id}`);
// Create env file
generateEnvFile(id, form);
ensureAppState(id, true);
// Run script
await runAppScript(['install', id]);
}
return Promise.resolve();
};
const listApps = async (): Promise<AppConfig[]> => {
const apps: AppConfig[] = getAvailableApps()
.map((app) => {
try {
return readJsonFile(`/apps/${app}/config.json`);
} catch {
return null;
}
})
.filter(Boolean);
const dockerContainers = await si.dockerContainers();
const state = getStateFile();
const installed: string[] = state.installed.split(' ').filter(Boolean);
apps.forEach((app) => {
app.installed = installed.includes(app.id);
app.status = (dockerContainers.find((container) => container.name === `${app.id}`)?.state as 'running') || 'stopped';
});
return apps;
};
const getAppInfo = async (id: string): Promise<AppConfig> => {
const dockerContainers = await si.dockerContainers();
const configFile: AppConfig = readJsonFile(`/apps/${id}/config.json`);
const state = getStateFile();
const installed: string[] = state.installed.split(' ').filter(Boolean);
configFile.installed = installed.includes(id);
configFile.status = (dockerContainers.find((container) => container.name === `${id}`)?.state as 'running') || 'stopped';
return configFile;
};
const updateAppConfig = async (id: string, form: Record<string, string>): Promise<void> => {
checkAppExists(id);
generateEnvFile(id, form);
};
const stopApp = async (id: string): Promise<void> => {
checkAppExists(id);
// Run script
await runAppScript(['stop', id]);
};
const uninstallApp = async (id: string): Promise<void> => {
checkAppExists(id);
ensureAppState(id, false);
// Run script
await runAppScript(['uninstall', id]);
};
export default { installApp, startApp, listApps, getAppInfo, updateAppConfig, stopApp, uninstallApp };

View file

@ -0,0 +1,147 @@
import { Request, Response } from 'express';
import fs from 'fs';
import * as argon2 from 'argon2';
import config from '../../../config';
import AuthController from '../auth.controller';
let user: any;
jest.mock('fs');
const next = jest.fn();
const MOCK_USER_REGISTERED = () => ({
[`${config.ROOT_FOLDER}/state/users.json`]: `[${user}]`,
});
const MOCK_NO_USER = {
[`${config.ROOT_FOLDER}/state/users.json`]: '[]',
};
beforeAll(async () => {
const hash = await argon2.hash('password');
user = JSON.stringify({
email: 'username',
password: hash,
});
});
describe('Login', () => {
beforeEach(() => {
// @ts-ignore
fs.__createMockFiles(MOCK_USER_REGISTERED());
});
it('Should put cookie in response after login', async () => {
const json = jest.fn();
const res = { cookie: jest.fn(), status: jest.fn(() => ({ json })), json: jest.fn() } as unknown as Response;
const req = { body: { email: 'username', password: 'password' } } as Request;
await AuthController.login(req, res, next);
expect(res.cookie).toHaveBeenCalledWith('tipi_token', expect.any(String), expect.any(Object));
expect(res.status).toHaveBeenCalledWith(200);
expect(json).toHaveBeenCalledWith({ token: expect.any(String) });
expect(next).not.toHaveBeenCalled();
});
it('Should throw if username is not provided in request', async () => {
const res = { cookie: jest.fn(), status: jest.fn(), json: jest.fn() } as unknown as Response;
const req = { body: { password: 'password' } } as Request;
await AuthController.login(req, res, next);
expect(res.cookie).not.toHaveBeenCalled();
expect(next).toHaveBeenCalledWith(expect.any(Error));
});
it('Should throw if password is not provided in request', async () => {
const res = { cookie: jest.fn(), status: jest.fn(), json: jest.fn() } as unknown as Response;
const req = { body: { email: 'username' } } as Request;
await AuthController.login(req, res, next);
expect(res.cookie).not.toHaveBeenCalled();
expect(next).toHaveBeenCalledWith(expect.any(Error));
});
});
describe('Register', () => {
beforeEach(() => {
// @ts-ignore
fs.__createMockFiles(MOCK_NO_USER);
});
it('Should put cookie in response after register', async () => {
const json = jest.fn();
const res = { cookie: jest.fn(), status: jest.fn(() => ({ json })), json: jest.fn() } as unknown as Response;
const req = { body: { email: 'username', password: 'password', name: 'name' } } as Request;
await AuthController.register(req, res, next);
expect(res.cookie).toHaveBeenCalledWith('tipi_token', expect.any(String), expect.any(Object));
expect(res.status).toHaveBeenCalledWith(200);
expect(json).toHaveBeenCalledWith({ token: expect.any(String) });
});
});
describe('Me', () => {
beforeEach(() => {
// @ts-ignore
fs.__createMockFiles(MOCK_USER_REGISTERED());
});
it('Should return user if present in request', async () => {
const json = jest.fn();
const res = { status: jest.fn(() => ({ json })) } as unknown as Response;
const req = { user } as Request;
await AuthController.me(req, res, next);
expect(res.status).toHaveBeenCalledWith(200);
expect(json).toHaveBeenCalledWith({ user });
});
it('Should return null if user is not present in request', async () => {
const json = jest.fn();
const res = { status: jest.fn(() => ({ json })) } as unknown as Response;
const req = {} as Request;
await AuthController.me(req, res, next);
expect(res.status).toHaveBeenCalledWith(200);
expect(json).toHaveBeenCalledWith({ user: null });
});
});
describe('isConfigured', () => {
beforeEach(() => {
// @ts-ignore
fs.__createMockFiles(MOCK_NO_USER);
});
it('Should return false if no user is registered', async () => {
const json = jest.fn();
const res = { status: jest.fn(() => ({ json })) } as unknown as Response;
const req = {} as Request;
await AuthController.isConfigured(req, res, next);
expect(res.status).toHaveBeenCalledWith(200);
expect(json).toHaveBeenCalledWith({ configured: false });
});
it('Should return true if user is registered', async () => {
// @ts-ignore
fs.__createMockFiles(MOCK_USER_REGISTERED());
const json = jest.fn();
const res = { status: jest.fn(() => ({ json })) } as unknown as Response;
const req = { user } as Request;
await AuthController.isConfigured(req, res, next);
expect(res.status).toHaveBeenCalledWith(200);
expect(json).toHaveBeenCalledWith({ configured: true });
});
});

View file

@ -0,0 +1,71 @@
import * as argon2 from 'argon2';
import fs from 'fs';
import config from '../../../config';
import { IUser } from '../../../config/types';
import AuthHelpers from '../auth.helpers';
let user: IUser;
beforeAll(async () => {
const hash = await argon2.hash('password');
user = { email: 'username', password: hash, name: 'name' };
});
jest.mock('fs');
const MOCK_USER_REGISTERED = () => ({
[`${config.ROOT_FOLDER}/state/users.json`]: `[${JSON.stringify(user)}]`,
});
describe('TradeTokenForUser', () => {
beforeEach(() => {
// @ts-ignore
fs.__createMockFiles(MOCK_USER_REGISTERED());
});
it('Should return null if token is invalid', () => {
const result = AuthHelpers.tradeTokenForUser('invalid token');
expect(result).toBeNull();
});
it('Should return user if token is valid', async () => {
const token = await AuthHelpers.getJwtToken(user, 'password');
const result = AuthHelpers.tradeTokenForUser(token);
expect(result).toEqual(user);
});
});
describe('GetJwtToken', () => {
beforeEach(() => {
// @ts-ignore
fs.__createMockFiles(MOCK_USER_REGISTERED());
});
it('Should return token if user and password are valid', async () => {
const token = await AuthHelpers.getJwtToken(user, 'password');
expect(token).toBeDefined();
});
it('Should throw if password is invalid', async () => {
await expect(AuthHelpers.getJwtToken(user, 'invalid password')).rejects.toThrow('Wrong password');
});
});
describe('getUser', () => {
beforeEach(() => {
// @ts-ignore
fs.__createMockFiles(MOCK_USER_REGISTERED());
});
it('Should return null if user is not found', () => {
const result = AuthHelpers.getUser('invalid token');
expect(result).toBeUndefined();
});
it('Should return user if token is valid', async () => {
const result = AuthHelpers.getUser('username');
expect(result).toEqual(user);
});
});

View file

@ -0,0 +1,103 @@
import fs from 'fs';
// import bcrypt from 'bcrypt';
import jsonwebtoken from 'jsonwebtoken';
import * as argon2 from 'argon2';
import config from '../../../config';
import AuthService from '../auth.service';
import { IUser } from '../../../config/types';
jest.mock('fs');
let user: any;
const MOCK_USER_REGISTERED = () => ({
[`${config.ROOT_FOLDER}/state/users.json`]: `[${user}]`,
});
const MOCK_NO_USER = {
[`${config.ROOT_FOLDER}/state/users.json`]: '[]',
};
beforeAll(async () => {
const hash = await argon2.hash('password');
user = JSON.stringify({
email: 'username',
password: hash,
});
});
describe('Login', () => {
beforeEach(() => {
// @ts-ignore
fs.__createMockFiles(MOCK_USER_REGISTERED());
});
it('Should return token after login', async () => {
const token = await AuthService.login('username', 'password');
const { email } = jsonwebtoken.verify(token, config.JWT_SECRET) as { email: string };
expect(token).toBeDefined();
expect(email).toBe('username');
});
it('Should throw if user does not exist', async () => {
await expect(AuthService.login('username1', 'password')).rejects.toThrowError('User not found');
});
it('Should throw if password is incorrect', async () => {
await expect(AuthService.login('username', 'password1')).rejects.toThrowError('Wrong password');
});
});
describe('Register', () => {
beforeEach(() => {
// @ts-ignore
fs.__createMockFiles(MOCK_NO_USER);
});
it('Should return token after register', async () => {
const token = await AuthService.register('username', 'password', 'name');
const { email } = jsonwebtoken.verify(token, config.JWT_SECRET) as { email: string };
expect(token).toBeDefined();
expect(email).toBe('username');
});
it('Should correctly write user to file', async () => {
await AuthService.register('username', 'password', 'name');
const users: IUser[] = JSON.parse(fs.readFileSync(`${config.ROOT_FOLDER}/state/users.json`, 'utf8'));
expect(users.length).toBe(1);
expect(users[0].email).toBe('username');
expect(users[0].name).toBe('name');
const valid = await argon2.verify(users[0].password, 'password');
expect(valid).toBeTruthy();
});
it('Should throw if user already exists', async () => {
await AuthService.register('username', 'password', 'name');
await expect(AuthService.register('username', 'password', 'name')).rejects.toThrowError('There is already an admin user');
});
it('Should throw if email is not provided', async () => {
await expect(AuthService.register('', 'password', 'name')).rejects.toThrowError('Missing email or password');
});
it('Should throw if password is not provided', async () => {
await expect(AuthService.register('username', '', 'name')).rejects.toThrowError('Missing email or password');
});
it('Does not throw if name is not provided', async () => {
await AuthService.register('username', 'password', '');
const users: IUser[] = JSON.parse(fs.readFileSync(`${config.ROOT_FOLDER}/state/users.json`, 'utf8'));
expect(users.length).toBe(1);
});
});

View file

@ -1,8 +1,7 @@
import { NextFunction, Request, Response } from 'express';
import bcrypt from 'bcrypt';
import { IUser } from '../../config/types';
import { readJsonFile, writeFile } from '../fs/fs.helpers';
import { getJwtToken, getUser } from './auth.helpers';
import { readJsonFile } from '../fs/fs.helpers';
import AuthService from './auth.service';
const login = async (req: Request, res: Response, next: NextFunction) => {
try {
@ -12,13 +11,7 @@ const login = async (req: Request, res: Response, next: NextFunction) => {
throw new Error('Missing id or password');
}
const user = getUser(email);
if (!user) {
throw new Error('User not found');
}
const token = await getJwtToken(user, password);
const token = await AuthService.login(email, password);
res.cookie('tipi_token', token, {
httpOnly: false,
@ -34,26 +27,9 @@ const login = async (req: Request, res: Response, next: NextFunction) => {
const register = async (req: Request, res: Response, next: NextFunction) => {
try {
const users: IUser[] = readJsonFile('/state/users.json');
if (users.length > 0) {
throw new Error('There is already an admin user');
}
const { email, password, name } = req.body;
if (!email || !password) {
throw new Error('Missing email or password');
}
if (users.find((user) => user.email === email)) {
throw new Error('User already exists');
}
const hash = await bcrypt.hash(password, 10);
const newuser: IUser = { email, name, password: hash };
const token = await getJwtToken(newuser, password);
const token = await AuthService.register(email, password, name);
res.cookie('tipi_token', token, {
httpOnly: false,
@ -61,28 +37,34 @@ const register = async (req: Request, res: Response, next: NextFunction) => {
maxAge: 1000 * 60 * 60 * 24 * 7,
});
writeFile('/state/users.json', JSON.stringify([newuser]));
res.status(200).json({ token });
} catch (e) {
next(e);
}
};
const me = async (req: Request, res: Response) => {
const { user } = req;
const me = async (req: Request, res: Response, next: NextFunction) => {
try {
const { user } = req;
if (user) {
res.status(200).json({ user });
} else {
res.status(200).json({ user: null });
if (user) {
res.status(200).json({ user });
} else {
res.status(200).json({ user: null });
}
} catch (e) {
next(e);
}
};
const isConfigured = async (req: Request, res: Response) => {
const users: IUser[] = readJsonFile('/state/users.json');
const isConfigured = async (req: Request, res: Response, next: NextFunction) => {
try {
const users: IUser[] = readJsonFile('/state/users.json');
res.status(200).json({ configured: users.length > 0 });
res.status(200).json({ configured: users.length > 0 });
} catch (e) {
next(e);
}
};
export default { login, me, register, isConfigured };

View file

@ -1,10 +1,10 @@
import jsonwebtoken from 'jsonwebtoken';
import bcrypt from 'bcrypt';
import * as argon2 from 'argon2';
import { IUser, Maybe } from '../../config/types';
import { readJsonFile } from '../fs/fs.helpers';
import config from '../../config';
export const getUser = (email: string): Maybe<IUser> => {
const getUser = (email: string): Maybe<IUser> => {
const savedUser: IUser[] = readJsonFile('/state/users.json');
const user = savedUser.find((u) => u.email === email);
@ -13,17 +13,19 @@ export const getUser = (email: string): Maybe<IUser> => {
};
const compareHashPassword = (password: string, hash = ''): Promise<boolean> => {
return bcrypt.compare(password, hash || '');
return argon2.verify(hash, password);
};
const getJwtToken = async (user: IUser, password: string) => {
const validPassword = await compareHashPassword(password, user.password || '');
const validPassword = await compareHashPassword(password, user.password);
if (validPassword) {
if (config.JWT_SECRET) {
return jsonwebtoken.sign({ email: user.email }, config.JWT_SECRET, {
expiresIn: '7d',
});
} else {
throw new Error('JWT_SECRET is not set');
}
}
@ -33,6 +35,7 @@ const getJwtToken = async (user: IUser, password: string) => {
const tradeTokenForUser = (token: string): Maybe<IUser> => {
try {
const { email } = jsonwebtoken.verify(token, config.JWT_SECRET) as { email: string };
const users: IUser[] = readJsonFile('/state/users.json');
return users.find((user) => user.email === email);
@ -41,4 +44,4 @@ const tradeTokenForUser = (token: string): Maybe<IUser> => {
}
};
export { tradeTokenForUser, getJwtToken };
export default { tradeTokenForUser, getJwtToken, getUser };

View file

@ -0,0 +1,48 @@
import * as argon2 from 'argon2';
import { IUser } from '../../config/types';
import { readJsonFile, writeFile } from '../fs/fs.helpers';
import AuthHelpers from './auth.helpers';
const login = async (email: string, password: string) => {
const user = AuthHelpers.getUser(email);
if (!user) {
throw new Error('User not found');
}
const token = await AuthHelpers.getJwtToken(user, password);
return token;
};
const register = async (email: string, password: string, name: string) => {
const users: IUser[] = readJsonFile('/state/users.json');
if (users.length > 0) {
throw new Error('There is already an admin user');
}
if (!email || !password) {
throw new Error('Missing email or password');
}
if (users.find((user) => user.email === email)) {
throw new Error('User already exists');
}
const hash = await argon2.hash(password); // bcrypt.hash(password, 10);
const newuser: IUser = { email, name, password: hash };
const token = await AuthHelpers.getJwtToken(newuser, password);
writeFile('/state/users.json', JSON.stringify([newuser]));
return token;
};
const AuthService = {
login,
register,
};
export default AuthService;

View file

@ -11,6 +11,8 @@ export const readJsonFile = (path: string): any => {
export const readFile = (path: string): string => fs.readFileSync(getAbsolutePath(path)).toString();
export const readdirSync = (path: string): string[] => fs.readdirSync(getAbsolutePath(path));
export const fileExists = (path: string): boolean => fs.existsSync(getAbsolutePath(path));
export const writeFile = (path: string, data: any) => fs.writeFileSync(getAbsolutePath(path), data);

View file

@ -1,7 +1,7 @@
import { Request, Response } from 'express';
import publicIp from 'public-ip';
import portScanner from 'node-port-scanner';
import { internalIpV4 } from 'internal-ip';
import internalIp from 'internal-ip';
const isPortOpen = async (req: Request, res: Response<boolean>) => {
const { port } = req.params;
@ -14,7 +14,7 @@ const isPortOpen = async (req: Request, res: Response<boolean>) => {
};
const getInternalIp = async (req: Request, res: Response<string>) => {
const ip = await internalIpV4();
const ip = await internalIp.v4();
res.status(200).send(ip);
};

View file

@ -0,0 +1,3 @@
import * as dotenv from 'dotenv';
dotenv.config({ path: '.env.test' });

View file

@ -8,13 +8,13 @@
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"module": "commonjs",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"isolatedModules": false,
"jsx": "preserve",
"incremental": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.cjs"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.cjs", "jest.config.cjs"],
"exclude": ["node_modules"]
}

File diff suppressed because it is too large Load diff