diff --git a/config.mjs b/config.mjs index 693cffe5..a22e4983 100644 --- a/config.mjs +++ b/config.mjs @@ -44,7 +44,7 @@ export default { expiresIn: '24h', cookie_options: { encoding: 'none', // we already used JWT to encode - isSecure: true, // https only? + isSecure: false, // https only? isHttpOnly: true, // prevent client alteration clearInvalid: true, // remove invalid cookies strictHeader: true, // don't allow violations of RFC 6265 diff --git a/lib/common/di/modules.mjs b/lib/common/di/modules.mjs index 1fdd1d9a..1841c7dd 100644 --- a/lib/common/di/modules.mjs +++ b/lib/common/di/modules.mjs @@ -59,10 +59,9 @@ import UserService from '../../domain/services/UserService'; import sequelize from '../../infrastructure/database/sequelize'; import TransactionService from '../../domain/services/TransactionService'; import Sequelize from 'sequelize'; -import UserRemoteDataSource from '../../domain/repositories/UserRemoteDataSource'; -import UserRemoteDataSourceImpl from '../../interfaces/storage/UserRemoteDataSourceImpl'; -import CafeteriaRemoteDataSourceImpl from '../../interfaces/storage/CafeteriaRemoteDataSourceImpl'; -import CafeteriaRemoteDataSource from '../../domain/repositories/CafeteriaRemoteDataSource'; +import UserRemoteDataSource from '../../interfaces/storage/UserRemoteDataSource'; +import UserLocalDataSource from '../../interfaces/storage/UserLocalDataSource'; +import CafeteriaRemoteDataSource from '../../interfaces/storage/CafeteriaRemoteDataSource'; export default [ /** @@ -180,6 +179,7 @@ export default [ { create: async (r) => new UserRepositoryImpl({ db: await r(Sequelize), + localDataSource: await r(UserLocalDataSource), remoteDataSource: await r(UserRemoteDataSource), }), as: UserRepository, @@ -195,11 +195,15 @@ export default [ * Data Sources */ { - create: async (r) => new CafeteriaRemoteDataSourceImpl(), + create: async (r) => new CafeteriaRemoteDataSource(), as: CafeteriaRemoteDataSource, }, { - create: async (r) => new UserRemoteDataSourceImpl(), + create: async (r) => new UserLocalDataSource(), + as: UserLocalDataSource, + }, + { + create: async (r) => new UserRemoteDataSource(), as: UserRemoteDataSource, }, diff --git a/lib/domain/repositories/CafeteriaRemoteDataSource.mjs b/lib/domain/repositories/CafeteriaDataSource.mjs similarity index 91% rename from lib/domain/repositories/CafeteriaRemoteDataSource.mjs rename to lib/domain/repositories/CafeteriaDataSource.mjs index 2a91ff70..eb9647b4 100644 --- a/lib/domain/repositories/CafeteriaRemoteDataSource.mjs +++ b/lib/domain/repositories/CafeteriaDataSource.mjs @@ -17,10 +17,10 @@ * along with this program. If not, see . */ -class CafeteriaRemoteDataSource { +class CafeteriaDataSource { fetchRawMenus() { throw new Error('Not implemented!'); } } -export default CafeteriaRemoteDataSource; +export default CafeteriaDataSource; diff --git a/lib/domain/repositories/UserRemoteDataSource.mjs b/lib/domain/repositories/UserDataSource.mjs similarity index 87% rename from lib/domain/repositories/UserRemoteDataSource.mjs rename to lib/domain/repositories/UserDataSource.mjs index 6d687492..ae629513 100644 --- a/lib/domain/repositories/UserRemoteDataSource.mjs +++ b/lib/domain/repositories/UserDataSource.mjs @@ -17,10 +17,14 @@ * along with this program. If not, see . */ -class UserRemoteDataSource { +class UserDataSource { + userExists(id) { + throw new Error('Not implemented!'); + } + fetchLoginResult(id, password) { throw new Error('Not implemented!'); } } -export default UserRemoteDataSource; +export default UserDataSource; diff --git a/lib/domain/repositories/UserRepository.mjs b/lib/domain/repositories/UserRepository.mjs index 66a4858c..7fcd77ce 100644 --- a/lib/domain/repositories/UserRepository.mjs +++ b/lib/domain/repositories/UserRepository.mjs @@ -18,7 +18,7 @@ */ class UserRepository { - getRemoteLoginResult(id, password) { + getLoginResult(id, password) { throw new Error('Not implemented!'); } diff --git a/lib/domain/services/UserService.mjs b/lib/domain/services/UserService.mjs index a390ae04..d1ab906b 100644 --- a/lib/domain/services/UserService.mjs +++ b/lib/domain/services/UserService.mjs @@ -53,7 +53,7 @@ class UserService { } async _loginWithIdAndPassword(id, password) { - const remoteLoginResult = await this.userRepository.getRemoteLoginResult(id, password); + const remoteLoginResult = await this.userRepository.getLoginResult(id, password); const isSucceeded = (remoteLoginResult === RemoteLoginResult.SUCCESS); if (isSucceeded) { diff --git a/lib/infrastructure/webserver/server.mjs b/lib/infrastructure/webserver/server.mjs index 62691512..1a0c7dd4 100644 --- a/lib/infrastructure/webserver/server.mjs +++ b/lib/infrastructure/webserver/server.mjs @@ -65,7 +65,7 @@ async function registerPlugins(server) { plugin: HapiSwagger, options: { host: config.server.host, - schemes: ['https'], + schemes: ['http', 'https'], info: { title: '카페테리아 서버 API', version: thisPackage.version, diff --git a/lib/interfaces/controllers/UserController.mjs b/lib/interfaces/controllers/UserController.mjs index 5da971c9..306d9577 100644 --- a/lib/interfaces/controllers/UserController.mjs +++ b/lib/interfaces/controllers/UserController.mjs @@ -77,14 +77,13 @@ class LoginHandler { } _successfulResponse(body, jwt) { - return this._h - .response(body) - .state('token', jwt, config.auth.cookie_options) // cookie - .header('Authorization', jwt); // header + return this._h.response(body) + .header('Authorization', jwt) // header + .state('token', jwt, config.auth.cookie_options); // cookie } _onLoginFailure(result) { - logger.warn(`Failed attempt to login. Full request: ${JSON.stringify(this._request)}`); + logger.warn(`Failed attempt to login. Request payload: ${JSON.stringify(this._request.payload)}`); switch (result) { case LoginResults.WRONG_AUTH: @@ -134,7 +133,7 @@ class LogoutHandler { } _successfulResponse() { - return this._h + return this._h.response() .code(204) .state('token', 'expired', config.auth.cookie_options); } diff --git a/lib/interfaces/storage/CafeteriaRemoteDataSourceImpl.mjs b/lib/interfaces/storage/CafeteriaRemoteDataSource.mjs similarity index 82% rename from lib/interfaces/storage/CafeteriaRemoteDataSourceImpl.mjs rename to lib/interfaces/storage/CafeteriaRemoteDataSource.mjs index 1c9214f6..227c33d2 100644 --- a/lib/interfaces/storage/CafeteriaRemoteDataSourceImpl.mjs +++ b/lib/interfaces/storage/CafeteriaRemoteDataSource.mjs @@ -17,15 +17,15 @@ * along with this program. If not, see . */ -import CafeteriaRemoteDataSource from '../../domain/repositories/CafeteriaRemoteDataSource'; +import CafeteriaDataSource from '../../domain/repositories/CafeteriaDataSource'; import config from '../../../config'; import fetch from '../../common/utils/fetch'; -class CafeteriaRemoteDataSourceImpl extends CafeteriaRemoteDataSource { +class CafeteriaRemoteDataSource extends CafeteriaDataSource { async fetchRawMenus(url, date) { return await fetch.getJson(config.menu.url, {stdDate: date}); } } -export default CafeteriaRemoteDataSourceImpl; +export default CafeteriaRemoteDataSource; diff --git a/lib/interfaces/storage/CafeteriaRepositoryImpl.mjs b/lib/interfaces/storage/CafeteriaRepositoryImpl.mjs index 09980036..633a27f1 100644 --- a/lib/interfaces/storage/CafeteriaRepositoryImpl.mjs +++ b/lib/interfaces/storage/CafeteriaRepositoryImpl.mjs @@ -144,6 +144,17 @@ class CafeteriaRepositoryImpl extends CafeteriaRepository { return []; } + await this._fetchAndStoreIfNeeded(date); + + if (!this._hasMenus(date)) { + logger.warn('No menu after fetch!'); + return []; + } + + return this.menuCache.menus[date]; + } + + async _fetchAndStoreIfNeeded(date) { const nowMillis = Date.now(); const diff = nowMillis - this.menuCache.lastUpdatedMillis; @@ -151,23 +162,22 @@ class CafeteriaRepositoryImpl extends CafeteriaRepository { const noData = this.menuCache.menus[date] == null; if (cacheOld || noData) { - try { - const json = await this.remoteDataSource.fetchRawMenus(config.menu.url, date); - this.menuCache.menus[date] = this.menuConverter.convert(json); - this.menuCache.lastUpdatedMillis = Date.now(); - } catch (e) { - logger.error(e); - } + await this._fetchAndStore(date); } + } - const hasData = Array.isArray(this.menuCache.menus[date]) && this.menuCache.menus[date].length; - - if (!hasData) { - logger.warn('No menu after fetch!'); - return []; + async _fetchAndStore(date) { + try { + const json = await this.remoteDataSource.fetchRawMenus(config.menu.url, date); + this.menuCache.menus[date] = this.menuConverter.convert(json); + this.menuCache.lastUpdatedMillis = Date.now(); + } catch (e) { + logger.error(e); } + } - return this.menuCache.menus[date]; + _hasMenus(date) { + return Array.isArray(this.menuCache.menus[date]) && this.menuCache.menus[date].length; } } diff --git a/lib/interfaces/storage/UserLocalDataSource.mjs b/lib/interfaces/storage/UserLocalDataSource.mjs new file mode 100644 index 00000000..6db4212b --- /dev/null +++ b/lib/interfaces/storage/UserLocalDataSource.mjs @@ -0,0 +1,38 @@ +/** + * This file is part of INU Cafeteria. + * + * Copyright (C) 2020 INU Global App Center + * + * INU Cafeteria is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * INU Cafeteria is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import UserDataSource from '../../domain/repositories/UserDataSource'; +import getEnv from '../../common/utils/env'; +import RemoteLoginResult from '../../domain/constants/RemoteLoginResult'; + +class UserLocalDataSource extends UserDataSource { + userExists(id) { + return String(id) === getEnv('TEST_ID'); + } + + async fetchLoginResult(id, password) { + if (String(id) === getEnv('TEST_ID') && password === getEnv('TEST_PW')) { + return RemoteLoginResult.SUCCESS; + } else { + return RemoteLoginResult.FAIL; + } + } +} + +export default UserLocalDataSource; diff --git a/lib/interfaces/storage/UserRemoteDataSourceImpl.mjs b/lib/interfaces/storage/UserRemoteDataSource.mjs similarity index 83% rename from lib/interfaces/storage/UserRemoteDataSourceImpl.mjs rename to lib/interfaces/storage/UserRemoteDataSource.mjs index b1a8457e..b1a20c2c 100644 --- a/lib/interfaces/storage/UserRemoteDataSourceImpl.mjs +++ b/lib/interfaces/storage/UserRemoteDataSource.mjs @@ -17,14 +17,19 @@ * along with this program. If not, see . */ -import UserRemoteDataSource from '../../domain/repositories/UserRemoteDataSource'; import config from '../../../config'; import fetch from '../../common/utils/fetch'; import encrypt from '../../common/utils/encrypt'; import logger from '../../common/utils/logger'; import RemoteLoginResult from '../../domain/constants/RemoteLoginResult'; +import UserDataSource from '../../domain/repositories/UserDataSource'; +import getEnv from '../../common/utils/env'; + +class UserRemoteDataSource extends UserDataSource { + async userExists(id) { + return id !== getEnv('TEST_ID'); + } -class UserRemoteDataSourceImpl extends UserRemoteDataSource { async fetchLoginResult(id, password) { try { return await fetch.postAndGetReponseText( @@ -42,4 +47,4 @@ class UserRemoteDataSourceImpl extends UserRemoteDataSource { } } -export default UserRemoteDataSourceImpl; +export default UserRemoteDataSource; diff --git a/lib/interfaces/storage/UserRepositoryImpl.mjs b/lib/interfaces/storage/UserRepositoryImpl.mjs index bbf44469..76d66349 100644 --- a/lib/interfaces/storage/UserRepositoryImpl.mjs +++ b/lib/interfaces/storage/UserRepositoryImpl.mjs @@ -29,21 +29,25 @@ const {Sequelize} = seq; * Implementation of UserRepository. */ class UserRepositoryImpl extends UserRepository { - constructor({db, remoteDataSource}) { + constructor({db, localDataSource, remoteDataSource}) { super(); this.db = db; this.userModel = this.db.model('user'); - this.remoteDataSource = remoteDataSource; + this.userDataSources = [localDataSource, remoteDataSource]; } - async getRemoteLoginResult(id, password) { + async getLoginResult(id, password) { if (!id || !password) { return RemoteLoginResult.FUCK; } - return this.remoteDataSource.fetchLoginResult(id, password); + for (const dataSource of this.userDataSources) { + if (dataSource.userExists(id)) { + return dataSource.fetchLoginResult(id, password); + } + } } async findUserById(id) { diff --git a/test/mocks/UserRepositoryMock.mjs b/test/mocks/UserRepositoryMock.mjs index d73199dd..f17502f0 100644 --- a/test/mocks/UserRepositoryMock.mjs +++ b/test/mocks/UserRepositoryMock.mjs @@ -28,7 +28,7 @@ class UserRepositoryMock extends UserRepository { super(); } - getRemoteLoginResult(id, password) { + getLoginResult(id, password) { throw new Error('Not mocked! You need extra mock here'); } diff --git a/test/unit/domain/services/UserService.test.mjs b/test/unit/domain/services/UserService.test.mjs index 12de8221..9602b215 100644 --- a/test/unit/domain/services/UserService.test.mjs +++ b/test/unit/domain/services/UserService.test.mjs @@ -128,7 +128,7 @@ describe('# Logout', () => { const setRemoteLoginResultMock = function(result) { const mock = jest.fn(() => result); - resolve(UserService).userRepository.getRemoteLoginResult = mock; + resolve(UserService).userRepository.getLoginResult = mock; return mock; }; diff --git a/test/unit/interfaces/storage/CafeteriaRepositoryImpl.test.mjs b/test/unit/interfaces/storage/CafeteriaRepositoryImpl.test.mjs index a674dbb5..01df7e4d 100644 --- a/test/unit/interfaces/storage/CafeteriaRepositoryImpl.test.mjs +++ b/test/unit/interfaces/storage/CafeteriaRepositoryImpl.test.mjs @@ -21,7 +21,7 @@ import CafeteriaRepositoryImpl from '../../../../lib/interfaces/storage/Cafeteri import MenuConverter from '../../../../lib/interfaces/converters/MenuConverter'; import config from '../../../../config'; import sequelize from '../../infrastructure/database/sequelizeMock'; -import CafeteriaRemoteDataSource from '../../../../lib/domain/repositories/CafeteriaRemoteDataSource'; +import CafeteriaDataSource from '../../../../lib/domain/repositories/CafeteriaDataSource'; describe('# getAllCafeteria', () => { it('should succeed', async () => { @@ -140,7 +140,7 @@ describe('# getMenusByCornerId', () => { const getRepository = function() { return new CafeteriaRepositoryImpl({ db: sequelize, - remoteDataSource: new (class CafeteriaRemoteDataSourceMock extends CafeteriaRemoteDataSource { + remoteDataSource: new (class CafeteriaRemoteDataSourceMock extends CafeteriaDataSource { fetchRawMenus() { return rawMenuExample; } diff --git a/test/unit/interfaces/storage/UserRepositoryImpl.test.mjs b/test/unit/interfaces/storage/UserRepositoryImpl.test.mjs index 4c0d922a..ac46761a 100644 --- a/test/unit/interfaces/storage/UserRepositoryImpl.test.mjs +++ b/test/unit/interfaces/storage/UserRepositoryImpl.test.mjs @@ -19,27 +19,27 @@ import UserRepositoryImpl from '../../../../lib/interfaces/storage/UserRepositoryImpl'; import sequelize from '../../infrastructure/database/sequelizeMock'; -import UserRemoteDataSource from '../../../../lib/domain/repositories/UserRemoteDataSource'; import RemoteLoginResult from '../../../../lib/domain/constants/RemoteLoginResult'; +import UserDataSource from '../../../../lib/domain/repositories/UserDataSource'; describe('# getRemoteLoginResult', () => { it('should catch null id or password', async () => { const repo = getRepository(); - const result = await repo.getRemoteLoginResult(null, null); + const result = await repo.getLoginResult(null, null); expect(result).toBe(RemoteLoginResult.FUCK); }); it('should fail with wrong auth', async () => { const repo = getRepository(); - const result = await repo.getRemoteLoginResult(3543534242, 'hehe'); + const result = await repo.getLoginResult(3543534242, 'hehe'); expect(result).toBe(RemoteLoginResult.FAIL); }); it('should succeed', async () => { const repo = getRepository(); - const result = await repo.getRemoteLoginResult(201701562, 'password'); + const result = await repo.getLoginResult(201701562, 'password'); expect(result).toBe(RemoteLoginResult.SUCCESS); }); @@ -70,7 +70,7 @@ describe('# addOrUpdateUser', () => { const upsertMock = jest.fn(); sequelize.model('user').upsert = upsertMock; - repo.addOrUpdateUser(null, {}); + await repo.addOrUpdateUser(null, {}); expect(upsertMock).toBeCalledTimes(0); }); @@ -81,7 +81,7 @@ describe('# addOrUpdateUser', () => { const upsertMock = jest.fn(); sequelize.model('user').upsert = upsertMock; - repo.addOrUpdateUser(201701562, {token: 'token', barcode: 'barcode'}); + await repo.addOrUpdateUser(201701562, {token: 'token', barcode: 'barcode'}); expect(upsertMock).toBeCalledTimes(1); expect(upsertMock).toBeCalledWith({id: 201701562, token: 'token', barcode: 'barcode'}); @@ -95,7 +95,7 @@ describe('# updateLastLoginTimestamp', () => { const upsertMock = jest.fn(); sequelize.model('user').upsert = upsertMock; - repo.updateLastLoginTimestamp(201701562); + await repo.updateLastLoginTimestamp(201701562); expect(upsertMock).toBeCalledTimes(1); }); @@ -108,16 +108,18 @@ describe('# updateLastLogoutTimestamp', () => { const upsertMock = jest.fn(); sequelize.model('user').upsert = upsertMock; - repo.updateLastLogoutTimestamp(201701562); + await repo.updateLastLogoutTimestamp(201701562); expect(upsertMock).toBeCalledTimes(1); }); }); const getRepository = function() { - return new UserRepositoryImpl({ - db: sequelize, - remoteDataSource: new (class UserRemoteDataSourceMock extends UserRemoteDataSource { + const dataSource = new (class UserRemoteDataSourceMock extends UserDataSource { + userExists(id) { + return true; + } + fetchLoginResult(id, password) { if (id === 201701562 && password === 'password') { return RemoteLoginResult.SUCCESS; @@ -125,6 +127,11 @@ const getRepository = function() { return RemoteLoginResult.FAIL; } } - }), + }); + + return new UserRepositoryImpl({ + db: sequelize, + localDataSource: dataSource, + remoteDataSource: dataSource, }); };