Skip to content

Commit

Permalink
feat: secure frontend with password (chrisleekr#260)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisleekr authored Aug 1, 2021
1 parent 1c45699 commit 6173ca9
Show file tree
Hide file tree
Showing 56 changed files with 21,289 additions and 27,984 deletions.
7 changes: 7 additions & 0 deletions .env.dist
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,10 @@ BINANCE_LOCAL_TUNNEL_SUBDOMAIN=default
BINANCE_FEATURE_TOGGLE_NOTIFY_ORDER_CONFIRM=true
BINANCE_FEATURE_TOGGLE_NOTIFY_DEBUG=false
BINANCE_FEATURE_TOGGLE_NOTIFY_ORDER_EXECUTE=true

## Authentication
BINANCE_AUTHENTICATION_ENABLED=true
### Please set your own password.
BINANCE_AUTHENTICATION_PASSWORD=123456
BINANCE_JOBS_TRAILING_TRADE_BOT_OPTIONS_AUTHENTICATION_LOCK_LIST=true
BINANCE_JOBS_TRAILING_TRADE_BOT_OPTIONS_AUTHENTICATION_LOCK_AFTER=120
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

- Secure frontend with the password authentication. Thanks [@pedrohusky](https://github.com/pedrohusky) - [#240](https://github.com/chrisleekr/binance-trading-bot/pull/240)
- Show badge for the customised symbol configuration by [@habibalkhabbaz](https://github.com/habibalkhabbaz) - [#258](https://github.com/chrisleekr/binance-trading-bot/pull/258)
- Filter symbols in the frontend. Thanks [@pedrohusky](https://github.com/pedrohusky) - [#120](https://github.com/chrisleekr/binance-trading-bot/issues/120) [#242](https://github.com/chrisleekr/binance-trading-bot/pull/242)

Expand Down
3 changes: 3 additions & 0 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,11 @@
| BINANCE_SLACK_USERNAME | 슬랙(Slack) username | Chris |
| BINANCE_LOCAL_TUNNEL_ENABLED | 로컬터널([local tunnel](https://github.com/localtunnel/localtunnel)) 활성화/비활성화 | true |
| BINANCE_LOCAL_TUNNEL_SUBDOMAIN | 외부 링크를 위한 로컬터널(local tunnel) 서브도메인 | binance |
| BINANCE_AUTHENTICATION_ENABLED | 프론트엔드 인증 활성화/비활성화 | true |
| BINANCE_AUTHENTICATION_PASSWORD | 프론트엔드 인증 암호 | 123456 |

*로컬 터널은 봇을 외부에서 접근이 가능하도록 설정합니다. 로컬 터널의 하위도메인은 자신만 기억할 수 있는 서브도메인으로 설정하시기 바랍니다.*
*프론트엔드 인증 암호를 꼭 변경하시기 바랍니다. 변경하지 않으면 기본 암호를 사용하게 됩니다.*

2. docker-compose를 이용하여 프로그램을 실행하시기 바랍니다.

Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,8 +299,11 @@ Or use the frontend to adjust configurations after launching the application.
| BINANCE_SLACK_USERNAME | Slack username | Chris |
| BINANCE_LOCAL_TUNNEL_ENABLED | Enable/Disable [local tunnel](https://github.com/localtunnel/localtunnel) | true |
| BINANCE_LOCAL_TUNNEL_SUBDOMAIN | Local tunnel public URL subdomain | binance |
| BINANCE_AUTHENTICATION_ENABLED | Enable/Disable frontend authentication | true |
| BINANCE_AUTHENTICATION_PASSWORD | Frontend password | 123456 |

*A local tunnel makes the bot accessible from the outside. Please set the subdomain of the local tunnel as a subdomain that only you can remember.*
*You must change the authentication password; otherwise, it will be configured as the default password.*

2. Launch/Update the bot with docker-compose

Expand Down Expand Up @@ -367,7 +370,6 @@ Please refer
[CHANGELOG.md](https://github.com/chrisleekr/binance-trading-bot/blob/master/CHANGELOG.md)
to view the past changes.

- [ ] Secure frontend with the password authentication - [#240](https://github.com/chrisleekr/binance-trading-bot/pull/240)
- [ ] Improve sell strategy with conditional stop price percentage based on the profit percentage - [#94](https://github.com/chrisleekr/binance-trading-bot/issues/94)
- [ ] Add sudden drop buy strategy - [#67](https://github.com/chrisleekr/binance-trading-bot/issues/67)
- [ ] Display summary of transactions on the frontend - [#160](https://github.com/chrisleekr/binance-trading-bot/issues/160)
Expand Down
3 changes: 3 additions & 0 deletions README.zh-cn.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,8 +267,11 @@ The final profit would be
| BINANCE_SLACK_USERNAME | Slack username | Chris |
| BINANCE_LOCAL_TUNNEL_ENABLED | Enable/Disable [local tunnel](https://github.com/localtunnel/localtunnel) | true |
| BINANCE_LOCAL_TUNNEL_SUBDOMAIN | Local tunnel public URL subdomain | binance |
| BINANCE_AUTHENTICATION_ENABLED | Enable/Disable frontend authentication | true |
| BINANCE_AUTHENTICATION_PASSWORD | Frontend password | 123456 |

*本地隧道使机器人可以从外部访问。 请将本地隧道的子域设置为只有您能记住的子域。*
*You must change the authentication password; otherwise, it will be configured as the default password.*

2. Check `docker-compose.yml` for `BINANCE_MODE` environment parameter

Expand Down
23 changes: 23 additions & 0 deletions app/__tests__/server-frontend.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ describe('server-frontend', () => {
let mockExpressUse;
let mockExpressListen;
let mockExpressServerOn;
let mockExpressUrlEncoded;
let mockExpressJson;

let mockConfigureWebServer;
let mockConfigureWebSocket;
let mockConfigureLocalTunnel;

Expand All @@ -19,11 +22,14 @@ describe('server-frontend', () => {
jest.mock('ws');
jest.mock('config');

mockConfigureWebServer = jest.fn().mockResolvedValue(true);
mockConfigureWebSocket = jest.fn().mockResolvedValue(true);
mockConfigureLocalTunnel = jest.fn().mockResolvedValue(true);

mockExpressStatic = jest.fn().mockResolvedValue(true);
mockExpressUse = jest.fn().mockResolvedValue(true);
mockExpressUrlEncoded = jest.fn().mockResolvedValue(true);
mockExpressJson = jest.fn().mockResolvedValue(true);

mockExpressListen = jest.fn().mockReturnValue({
on: mockExpressServerOn
Expand All @@ -34,10 +40,19 @@ describe('server-frontend', () => {
use: mockExpressUse,
listen: mockExpressListen
});

Object.defineProperty(mockExpress, 'static', {
value: mockExpressStatic
});

Object.defineProperty(mockExpress, 'urlencoded', {
value: mockExpressUrlEncoded
});

Object.defineProperty(mockExpress, 'json', {
value: mockExpressJson
});

return mockExpress;
});

Expand All @@ -50,6 +65,10 @@ describe('server-frontend', () => {
}
});

jest.mock('../frontend/webserver/configure', () => ({
configureWebServer: mockConfigureWebServer
}));

jest.mock('../frontend/websocket/configure', () => ({
configureWebSocket: mockConfigureWebSocket
}));
Expand All @@ -70,6 +89,10 @@ describe('server-frontend', () => {
expect(mockExpressListen).toHaveBeenCalledWith(80);
});

it('triggers configureWebServer', () => {
expect(mockConfigureWebServer).toHaveBeenCalled();
});

it('triggers configureWebSocket', () => {
expect(mockConfigureWebSocket).toHaveBeenCalled();
});
Expand Down
86 changes: 86 additions & 0 deletions app/frontend/webserver/__tests__/configure.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/* eslint-disable global-require */

describe('webserver/configure.js', () => {
const mockHandlers = {
handleAuth: null,
handle404: null
};

let cacheMock;
let loggerMock;

beforeEach(() => {
jest.clearAllMocks().resetModules();

mockHandlers.handleAuth = jest.fn().mockResolvedValue(true);
mockHandlers.handle404 = jest.fn().mockResolvedValue(true);

jest.mock('../handlers', () => ({
handleAuth: mockHandlers.handleAuth,
handle404: mockHandlers.handle404
}));
});

describe('when jwt token is not cached', () => {
beforeEach(async () => {
const { logger, cache } = require('../../../helpers');

loggerMock = logger;
cacheMock = cache;
cacheMock.get = jest.fn().mockReturnValue(null);
cacheMock.set = jest.fn().mockReturnValue(true);

const { configureWebServer } = require('../configure');
await configureWebServer('app', loggerMock);
});

it('triggers cache.get', () => {
expect(cacheMock.get).toHaveBeenCalledWith('auth-jwt-secret');
});

it('triggers cache.set', () => {
expect(cacheMock.set).toHaveBeenCalledWith(
'auth-jwt-secret',
expect.any(String)
);
});

[
{
handlerFunc: 'handleAuth'
},
{
handlerFunc: 'handle404'
}
].forEach(t => {
it(`triggers ${t.handlerFunc}`, () => {
expect(mockHandlers[t.handlerFunc]).toHaveBeenCalledWith(
loggerMock,
'app'
);
});
});
});

describe('when jwt token is cached', () => {
beforeEach(async () => {
const { logger, cache } = require('../../../helpers');

loggerMock = logger;
cacheMock = cache;
cacheMock.get = jest.fn().mockReturnValue('uuid');
cacheMock.set = jest.fn().mockReturnValue(true);

const { configureWebServer } = require('../configure');
await configureWebServer('app', loggerMock);
});

it('triggers cache.get', () => {
expect(cacheMock.get).toHaveBeenCalledWith('auth-jwt-secret');
});

it('does not trigger cache.set', () => {
expect(cacheMock.set).not.toHaveBeenCalled();
});
});
});
28 changes: 28 additions & 0 deletions app/frontend/webserver/configure.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const { v4: uuidv4 } = require('uuid');

const { cache } = require('../../helpers');

const { handleAuth, handle404 } = require('./handlers');

const configureJWTToken = async () => {
let jwtSecret = await cache.get('auth-jwt-secret');

if (jwtSecret === null) {
jwtSecret = uuidv4();
await cache.set('auth-jwt-secret', jwtSecret);
}

return jwtSecret;
};

const configureWebServer = async (app, funcLogger) => {
const logger = funcLogger.child({ server: 'webserver' });

// Firstly get(or set) JWT secret
await configureJWTToken();

handleAuth(logger, app);
handle404(logger, app);
};

module.exports = { configureWebServer };
11 changes: 11 additions & 0 deletions app/frontend/webserver/handlers/404.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const handle404 = async (_logger, app) => {
// catch 404 and forward to error handler
app.get('*', (_req, res) => {
res.send(
{ success: false, status: 404, message: 'Route not found.', data: {} },
404
);
});
};

module.exports = { handle404 };
24 changes: 24 additions & 0 deletions app/frontend/webserver/handlers/__tests__/404.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/* eslint-disable global-require */
describe('webserver/handlers/404', () => {
const appMock = {};

let resSendMock;

beforeEach(async () => {
resSendMock = jest.fn().mockResolvedValue(true);
appMock.get = jest.fn().mockImplementation((_path, func) => {
func(null, { send: resSendMock });
});

const { handle404 } = require('../404');

await handle404(null, appMock);
});

it('triggers res.send', () => {
expect(resSendMock).toHaveBeenCalledWith(
{ success: false, status: 404, message: 'Route not found.', data: {} },
404
);
});
});
Loading

0 comments on commit 6173ca9

Please sign in to comment.