From 2838428816cb460db5711d311c2614607d43caf3 Mon Sep 17 00:00:00 2001 From: Victoria Teufel Date: Thu, 5 Mar 2020 17:58:06 +0100 Subject: [PATCH 1/6] Added license server --- .dockerignore | 1 + packages/license-server/docker-compose.yml | 24 ++++++++++++++++++++++ packages/license-server/licenses/123.json | 4 ++++ packages/license-server/licenses/456.json | 4 ++++ packages/license-server/site.conf | 19 +++++++++++++++++ packages/license-server/src/index.php | 14 +++++++++++++ 6 files changed, 66 insertions(+) create mode 100644 packages/license-server/docker-compose.yml create mode 100644 packages/license-server/licenses/123.json create mode 100644 packages/license-server/licenses/456.json create mode 100644 packages/license-server/site.conf create mode 100644 packages/license-server/src/index.php diff --git a/.dockerignore b/.dockerignore index bbd7301..fe009f7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,5 @@ node_modules packages/server/node_modules packages/app/node_modules +packages/license-server .git \ No newline at end of file diff --git a/packages/license-server/docker-compose.yml b/packages/license-server/docker-compose.yml new file mode 100644 index 0000000..38b8b8d --- /dev/null +++ b/packages/license-server/docker-compose.yml @@ -0,0 +1,24 @@ +version: '2' + +services: + web: + image: nginx:latest + ports: + - "8080:80" + volumes: + - ./src:/ + - ./licenses:/licenses + - ./site.conf:/etc/nginx/conf.d/default.conf + networks: + - code-network + php: + image: php:fpm + volumes: + - ./src:/ + - ./licenses:/licenses + networks: + - code-network + +networks: + code-network: + driver: bridge diff --git a/packages/license-server/licenses/123.json b/packages/license-server/licenses/123.json new file mode 100644 index 0000000..60271ab --- /dev/null +++ b/packages/license-server/licenses/123.json @@ -0,0 +1,4 @@ +{ + "valid_until": "2020-12-01", + "user_limit": 10 +} \ No newline at end of file diff --git a/packages/license-server/licenses/456.json b/packages/license-server/licenses/456.json new file mode 100644 index 0000000..0c2fe0c --- /dev/null +++ b/packages/license-server/licenses/456.json @@ -0,0 +1,4 @@ +{ + "valid_until": "2020-06-01", + "user_limit": 50 +} \ No newline at end of file diff --git a/packages/license-server/site.conf b/packages/license-server/site.conf new file mode 100644 index 0000000..bd9f2b6 --- /dev/null +++ b/packages/license-server/site.conf @@ -0,0 +1,19 @@ +server { + listen 80; + index index.php index.html; + server_name localhost; + error_log /var/log/nginx/error.log; + access_log /var/log/nginx/access.log; + root /code; + + location ~ \.php$ { + try_files $uri =404; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass php:9000; + fastcgi_index index.php; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + } +} + diff --git a/packages/license-server/src/index.php b/packages/license-server/src/index.php new file mode 100644 index 0000000..edaeeb5 --- /dev/null +++ b/packages/license-server/src/index.php @@ -0,0 +1,14 @@ + \ No newline at end of file From 55ed38380ecdcac7c83c6c98f19c6d758b3ec022 Mon Sep 17 00:00:00 2001 From: Victoria Teufel Date: Tue, 10 Mar 2020 14:51:21 +0100 Subject: [PATCH 2/6] Added license management in settings --- .../app/src/collections/SettingsCollection.ts | 3 +- .../Settings/License/LicenseWidget.tsx | 105 ++++++++++++++++++ .../License/LicenseWidgetAddModal.tsx | 56 ++++++++++ .../src/components/Settings/SettingsPage.tsx | 2 + packages/app/src/graphql/fragments.ts | 8 ++ packages/app/src/graphql/gql.ts | 18 +++ packages/app/src/graphql/index.ts | 16 +++ packages/app/src/i18n/de_DE.ts | 9 +- packages/app/src/i18n/en_US.ts | 9 +- packages/common/types.ts | 29 +++++ .../collections/SettingsCollection.test.ts | 65 ++++++++++- .../src/collections/SettingsCollection.ts | 63 ++++++++++- packages/server/src/errorKeys.ts | 4 + packages/server/src/index.ts | 10 +- packages/server/src/schema.ts | 8 ++ 15 files changed, 394 insertions(+), 11 deletions(-) create mode 100644 packages/app/src/components/Settings/License/LicenseWidget.tsx create mode 100644 packages/app/src/components/Settings/License/LicenseWidgetAddModal.tsx diff --git a/packages/app/src/collections/SettingsCollection.ts b/packages/app/src/collections/SettingsCollection.ts index 757761b..2d32b6a 100644 --- a/packages/app/src/collections/SettingsCollection.ts +++ b/packages/app/src/collections/SettingsCollection.ts @@ -12,7 +12,8 @@ export class SettingsCollection { if (!this.db) { this.db = low(new LocalStorage('connection')); } - this.db.defaults({ settings: { url: window.location.host } }).write(); + const url = process.env.NODE_ENV === 'production' ? window.location.host : `${window.location.hostname}:3001`; + this.db.defaults({ settings: { url } }).write(); return this.db; } diff --git a/packages/app/src/components/Settings/License/LicenseWidget.tsx b/packages/app/src/components/Settings/License/LicenseWidget.tsx new file mode 100644 index 0000000..a256d06 --- /dev/null +++ b/packages/app/src/components/Settings/License/LicenseWidget.tsx @@ -0,0 +1,105 @@ +import { GraphQLErrorMessage } from 'components/Error/GraphQLErrorMessage'; +import { ApolloProps } from 'components/hoc/WithApollo'; +import { GraphQLError } from 'graphql'; +import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Card, CardBody, CardFooter, CardHeader, Table, Button } from 'reactstrap'; +import * as t from 'common/types'; +import { LicenseWidgetAddModal } from './LicenseWidgetAddModal'; + +type Props = ApolloProps; + +interface State { + license: t.License; + errors: readonly GraphQLError[]; + isModalOpen: boolean; +} + +export class LicenseWidget extends React.Component { + constructor(props: Props) { + super(props); + this.state = { + license: { + key: '', + validUntil: '', + userLimit: '' + }, + errors: [], + isModalOpen: false + }; + } + + componentDidMount(): void { + this.props.apollo.getLicense().then(result => { + if (result.data) { + this.setState({ license: result.data.getLicense }); + } + if (result.errors) { + this.setState({ errors: result.errors }); + } + }); + } + + toggleModal = (): void => { + const isModalOpen = !this.state.isModalOpen; + this.setState({ isModalOpen }); + }; + + addLicense = (key: string): void => { + this.props.apollo.addLicense(key).then(result => { + if (result.data) { + this.setState({ license: result.data.addLicense, errors: [] }); + } + if (result.errors) { + this.setState({ errors: result.errors }); + } + }); + }; + + render(): JSX.Element { + return ( + + + + + + + + + + + + + + + + + + + + +
+ + {this.state.license.key}
+ + + {this.state.license.validUntil ? this.state.license.validUntil : } +
+ + {this.state.license.userLimit}
+
+ + + + + +
+ ); + } +} diff --git a/packages/app/src/components/Settings/License/LicenseWidgetAddModal.tsx b/packages/app/src/components/Settings/License/LicenseWidgetAddModal.tsx new file mode 100644 index 0000000..f480775 --- /dev/null +++ b/packages/app/src/components/Settings/License/LicenseWidgetAddModal.tsx @@ -0,0 +1,56 @@ +import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Button, Modal, ModalBody, ModalFooter, ModalHeader, Input } from 'reactstrap'; + +interface State { + key: string; +} + +interface Props { + isOpen: boolean; + toggleModal: () => void; + saveModal: (key: string) => void; +} + +const initialState = { + key: '' +}; + +export class LicenseWidgetAddModal extends React.Component { + constructor(props: Props) { + super(props); + this.state = initialState; + } + + save = (): void => { + this.props.saveModal(this.state.key); + this.toggleModal(); + }; + + toggleModal = () => this.setState(initialState, () => this.props.toggleModal()); + + setKey = (event: React.ChangeEvent): void => { + this.setState({ key: event.target.value }); + }; + + render(): JSX.Element { + return ( + + + + + + + + + {' '} + + + + ); + } +} diff --git a/packages/app/src/components/Settings/SettingsPage.tsx b/packages/app/src/components/Settings/SettingsPage.tsx index ad70e88..a900341 100644 --- a/packages/app/src/components/Settings/SettingsPage.tsx +++ b/packages/app/src/components/Settings/SettingsPage.tsx @@ -6,6 +6,7 @@ import { WrappedComponentProps, injectIntl } from 'react-intl'; import { RouteComponentProps, withRouter } from 'react-router-dom'; import { Col, Container, Row } from 'reactstrap'; import { PauseWidget } from './Pause/PauseWidget'; +import { LicenseWidget } from './License/LicenseWidget'; export class SettingsPage extends React.Component & ApolloProps, {}> { render(): JSX.Element { @@ -15,6 +16,7 @@ export class SettingsPage extends React.Component + diff --git a/packages/app/src/graphql/fragments.ts b/packages/app/src/graphql/fragments.ts index 2bf490e..8463c80 100644 --- a/packages/app/src/graphql/fragments.ts +++ b/packages/app/src/graphql/fragments.ts @@ -194,3 +194,11 @@ export const TimestampUserAndStatisticFragment = gql` ${TimestampFragment} ${UserFragment} `; + +export const LicenseFragment = gql` + fragment LicenseFields on License { + key + validUntil + userLimit + } +`; diff --git a/packages/app/src/graphql/gql.ts b/packages/app/src/graphql/gql.ts index 077238c..006a7b6 100644 --- a/packages/app/src/graphql/gql.ts +++ b/packages/app/src/graphql/gql.ts @@ -140,6 +140,15 @@ export const UPDATE_WORKTIME_SETTINGS = gql` ${fragments.WorkTimeSettingsFragment} `; +export const ADD_LICENSE = gql` + mutation AddLicense($key: String!) { + addLicense(key: $key) { + ...LicenseFields + } + } + ${fragments.LicenseFragment} +`; + /** * QUERIES */ @@ -334,3 +343,12 @@ export const GET_WORKTIME_SETTINGS = gql` } ${fragments.WorkTimeSettingsFragment} `; + +export const GET_LICENSE = gql` + query GetLicense { + getLicense { + ...LicenseFields + } + } + ${fragments.LicenseFragment} +`; diff --git a/packages/app/src/graphql/index.ts b/packages/app/src/graphql/index.ts index 7bd1b59..c484e7b 100644 --- a/packages/app/src/graphql/index.ts +++ b/packages/app/src/graphql/index.ts @@ -188,6 +188,15 @@ export class Apollo { }); } + static addLicense(key: string): Promise> { + return this.mutate({ + variables: { + key + }, + mutation: gql.ADD_LICENSE + }); + } + /** * QUERIES */ @@ -331,4 +340,11 @@ export class Apollo { query: gql.GET_USER_BY_ID }); } + + static getLicense(): Promise> { + return this.query({ + variables: {}, + query: gql.GET_LICENSE + }); + } } diff --git a/packages/app/src/i18n/de_DE.ts b/packages/app/src/i18n/de_DE.ts index 51befb1..67c767e 100644 --- a/packages/app/src/i18n/de_DE.ts +++ b/packages/app/src/i18n/de_DE.ts @@ -142,5 +142,12 @@ export const de_DE = { DELETE_USER: 'Benutzer Löschen?', PRINT: 'Drucken', REWRITE_TIMESTAMPS: 'Zeitstempel neu schreiben', - COMMENT: 'Kommentar' + COMMENT: 'Kommentar', + LICENSE: 'Lizenz', + KEY: 'Linzenzschlüssel', + VALID_UNTIL: 'Gültig bis', + USER_LIMIT: 'Max. Benutzerzahl', + UNLIMITED: 'Unbegrenzt', + ADD_LICENSE: 'Lizenz hinzufügen', + LICENSE_NOT_FOUND: 'Lizenz nicht gefunden!' }; diff --git a/packages/app/src/i18n/en_US.ts b/packages/app/src/i18n/en_US.ts index 5a1fbdf..af0c393 100644 --- a/packages/app/src/i18n/en_US.ts +++ b/packages/app/src/i18n/en_US.ts @@ -142,5 +142,12 @@ export const en_US = { DELETE_USER: 'Delete User?', PRINT: 'Print', REWRITE_TIMESTAMPS: 'Rewrite Timestamps', - COMMENT: 'Comment' + COMMENT: 'Comment', + LICENSE: 'License', + KEY: 'License Key', + VALID_UNTIL: 'Valid Until', + USER_LIMIT: 'User Limit', + UNLIMITED: 'Unlimited', + ADD_LICENSE: 'Add License', + LICENSE_NOT_FOUND: 'License not found!' }; diff --git a/packages/common/types.ts b/packages/common/types.ts index 7b53a22..4dc7e7f 100644 --- a/packages/common/types.ts +++ b/packages/common/types.ts @@ -97,6 +97,13 @@ export type LeaveInput = { requestedLeaveDays?: Maybe; }; +export type License = { + __typename?: 'License'; + key: Scalars['String']; + validUntil: Scalars['String']; + userLimit: Scalars['String']; +}; + export type Mutation = { __typename?: 'Mutation'; createUser: User; @@ -114,6 +121,7 @@ export type Mutation = { deletePublicHoliday: Array; addTimestampByCode: TimestampUserAndStatistic; rewriteTimestamps?: Maybe; + addLicense: License; }; export type MutationCreateUserArgs = { @@ -185,6 +193,10 @@ export type MutationRewriteTimestampsArgs = { date: Scalars['String']; }; +export type MutationAddLicenseArgs = { + key: Scalars['String']; +}; + export type Pause = { __typename?: 'Pause'; id: Scalars['String']; @@ -229,6 +241,7 @@ export type Query = { getPauses: Array; getEvaluationForMonth: Array; getEvaluationForUsers: Array; + getLicense: License; }; export type QueryGetUsersArgs = { @@ -546,6 +559,8 @@ export type TimestampUserAndStatisticFieldsFragment = { __typename?: 'TimestampU user: { __typename?: 'User' } & UserFieldsFragment; }; +export type LicenseFieldsFragment = { __typename?: 'License' } & Pick; + export type UpdateAllUserWorkTimesByIdMutationVariables = { userId: Scalars['String']; workTimes: WorkTimesInput; @@ -675,6 +690,14 @@ export type UpdateWorkTimeSettingsMutation = { __typename?: 'Mutation' } & { updateWorkTimeSettings: { __typename?: 'WorkTimeSettings' } & WorkTimeSettingsFieldsFragment; }; +export type AddLicenseMutationVariables = { + key: Scalars['String']; +}; + +export type AddLicenseMutation = { __typename?: 'Mutation' } & { + addLicense: { __typename?: 'License' } & LicenseFieldsFragment; +}; + export type GetUsersQueryVariables = {}; export type GetUsersQuery = { __typename?: 'Query' } & { @@ -833,3 +856,9 @@ export type GetWorkTimeSettingsQueryVariables = {}; export type GetWorkTimeSettingsQuery = { __typename?: 'Query' } & { getWorkTimeSettings: { __typename?: 'WorkTimeSettings' } & WorkTimeSettingsFieldsFragment; }; + +export type GetLicenseQueryVariables = {}; + +export type GetLicenseQuery = { __typename?: 'Query' } & { + getLicense: { __typename?: 'License' } & LicenseFieldsFragment; +}; diff --git a/packages/server/src/collections/SettingsCollection.test.ts b/packages/server/src/collections/SettingsCollection.test.ts index eb15e2f..0853b2c 100644 --- a/packages/server/src/collections/SettingsCollection.test.ts +++ b/packages/server/src/collections/SettingsCollection.test.ts @@ -1,7 +1,7 @@ jest.mock('lowdb/adapters/FileAsync'); import * as moment from 'moment'; import * as t from 'common/types'; -import { PauseErrorKeys } from './../errorKeys'; +import { PauseErrorKeys, LicenseErrorKeys } from './../errorKeys'; import { SettingsCollection } from './SettingsCollection'; describe('SettingsCollection Tests', () => { @@ -88,4 +88,67 @@ describe('SettingsCollection Tests', () => { expect(result).toEqual([otherPause]); }); }); + + describe('Licenses', () => { + describe('removeLicense()', () => { + it('should set default license', async () => { + db.setState({ license: { key: '1', validUntil: '2020-06-01', userLimit: '10' } }); + + const result = await SettingsCollection.removeLicense(); + + expect(result).toEqual(SettingsCollection.defaultLicense); + }); + }); + + describe('getLicense()', () => { + it('should return default license if no license is set', async () => { + const result = await SettingsCollection.getLicense(); + + expect(result).toEqual(SettingsCollection.defaultLicense); + }); + + it('should return license from db', async () => { + const license = { key: '1', validUntil: '2020-06-01', userLimit: '10' }; + db.setState({ license }); + + const result = await SettingsCollection.getLicense(); + + expect(result).toEqual(license); + }); + }); + + describe('addLicense()', () => { + it('should return error if server does not return license', async () => { + SettingsCollection.queryLicense = jest.fn(key => { + return Promise.reject(new Error(LicenseErrorKeys.LICENSE_NOT_FOUND)); + }); + + return SettingsCollection.addLicense('123').catch(e => { + expect(e.message).toEqual(LicenseErrorKeys.LICENSE_NOT_FOUND); + }); + }); + + it('should return license if server returns license', async () => { + const license = { key: '123', validUntil: '2020-06-01', userLimit: '10' }; + SettingsCollection.queryLicense = jest.fn(key => { + return Promise.resolve(license); + }); + + const result = await SettingsCollection.addLicense('123'); + + expect(result).toEqual(license); + }); + + it('should save license if server returns license', async () => { + const license = { key: '123', validUntil: '2020-06-01', userLimit: '10' }; + SettingsCollection.queryLicense = jest.fn(key => { + return Promise.resolve(license); + }); + + await SettingsCollection.addLicense('123'); + + expect(db.getState().license).toEqual(license); + }); + }); + }); }); diff --git a/packages/server/src/collections/SettingsCollection.ts b/packages/server/src/collections/SettingsCollection.ts index 076068b..66c0ad5 100644 --- a/packages/server/src/collections/SettingsCollection.ts +++ b/packages/server/src/collections/SettingsCollection.ts @@ -1,11 +1,19 @@ import * as t from 'common/types'; import { v4 } from 'uuid'; +import * as https from 'https'; import { DbAdapterWithColKey } from './DbAdapterWithColKey'; +import { LicenseErrorKeys } from '../errorKeys'; export class SettingsCollection extends DbAdapterWithColKey { static db: any; static jsonFileName = 'settings.json'; + static defaultLicense = { + key: '0', + validUntil: '', + userLimit: '1' + }; + static defaultObject(): object { return { ['pauses']: [], @@ -15,7 +23,8 @@ export class SettingsCollection extends DbAdapterWithColKey { publicHoliday: t.WorkDayPaymentType.PAID, holiday: t.WorkDayPaymentType.PAID, sickday: t.WorkDayPaymentType.PAID - } + }, + ['license']: this.defaultLicense }; } @@ -49,4 +58,56 @@ export class SettingsCollection extends DbAdapterWithColKey { static async setWorkTimeSettings(settings: t.WorkTimeSettingsInput): Promise { return this.set('workTimeSettings', settings).then(() => this.getWorkTimeSettings()); } + + static async addLicense(key: string): Promise { + try { + const license = await this.queryLicense(key); + return this.set('license', license).then(() => this.getLicense()); + } catch (error) { + return Promise.reject(error); + } + } + + static async queryLicense(key: string): Promise { + return new Promise((resolve, reject) => { + let output = ''; + + const req = https.request(`https://teufel-it.de/license.php?license_key=${key}`, res => { + res.setEncoding('utf8'); + + if (res.statusCode !== 200) { + req.end(); + reject(new Error(LicenseErrorKeys.LICENSE_NOT_FOUND)); + return; + } + + res.on('data', chunk => { + output += chunk; + }); + + res.on('end', () => { + const license = JSON.parse(output); + resolve({ key: key, validUntil: license.valid_until, userLimit: license.user_limit }); + }); + }); + + req.on('error', error => { + reject(error); + }); + + req.end(); + }); + } + + static async removeLicense(): Promise { + return this.set('license', this.defaultLicense).then(() => this.getLicense()); + } + + static async getLicense(): Promise { + const result = await this.getCol('license'); + if (result.value().length === 0) { + return this.defaultLicense; + } + return result.value() as t.License; + } } diff --git a/packages/server/src/errorKeys.ts b/packages/server/src/errorKeys.ts index 58503da..b46694e 100644 --- a/packages/server/src/errorKeys.ts +++ b/packages/server/src/errorKeys.ts @@ -40,3 +40,7 @@ export enum UserErrorKeys { } export enum ScannedUserErrorKeys {} + +export enum LicenseErrorKeys { + LICENSE_NOT_FOUND = 'LICENSE_NOT_FOUND' +} diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 463d66f..621d4e3 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -54,7 +54,8 @@ const resolvers = { getEvaluationForMonth: async (source: any, args: t.GetEvaluationForMonthQueryVariables) => EvaluationCalculator.getEvaluationForMonth(args.date, args.userId), getEvaluationForUsers: async (source: any, args: t.GetEvaluationForUsersQueryVariables) => - EvaluationCalculator.getEvaluationForUsers(args.date) + EvaluationCalculator.getEvaluationForUsers(args.date), + getLicense: async (source: any, args: t.GetLicenseQueryVariables) => SettingsCollection.getLicense() }, Mutation: { createUser: async (source: any, args: t.CreateUserMutationVariables) => UserCollection.create(args.user), @@ -82,7 +83,8 @@ const resolvers = { PublicHolidayCollection.removePublicHolidayById(args.year, args.holidayId), addTimestampByCode: async (source: any, args: t.AddTimestampByCodeMutationVariables) => UserCollection.addTimestampByCode(args.code), - rewriteTimestamps: async (source: any, args: any) => TimestampsBatch.rewriteTimestamps(args.userId, args.date) + rewriteTimestamps: async (source: any, args: any) => TimestampsBatch.rewriteTimestamps(args.userId, args.date), + addLicense: async (source: any, args: t.AddLicenseMutationVariables) => SettingsCollection.addLicense(args.key) } } as IResolvers; @@ -91,10 +93,6 @@ const app = express(); app.use(cors()); app.use(bodyParser.json()); -/*if (process.env.NODE_ENV === 'production') { - app.use(express.static(path.join(__dirname, 'dist-web'))); -}*/ - app.post('/api/login', (req, res) => { UserCollection.loginUser(req.body.password) .then(result => res.status(200).json(result)) diff --git a/packages/server/src/schema.ts b/packages/server/src/schema.ts index 9b069ea..14e5eca 100644 --- a/packages/server/src/schema.ts +++ b/packages/server/src/schema.ts @@ -19,6 +19,7 @@ export const typeDefs = gql` getPauses: [Pause!]! getEvaluationForMonth(date: String!, userId: String!): [Evaluation!]! getEvaluationForUsers(date: String!): [UserEvaluation!]! + getLicense: License! } type Mutation { @@ -37,6 +38,7 @@ export const typeDefs = gql` deletePublicHoliday(year: String!, holidayId: String!): [PublicHoliday!]! addTimestampByCode(code: String!): TimestampUserAndStatistic! rewriteTimestamps(userId: String!, date: String!): Boolean + addLicense(key: String!): License! } input PublicHolidayInput { @@ -268,6 +270,12 @@ export const typeDefs = gql` error: String } + type License { + key: String! + validUntil: String! + userLimit: String! + } + enum WorkDayPaymentType { PAID UNPAID From b9db59f93cfc8cbf37a2b9ff649069e382758326 Mon Sep 17 00:00:00 2001 From: Victoria Teufel Date: Tue, 10 Mar 2020 18:38:37 +0100 Subject: [PATCH 3/6] Move license querying to service --- .../collections/SettingsCollection.test.ts | 7 ++-- .../src/collections/SettingsCollection.ts | 36 ++----------------- .../server/src/services/LicenseService.ts | 36 +++++++++++++++++++ 3 files changed, 42 insertions(+), 37 deletions(-) create mode 100644 packages/server/src/services/LicenseService.ts diff --git a/packages/server/src/collections/SettingsCollection.test.ts b/packages/server/src/collections/SettingsCollection.test.ts index 0853b2c..8c7228e 100644 --- a/packages/server/src/collections/SettingsCollection.test.ts +++ b/packages/server/src/collections/SettingsCollection.test.ts @@ -3,6 +3,7 @@ import * as moment from 'moment'; import * as t from 'common/types'; import { PauseErrorKeys, LicenseErrorKeys } from './../errorKeys'; import { SettingsCollection } from './SettingsCollection'; +import { LicenseService } from '../services/LicenseService'; describe('SettingsCollection Tests', () => { let db: any; @@ -119,7 +120,7 @@ describe('SettingsCollection Tests', () => { describe('addLicense()', () => { it('should return error if server does not return license', async () => { - SettingsCollection.queryLicense = jest.fn(key => { + LicenseService.queryLicense = jest.fn(key => { return Promise.reject(new Error(LicenseErrorKeys.LICENSE_NOT_FOUND)); }); @@ -130,7 +131,7 @@ describe('SettingsCollection Tests', () => { it('should return license if server returns license', async () => { const license = { key: '123', validUntil: '2020-06-01', userLimit: '10' }; - SettingsCollection.queryLicense = jest.fn(key => { + LicenseService.queryLicense = jest.fn(key => { return Promise.resolve(license); }); @@ -141,7 +142,7 @@ describe('SettingsCollection Tests', () => { it('should save license if server returns license', async () => { const license = { key: '123', validUntil: '2020-06-01', userLimit: '10' }; - SettingsCollection.queryLicense = jest.fn(key => { + LicenseService.queryLicense = jest.fn(key => { return Promise.resolve(license); }); diff --git a/packages/server/src/collections/SettingsCollection.ts b/packages/server/src/collections/SettingsCollection.ts index 66c0ad5..1ee92dc 100644 --- a/packages/server/src/collections/SettingsCollection.ts +++ b/packages/server/src/collections/SettingsCollection.ts @@ -1,8 +1,7 @@ import * as t from 'common/types'; import { v4 } from 'uuid'; -import * as https from 'https'; import { DbAdapterWithColKey } from './DbAdapterWithColKey'; -import { LicenseErrorKeys } from '../errorKeys'; +import { LicenseService } from '../services/LicenseService'; export class SettingsCollection extends DbAdapterWithColKey { static db: any; @@ -61,44 +60,13 @@ export class SettingsCollection extends DbAdapterWithColKey { static async addLicense(key: string): Promise { try { - const license = await this.queryLicense(key); + const license = await LicenseService.queryLicense(key); return this.set('license', license).then(() => this.getLicense()); } catch (error) { return Promise.reject(error); } } - static async queryLicense(key: string): Promise { - return new Promise((resolve, reject) => { - let output = ''; - - const req = https.request(`https://teufel-it.de/license.php?license_key=${key}`, res => { - res.setEncoding('utf8'); - - if (res.statusCode !== 200) { - req.end(); - reject(new Error(LicenseErrorKeys.LICENSE_NOT_FOUND)); - return; - } - - res.on('data', chunk => { - output += chunk; - }); - - res.on('end', () => { - const license = JSON.parse(output); - resolve({ key: key, validUntil: license.valid_until, userLimit: license.user_limit }); - }); - }); - - req.on('error', error => { - reject(error); - }); - - req.end(); - }); - } - static async removeLicense(): Promise { return this.set('license', this.defaultLicense).then(() => this.getLicense()); } diff --git a/packages/server/src/services/LicenseService.ts b/packages/server/src/services/LicenseService.ts new file mode 100644 index 0000000..5d5c978 --- /dev/null +++ b/packages/server/src/services/LicenseService.ts @@ -0,0 +1,36 @@ +import * as t from 'common/types'; +import * as https from 'https'; +import { LicenseErrorKeys } from '../errorKeys'; + +export class LicenseService { + static async queryLicense(key: string): Promise { + return new Promise((resolve, reject) => { + let output = ''; + + const req = https.request(`https://teufel-it.de/license.php?license_key=${key}`, res => { + res.setEncoding('utf8'); + + if (res.statusCode !== 200) { + req.end(); + reject(new Error(LicenseErrorKeys.LICENSE_NOT_FOUND)); + return; + } + + res.on('data', chunk => { + output += chunk; + }); + + res.on('end', () => { + const license = JSON.parse(output); + resolve({ key: key, validUntil: license.valid_until, userLimit: license.user_limit }); + }); + }); + + req.on('error', error => { + reject(error); + }); + + req.end(); + }); + } +} From 1dd598df4ba9e0a99f47d1d66af9927721e371bf Mon Sep 17 00:00:00 2001 From: Victoria Teufel Date: Tue, 17 Mar 2020 19:26:27 +0100 Subject: [PATCH 4/6] Enfore license --- .../src/components/Booking/BookingPage.tsx | 37 ++++- .../Booking/ComplainWidget/ComplainWidget.tsx | 2 +- .../components/Booking/StatisticWidget.tsx | 4 + .../TimestampWidget/TimestampWidget.tsx | 4 + .../components/Evaluation/EvaluationPage.tsx | 16 ++- .../Evaluation/UserEvaluationPage.tsx | 19 ++- .../PublicHolidayWidget.tsx | 23 ++- .../Settings/License/LicenseWidget.tsx | 4 +- .../components/Settings/Pause/PauseWidget.tsx | 4 + .../components/Settings/WorkDaySettings.tsx | 15 +- .../components/UserManagement/UserTabPage.tsx | 2 + packages/app/src/i18n/de_DE.ts | 4 +- packages/app/src/i18n/en_US.ts | 4 +- packages/server/src/errorKeys.ts | 4 +- packages/server/src/index.ts | 19 ++- .../src/services/LicenseService.test.ts | 132 ++++++++++++++++++ .../server/src/services/LicenseService.ts | 60 ++++++++ 17 files changed, 331 insertions(+), 22 deletions(-) create mode 100644 packages/server/src/services/LicenseService.test.ts diff --git a/packages/app/src/components/Booking/BookingPage.tsx b/packages/app/src/components/Booking/BookingPage.tsx index bf587c5..f316abf 100644 --- a/packages/app/src/components/Booking/BookingPage.tsx +++ b/packages/app/src/components/Booking/BookingPage.tsx @@ -34,11 +34,13 @@ interface States { statisticForDate: t.Statistic; statisticForWeek: t.Statistic; statisticForMonth: t.Statistic; + statisticErrors: readonly GraphQLError[]; hoursSpentForMonthPerDay: t.HoursPerDay[]; yearSaldo: string; timestamps: t.Timestamp[]; timestampError?: string | null; - complainsErrors: GraphQLError[]; + timestampErrors: readonly GraphQLError[]; + complainsErrors: readonly GraphQLError[]; complains: t.Complain[]; listOfLeaves: t.Leave[]; publicHolidays: t.PublicHoliday[]; @@ -55,10 +57,12 @@ export class BookingPage extends React.Component { statisticForDate: initialStatisticState, statisticForMonth: initialStatisticState, statisticForWeek: initialStatisticState, + statisticErrors: [], hoursSpentForMonthPerDay: [], yearSaldo: '00:00', timestamps: [], timestampError: '', + timestampErrors: [], complains: [], complainsErrors: [], listOfLeaves: [], @@ -121,6 +125,11 @@ export class BookingPage extends React.Component { yearSaldo: result.data.getYearSaldo }); } + if (result.errors) { + this.setState({ statisticErrors: result.errors }); + } else { + this.setState({ statisticErrors: [] }); + } }); } @@ -140,6 +149,11 @@ export class BookingPage extends React.Component { previousSelectedDate: result.data.getStatisticForWeek.selectedDate }); } + if (result.errors) { + this.setState({ statisticErrors: result.errors }); + } else { + this.setState({ statisticErrors: [] }); + } }); } @@ -159,6 +173,11 @@ export class BookingPage extends React.Component { previousSelectedDate: result.data.getStatisticForMonth.selectedDate }); } + if (result.errors) { + this.setState({ statisticErrors: result.errors }); + } else { + this.setState({ statisticErrors: [] }); + } }); } @@ -191,16 +210,25 @@ export class BookingPage extends React.Component { if (result.data) { this.setState({ timestamps: result.data.updateTimestamps.timestamps, - timestampError: result.data.updateTimestamps.error + timestampError: result.data.updateTimestamps.error, + timestampErrors: [] }); } + if (result.errors) { + this.setState({ timestampErrors: result.errors }); + } }); } updateComplains(userId: string, dateKey: string, complains: t.ComplainInput[]) { return this.props.apollo.updateComplains(userId, dateKey, complains).then(result => { if (result.data) { - this.setState({ complains: result.data.updateComplains }); + this.setState({ complains: result.data.updateComplains, complainsErrors: [] }); + } + if (result.errors) { + this.setState({ complainsErrors: result.errors }); + } else { + this.setState({ complainsErrors: [] }); } }); } @@ -216,6 +244,7 @@ export class BookingPage extends React.Component { this.setState({ timestamps: [], timestampError: '', + timestampErrors: [], complains: [], complainsErrors: [] }); @@ -307,6 +336,7 @@ export class BookingPage extends React.Component { statisticForDate={this.state.statisticForDate} statisticForWeek={this.state.statisticForWeek} statisticForMonth={this.state.statisticForMonth} + errors={this.state.statisticErrors} /> @@ -335,6 +365,7 @@ export class BookingPage extends React.Component { selectedDate={this.state.selectedDate} timestamps={this.state.timestamps} timestampError={this.state.timestampError} + errors={this.state.timestampErrors} onClose={this.handleBookingModalClose} onUpdateTimestamps={this.updateTimestampsAndRefreshStatistics} showData={this.state.showData} diff --git a/packages/app/src/components/Booking/ComplainWidget/ComplainWidget.tsx b/packages/app/src/components/Booking/ComplainWidget/ComplainWidget.tsx index 02223e1..011ce48 100644 --- a/packages/app/src/components/Booking/ComplainWidget/ComplainWidget.tsx +++ b/packages/app/src/components/Booking/ComplainWidget/ComplainWidget.tsx @@ -12,7 +12,7 @@ declare let MOCKLOGIN: boolean; interface Props { selectedDate: moment.Moment; complains: t.Complain[]; - complainsErrors: GraphQLError[]; + complainsErrors: readonly GraphQLError[]; onUpdateComplains: (complains: t.Complain[]) => void; showData: boolean; userRole: string; diff --git a/packages/app/src/components/Booking/StatisticWidget.tsx b/packages/app/src/components/Booking/StatisticWidget.tsx index f77389f..a0b8bf0 100644 --- a/packages/app/src/components/Booking/StatisticWidget.tsx +++ b/packages/app/src/components/Booking/StatisticWidget.tsx @@ -1,7 +1,9 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; +import { GraphQLError } from 'graphql'; import { Card, CardBody, CardFooter, CardHeader, Table } from 'reactstrap'; import * as t from 'common/types'; +import { GraphQLErrorMessage } from 'components/Error/GraphQLErrorMessage'; interface Props { yearSaldo: string; @@ -9,6 +11,7 @@ interface Props { statisticForDate: t.Statistic; statisticForWeek: t.Statistic; statisticForMonth: t.Statistic; + errors: readonly GraphQLError[]; } export class StatisticWidget extends React.Component { @@ -125,6 +128,7 @@ export class StatisticWidget extends React.Component { > {this.props.yearSaldo} + ); diff --git a/packages/app/src/components/Booking/TimestampWidget/TimestampWidget.tsx b/packages/app/src/components/Booking/TimestampWidget/TimestampWidget.tsx index 115c0f0..939757e 100644 --- a/packages/app/src/components/Booking/TimestampWidget/TimestampWidget.tsx +++ b/packages/app/src/components/Booking/TimestampWidget/TimestampWidget.tsx @@ -1,11 +1,13 @@ import { ErrorMessage } from 'components/Error/ErrorMessage'; import * as moment from 'moment'; import * as React from 'react'; +import { GraphQLError } from 'graphql'; import { FormattedMessage } from 'react-intl'; import { Button, Card, CardBody, CardFooter, CardHeader, Table } from 'reactstrap'; import * as t from 'common/types'; import { TimestampWidgetCreateModal } from './TimestampWidgetCreateModal'; import { TimestampWidgetItem } from './TimestampWidgetItem'; +import { GraphQLErrorMessage } from 'components/Error/GraphQLErrorMessage'; declare let MOCKLOGIN: boolean; interface Props { @@ -16,6 +18,7 @@ interface Props { showData: boolean; timestampError?: string | null; userRole: string; + errors: readonly GraphQLError[]; } interface State { @@ -109,6 +112,7 @@ export class TimestampWidget extends React.Component { )} + diff --git a/packages/app/src/components/Evaluation/EvaluationPage.tsx b/packages/app/src/components/Evaluation/EvaluationPage.tsx index 6fd885c..c803430 100644 --- a/packages/app/src/components/Evaluation/EvaluationPage.tsx +++ b/packages/app/src/components/Evaluation/EvaluationPage.tsx @@ -7,15 +7,18 @@ import * as React from 'react'; import { WrappedComponentProps, injectIntl } from 'react-intl'; import { RouteComponentProps } from 'react-router-dom'; import { Container } from 'reactstrap'; +import { GraphQLError } from 'graphql'; import * as t from 'common/types'; import { EvaluationTable } from './EvaluationTable'; import { MonthAndYearPickerWidget } from './MonthAndYearPickerWidget'; +import { GraphQLErrorMessage } from 'components/Error/GraphQLErrorMessage'; interface Props extends RouteComponentProps<{ userId: string }>, ApolloProps, WrappedComponentProps {} interface State { listOfEvaluation: t.Evaluation[]; selectedUser: t.User; + errors: readonly GraphQLError[]; } export class EvaluationPage extends React.Component { @@ -23,7 +26,8 @@ export class EvaluationPage extends React.Component { super(props); this.state = { listOfEvaluation: [], - selectedUser: initialUserState + selectedUser: initialUserState, + errors: [] }; } @@ -46,11 +50,16 @@ export class EvaluationPage extends React.Component { if (result.data) { this.setState({ listOfEvaluation: result.data.getEvaluationForMonth }); } + if (result.errors) { + this.setState({ errors: result.errors }); + } else { + this.setState({ errors: [] }); + } }); } handleChange = (date: moment.Moment) => { - this.setState({ listOfEvaluation: [] }); + this.setState({ listOfEvaluation: [], errors: [] }); const userId = this.props.match.params.userId; this.getEvaluationForMonth(userId, moment(date).format(API_DATE)); }; @@ -67,7 +76,8 @@ export class EvaluationPage extends React.Component { className="mt-3" /> )} - {!this.state.listOfEvaluation.length && } + {!!this.state.errors.length && } + {!this.state.listOfEvaluation.length && !this.state.errors && } ); } diff --git a/packages/app/src/components/Evaluation/UserEvaluationPage.tsx b/packages/app/src/components/Evaluation/UserEvaluationPage.tsx index 051390d..2e36dea 100644 --- a/packages/app/src/components/Evaluation/UserEvaluationPage.tsx +++ b/packages/app/src/components/Evaluation/UserEvaluationPage.tsx @@ -3,24 +3,28 @@ import { LoadingSpinner } from 'components/Spinner/LoadingSpinner'; import { API_DATE } from 'common/constants'; import * as moment from 'moment'; import * as React from 'react'; +import { GraphQLError } from 'graphql'; import { WrappedComponentProps, injectIntl } from 'react-intl'; import { RouteComponentProps } from 'react-router-dom'; import { Container } from 'reactstrap'; import * as t from 'common/types'; import { EvaluationTable } from './EvaluationTable'; import { MonthAndYearPickerWidget } from './MonthAndYearPickerWidget'; +import { GraphQLErrorMessage } from 'components/Error/GraphQLErrorMessage'; interface Props extends RouteComponentProps<{}>, ApolloProps, WrappedComponentProps {} interface State { listOfUserEvaluation: t.UserEvaluation[]; + errors: readonly GraphQLError[]; } export class UserEvaluationPage extends React.Component { constructor(props: Props) { super(props); this.state = { - listOfUserEvaluation: [] + listOfUserEvaluation: [], + errors: [] }; } @@ -33,15 +37,21 @@ export class UserEvaluationPage extends React.Component { if (result.data) { this.setState({ listOfUserEvaluation: result.data.getEvaluationForUsers }); } + + if (result.errors) { + this.setState({ errors: result.errors }); + } else { + this.setState({ errors: [] }); + } }); } componentWillUnmount() { - this.setState({ listOfUserEvaluation: [] }); + this.setState({ listOfUserEvaluation: [], errors: [] }); } handleChange = (date: moment.Moment) => { - this.setState({ listOfUserEvaluation: [] }); + this.setState({ listOfUserEvaluation: [], errors: [] }); this.getEvaluationForUsers(moment(date).format(API_DATE)); }; @@ -61,7 +71,8 @@ export class UserEvaluationPage extends React.Component { /> ); })} - {!this.state.listOfUserEvaluation.length && } + {!!this.state.errors.length && } + {!this.state.listOfUserEvaluation.length && !this.state.errors.length && } ); } diff --git a/packages/app/src/components/Leave/PublicHolidayWidget/PublicHolidayWidget.tsx b/packages/app/src/components/Leave/PublicHolidayWidget/PublicHolidayWidget.tsx index efdd663..3bf2fc3 100644 --- a/packages/app/src/components/Leave/PublicHolidayWidget/PublicHolidayWidget.tsx +++ b/packages/app/src/components/Leave/PublicHolidayWidget/PublicHolidayWidget.tsx @@ -2,11 +2,13 @@ import { ApolloProps } from 'components/hoc/WithApollo'; import { MonthAndYearPicker } from 'components/TimePicker/MonthAndYearPicker.tsx'; import * as moment from 'moment'; import * as React from 'react'; +import { GraphQLError } from 'graphql'; import { FormattedMessage } from 'react-intl'; import { Button, Card, CardBody, CardFooter, CardHeader, Table } from 'reactstrap'; import * as t from 'common/types'; import { PublicHolidayWidgetCreateModal } from './PublicHolidayWidgetCreateModal'; import { PublicHolidayWidgetItem } from './PublicHolidayWidgetItem'; +import { GraphQLErrorMessage } from 'components/Error/GraphQLErrorMessage'; type Props = ApolloProps; @@ -14,6 +16,7 @@ interface State { isOpen: boolean; year: string; publicHolidays: t.PublicHoliday[]; + errors: readonly GraphQLError[]; } export class PublicHolidayWidget extends React.Component { @@ -22,7 +25,8 @@ export class PublicHolidayWidget extends React.Component { this.state = { isOpen: false, year: moment().format('YYYY'), - publicHolidays: [] + publicHolidays: [], + errors: [] }; } @@ -43,9 +47,13 @@ export class PublicHolidayWidget extends React.Component { this.props.apollo.loadPublicHolidays(this.state.year).then(result => { if (result.data) { this.setState({ - publicHolidays: result.data.loadPublicHolidays + publicHolidays: result.data.loadPublicHolidays, + errors: [] }); } + if (result.errors) { + this.setState({ errors: result.errors }); + } }); }; @@ -66,6 +74,11 @@ export class PublicHolidayWidget extends React.Component { publicHolidays: result.data.deletePublicHoliday }); } + if (result.errors) { + this.setState({ errors: result.errors }); + } else { + this.setState({ errors: [] }); + } }); }; @@ -74,6 +87,11 @@ export class PublicHolidayWidget extends React.Component { if (result.data) { this.setState({ publicHolidays: result.data.createPublicHoliday }); } + if (result.errors) { + this.setState({ errors: result.errors }); + } else { + this.setState({ errors: [] }); + } }); }; @@ -112,6 +130,7 @@ export class PublicHolidayWidget extends React.Component { + { - {this.state.license.userLimit} + + {this.state.license.userLimit ? this.state.license.userLimit : } + diff --git a/packages/app/src/components/Settings/Pause/PauseWidget.tsx b/packages/app/src/components/Settings/Pause/PauseWidget.tsx index 771ceaa..df14dd4 100644 --- a/packages/app/src/components/Settings/Pause/PauseWidget.tsx +++ b/packages/app/src/components/Settings/Pause/PauseWidget.tsx @@ -53,6 +53,8 @@ export class PauseWidget extends React.Component { } if (result.errors) { this.setState({ errors: result.errors }); + } else { + this.setState({ errors: [] }); } }); }; @@ -64,6 +66,8 @@ export class PauseWidget extends React.Component { } if (result.errors) { this.setState({ errors: result.errors }); + } else { + this.setState({ errors: [] }); } }); }; diff --git a/packages/app/src/components/Settings/WorkDaySettings.tsx b/packages/app/src/components/Settings/WorkDaySettings.tsx index 1b94b98..2efa78d 100644 --- a/packages/app/src/components/Settings/WorkDaySettings.tsx +++ b/packages/app/src/components/Settings/WorkDaySettings.tsx @@ -1,13 +1,16 @@ import { ApolloProps } from 'components/hoc/WithApollo'; import * as React from 'react'; +import { GraphQLError } from 'graphql'; import { FormattedMessage, WrappedComponentProps } from 'react-intl'; import { Card, CardBody, CardFooter, CardHeader, FormGroup, Input, Label } from 'reactstrap'; import * as t from 'common/types'; +import { GraphQLErrorMessage } from 'components/Error/GraphQLErrorMessage'; interface Props extends WrappedComponentProps, ApolloProps {} interface State { workTimeSettings: t.WorkTimeSettings; + errors: readonly GraphQLError[]; } export class WorkDaySettings extends React.Component { @@ -19,7 +22,8 @@ export class WorkDaySettings extends React.Component { publicHoliday: t.WorkDayPaymentType.UNPAID, schoolday: t.WorkDayPaymentType.UNPAID, sickday: t.WorkDayPaymentType.UNPAID - } + }, + errors: [] }; } @@ -105,13 +109,20 @@ export class WorkDaySettings extends React.Component { + ); } handleSave = () => { - this.props.apollo.UpdateWorkTimeSettings(this.state.workTimeSettings); + this.props.apollo.UpdateWorkTimeSettings(this.state.workTimeSettings).then(result => { + if (result.errors) { + this.setState({ errors: result.errors }); + } else { + this.setState({ errors: [] }); + } + }); }; handleInputChange = (event: React.ChangeEvent): void => { diff --git a/packages/app/src/components/UserManagement/UserTabPage.tsx b/packages/app/src/components/UserManagement/UserTabPage.tsx index 2777898..00417ba 100644 --- a/packages/app/src/components/UserManagement/UserTabPage.tsx +++ b/packages/app/src/components/UserManagement/UserTabPage.tsx @@ -118,6 +118,8 @@ export class UserTabPage extends React.Component { } if (result.errors) { this.setState({ errors: result.errors }); + } else { + this.setState({ errors: [] }); } }); }; diff --git a/packages/app/src/i18n/de_DE.ts b/packages/app/src/i18n/de_DE.ts index 67c767e..bfe735d 100644 --- a/packages/app/src/i18n/de_DE.ts +++ b/packages/app/src/i18n/de_DE.ts @@ -149,5 +149,7 @@ export const de_DE = { USER_LIMIT: 'Max. Benutzerzahl', UNLIMITED: 'Unbegrenzt', ADD_LICENSE: 'Lizenz hinzufügen', - LICENSE_NOT_FOUND: 'Lizenz nicht gefunden!' + LICENSE_NOT_FOUND: 'Lizenz nicht gefunden!', + LICENSE_EXPIRED: 'Lizenz abgelaufen!', + LICENSE_USER_LIMIT_EXCEEDED: 'Lizenz Benutzerlimit überschritten!' }; diff --git a/packages/app/src/i18n/en_US.ts b/packages/app/src/i18n/en_US.ts index af0c393..bf69f29 100644 --- a/packages/app/src/i18n/en_US.ts +++ b/packages/app/src/i18n/en_US.ts @@ -149,5 +149,7 @@ export const en_US = { USER_LIMIT: 'User Limit', UNLIMITED: 'Unlimited', ADD_LICENSE: 'Add License', - LICENSE_NOT_FOUND: 'License not found!' + LICENSE_NOT_FOUND: 'License not found!', + LICENSE_EXPIRED: 'License expired!', + LICENSE_USER_LIMIT_EXCEEDED: 'License user limit exceeded!' }; diff --git a/packages/server/src/errorKeys.ts b/packages/server/src/errorKeys.ts index b46694e..6fabe67 100644 --- a/packages/server/src/errorKeys.ts +++ b/packages/server/src/errorKeys.ts @@ -42,5 +42,7 @@ export enum UserErrorKeys { export enum ScannedUserErrorKeys {} export enum LicenseErrorKeys { - LICENSE_NOT_FOUND = 'LICENSE_NOT_FOUND' + LICENSE_NOT_FOUND = 'LICENSE_NOT_FOUND', + LICENSE_EXPIRED = 'LICENSE_EXPIRED', + LICENSE_USER_LIMIT_EXCEEDED = 'LICENSE_USER_LIMIT_EXCEEDED' } diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 621d4e3..573ea81 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -16,6 +16,7 @@ import { UserCollection } from './collections/UserCollection'; import { typeDefs } from './schema'; import { PublicHolidayService } from './services/PublicHolidayService'; import cors = require('cors'); +import { LicenseService } from './services/LicenseService'; declare global { namespace Express { @@ -101,10 +102,20 @@ app.post('/api/login', (req, res) => { const server = new ApolloServer({ typeDefs, resolvers }); -server.applyMiddleware({ app }); - // auth middleware -app.use(server.graphqlPath, (req, res, next) => { +app.use(server.graphqlPath, async (req, res, next) => { + const operation = req.body.operationName; + if (operation) { + try { + await LicenseService.checkLicenseValidity(operation); + } catch (error) { + res.json({ + errors: [{ message: error.message }] + }); + return; + } + } + const token = req.headers.authorization || 'no_token'; if (process.env.NODE_ENV === 'production') { return UserCollection.verifyLogin(token) @@ -122,6 +133,8 @@ app.use(server.graphqlPath, (req, res, next) => { } }); +server.applyMiddleware({ app }); + // Start the server app.listen(3001, () => { console.log(`Go to http://localhost:3001${server.graphqlPath} to run queries!`); diff --git a/packages/server/src/services/LicenseService.test.ts b/packages/server/src/services/LicenseService.test.ts new file mode 100644 index 0000000..737d8fe --- /dev/null +++ b/packages/server/src/services/LicenseService.test.ts @@ -0,0 +1,132 @@ +import { LicenseService } from './LicenseService'; +import { SettingsCollection } from '../collections/SettingsCollection'; +import { LicenseErrorKeys } from '../errorKeys'; +import { UserCollection } from '../collections/UserCollection'; +import * as m from 'common/mocks'; + +describe('License Service Test', () => { + describe('checkLicenseValidity()', () => { + describe('unprotected operation', () => { + beforeEach(() => { + jest.spyOn(LicenseService, 'isProtectedOperation').mockImplementation(() => false); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should not throw error if license is not valid anymore', () => { + const license = { key: '1', userLimit: '', validUntil: '2019-01-01' }; + SettingsCollection.getLicense = jest.fn(() => Promise.resolve(license)); + + expect(async () => { + await LicenseService.checkLicenseValidity('operation'); + }).not.toThrow(); + }); + + it('should not throw error if license user limit is exceeded', () => { + const license = { key: '1', userLimit: '1', validUntil: '' }; + SettingsCollection.getLicense = jest.fn(() => Promise.resolve(license)); + UserCollection.getUsers = jest.fn(() => Promise.resolve([m.userMock, m.userMock])); + + expect(async () => { + await LicenseService.checkLicenseValidity('operation'); + }).not.toThrow(); + }); + + it('should not throw error if user will be created and license user limit will be exceeded', () => { + const license = { key: '1', userLimit: '1', validUntil: '' }; + SettingsCollection.getLicense = jest.fn(() => Promise.resolve(license)); + UserCollection.getUsers = jest.fn(() => Promise.resolve([m.userMock])); + + expect(async () => { + await LicenseService.checkLicenseValidity('CreateUser'); + }).not.toThrow(); + }); + }); + + describe('protected operation', () => { + beforeEach(() => { + jest.spyOn(LicenseService, 'isProtectedOperation').mockImplementation(() => true); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should throw error if license is not valid anymore', async () => { + const license = { key: '1', userLimit: '', validUntil: '2019-01-01' }; + SettingsCollection.getLicense = jest.fn(() => Promise.resolve(license)); + + try { + await LicenseService.checkLicenseValidity('operation'); + } catch (error) { + expect(error.message).toEqual(LicenseErrorKeys.LICENSE_EXPIRED); + } + }); + + it('should throw error if license user limit is exceeded', async () => { + const license = { key: '1', userLimit: '1', validUntil: '' }; + SettingsCollection.getLicense = jest.fn(() => Promise.resolve(license)); + UserCollection.getUsers = jest.fn(() => Promise.resolve([m.userMock, m.userMock])); + + try { + await LicenseService.checkLicenseValidity('operation'); + } catch (error) { + expect(error.message).toEqual(LicenseErrorKeys.LICENSE_USER_LIMIT_EXCEEDED); + } + }); + + it('should throw error if user will be created and license user limit will be exceeded', async () => { + const license = { key: '1', userLimit: '1', validUntil: '' }; + SettingsCollection.getLicense = jest.fn(() => Promise.resolve(license)); + UserCollection.getUsers = jest.fn(() => Promise.resolve([m.userMock])); + + try { + await LicenseService.checkLicenseValidity('CreateUser'); + } catch (error) { + expect(error.message).toEqual(LicenseErrorKeys.LICENSE_USER_LIMIT_EXCEEDED); + } + }); + + it('should not throw error if user limit and validity is not exceeded', () => { + const license = { key: '1', userLimit: '1', validUntil: '' }; + SettingsCollection.getLicense = jest.fn(() => Promise.resolve(license)); + UserCollection.getUsers = jest.fn(() => Promise.resolve([m.userMock])); + + expect(async () => { + await LicenseService.checkLicenseValidity('operation'); + }).not.toThrow(); + }); + }); + }); + + describe('isProtectedOperation()', () => { + it('should return true if operation is protected', () => { + expect(LicenseService.isProtectedOperation('LoadPublicHolidays')).toBe(true); + expect(LicenseService.isProtectedOperation('GetStatisticForDate')).toBe(true); + expect(LicenseService.isProtectedOperation('GetStatisticForWeek')).toBe(true); + expect(LicenseService.isProtectedOperation('GetStatisticForMonth')).toBe(true); + expect(LicenseService.isProtectedOperation('GetEvaluationForMonth')).toBe(true); + expect(LicenseService.isProtectedOperation('GetEvaluationForUsers')).toBe(true); + expect(LicenseService.isProtectedOperation('CreateUser')).toBe(true); + expect(LicenseService.isProtectedOperation('UpdateUser')).toBe(true); + expect(LicenseService.isProtectedOperation('UpdateWorkTimeSettings')).toBe(true); + expect(LicenseService.isProtectedOperation('UpdateAllUserWorkTimesById')).toBe(true); + expect(LicenseService.isProtectedOperation('UpdateTimestamps')).toBe(true); + expect(LicenseService.isProtectedOperation('CreateLeave')).toBe(true); + expect(LicenseService.isProtectedOperation('DeleteLeave')).toBe(true); + expect(LicenseService.isProtectedOperation('UpdateComplains')).toBe(true); + expect(LicenseService.isProtectedOperation('CreatePause')).toBe(true); + expect(LicenseService.isProtectedOperation('DeletePause')).toBe(true); + expect(LicenseService.isProtectedOperation('CreatePublicHoliday')).toBe(true); + expect(LicenseService.isProtectedOperation('DeletePublicHoliday')).toBe(true); + expect(LicenseService.isProtectedOperation('RewriteTimestamps')).toBe(true); + }); + + it('should return false if operation is not protected', () => { + expect(LicenseService.isProtectedOperation('')).toBe(false); + expect(LicenseService.isProtectedOperation('anythingelse')).toBe(false); + }); + }); +}); diff --git a/packages/server/src/services/LicenseService.ts b/packages/server/src/services/LicenseService.ts index 5d5c978..4d50b42 100644 --- a/packages/server/src/services/LicenseService.ts +++ b/packages/server/src/services/LicenseService.ts @@ -1,6 +1,9 @@ import * as t from 'common/types'; import * as https from 'https'; import { LicenseErrorKeys } from '../errorKeys'; +import { SettingsCollection } from '../collections/SettingsCollection'; +import moment = require('moment'); +import { UserCollection } from '../collections/UserCollection'; export class LicenseService { static async queryLicense(key: string): Promise { @@ -33,4 +36,61 @@ export class LicenseService { req.end(); }); } + + static protectedOperations = [ + 'LoadPublicHolidays', + 'GetStatisticForDate', + 'GetStatisticForWeek', + 'GetStatisticForMonth', + 'GetEvaluationForMonth', + 'GetEvaluationForUsers', + + 'CreateUser', + 'UpdateUser', + 'UpdateWorkTimeSettings', + 'UpdateAllUserWorkTimesById', + 'UpdateTimestamps', + 'CreateLeave', + 'DeleteLeave', + 'UpdateComplains', + 'CreatePause', + 'DeletePause', + 'CreatePublicHoliday', + 'DeletePublicHoliday', + 'RewriteTimestamps' + ]; + + static isProtectedOperation(operation: string): boolean { + return this.protectedOperations.includes(operation); + } + + static async checkLicenseValidity(operation: string): Promise { + if (!this.isProtectedOperation(operation)) { + return; + } + + const license = await SettingsCollection.getLicense(); + if (license.validUntil) { + const validUntil = moment(license.validUntil); + const now = moment(); + if (validUntil < now) { + throw new Error(LicenseErrorKeys.LICENSE_EXPIRED); + } + } else { + // valid forever + } + + if (license.userLimit) { + const userLimit = parseInt(license.userLimit); + const users = await UserCollection.getUsers(); + if (users.length > userLimit) { + throw new Error(LicenseErrorKeys.LICENSE_USER_LIMIT_EXCEEDED); + } + if (operation === 'CreateUser' && users.length === userLimit) { + throw new Error(LicenseErrorKeys.LICENSE_USER_LIMIT_EXCEEDED); + } + } else { + // unlimited users + } + } } From 8ffe85a0b46a2676b8d04cb69176d828da332047 Mon Sep 17 00:00:00 2001 From: Victoria Teufel Date: Sat, 21 Mar 2020 19:57:04 +0100 Subject: [PATCH 5/6] Refresh license on login --- .../collections/SettingsCollection.test.ts | 12 ++++++ .../src/collections/SettingsCollection.ts | 6 ++- packages/server/src/index.ts | 8 +++- .../src/services/LicenseService.test.ts | 42 +++++++++++++++++++ .../server/src/services/LicenseService.ts | 14 +++++++ 5 files changed, 79 insertions(+), 3 deletions(-) diff --git a/packages/server/src/collections/SettingsCollection.test.ts b/packages/server/src/collections/SettingsCollection.test.ts index 8c7228e..862f02f 100644 --- a/packages/server/src/collections/SettingsCollection.test.ts +++ b/packages/server/src/collections/SettingsCollection.test.ts @@ -151,5 +151,17 @@ describe('SettingsCollection Tests', () => { expect(db.getState().license).toEqual(license); }); }); + + describe('replaceLicense()', () => { + it('should replace license', async () => { + db.setState({ license: { key: '1', validUntil: '2020-06-01', userLimit: '10' } }); + const license = { key: '123', validUntil: '2021-06-01', userLimit: '100' }; + + const newLicense = await SettingsCollection.replaceLicense(license); + + expect(newLicense).toEqual(license); + expect(db.getState().license).toEqual(license); + }); + }); }); }); diff --git a/packages/server/src/collections/SettingsCollection.ts b/packages/server/src/collections/SettingsCollection.ts index 1ee92dc..61343ec 100644 --- a/packages/server/src/collections/SettingsCollection.ts +++ b/packages/server/src/collections/SettingsCollection.ts @@ -61,12 +61,16 @@ export class SettingsCollection extends DbAdapterWithColKey { static async addLicense(key: string): Promise { try { const license = await LicenseService.queryLicense(key); - return this.set('license', license).then(() => this.getLicense()); + return this.replaceLicense(license); } catch (error) { return Promise.reject(error); } } + static async replaceLicense(license: t.License): Promise { + return this.set('license', license).then(() => this.getLicense()); + } + static async removeLicense(): Promise { return this.set('license', this.defaultLicense).then(() => this.getLicense()); } diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 573ea81..7e56bf2 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -22,6 +22,7 @@ declare global { namespace Express { interface Request { user: t.User; + license: t.License; } } } @@ -94,7 +95,9 @@ const app = express(); app.use(cors()); app.use(bodyParser.json()); -app.post('/api/login', (req, res) => { +app.post('/api/login', async (req, res) => { + await LicenseService.refreshLicense(); + UserCollection.loginUser(req.body.password) .then(result => res.status(200).json(result)) .catch(e => res.status(403).json(e.message)); @@ -119,8 +122,9 @@ app.use(server.graphqlPath, async (req, res, next) => { const token = req.headers.authorization || 'no_token'; if (process.env.NODE_ENV === 'production') { return UserCollection.verifyLogin(token) - .then(user => { + .then(async user => { req.user = user; + req.license = await SettingsCollection.getLicense(); next(); }) .catch(e => diff --git a/packages/server/src/services/LicenseService.test.ts b/packages/server/src/services/LicenseService.test.ts index 737d8fe..0cb9bfb 100644 --- a/packages/server/src/services/LicenseService.test.ts +++ b/packages/server/src/services/LicenseService.test.ts @@ -101,6 +101,48 @@ describe('License Service Test', () => { }); }); + describe('refreshLicense()', () => { + it('should not refresh license if default license', async () => { + const license = { key: '0', userLimit: '1', validUntil: '' }; + SettingsCollection.getLicense = jest.fn(() => Promise.resolve(license)); + const spy = jest.spyOn(LicenseService, 'queryLicense'); + + await LicenseService.refreshLicense(); + + expect(spy).toHaveBeenCalledTimes(0); + }); + + it('should refresh license from settings', async () => { + const license = { key: '1', userLimit: '10', validUntil: '2020-01-01' }; + SettingsCollection.getLicense = jest.fn(() => Promise.resolve(license)); + const newLicense = { key: '1', userLimit: '100', validUntil: '2022-01-01' }; + const licenseServiceSpy = jest + .spyOn(LicenseService, 'queryLicense') + .mockImplementation(() => Promise.resolve(newLicense)); + const settingsSpy = jest.spyOn(SettingsCollection, 'replaceLicense'); + + await LicenseService.refreshLicense(); + + expect(licenseServiceSpy).toHaveBeenCalledWith(license.key); + expect(settingsSpy).toHaveBeenCalledWith(newLicense); + }); + + it('should outdate license if license refresh fails', async () => { + const license = { key: '1', userLimit: '10', validUntil: '2020-01-01' }; + SettingsCollection.getLicense = jest.fn(() => Promise.resolve(license)); + const outdatedLicense = { key: '1', userLimit: '10', validUntil: '2000-01-01' }; + const licenseServiceSpy = jest.spyOn(LicenseService, 'queryLicense').mockImplementation(() => { + throw new Error('test'); + }); + const settingsSpy = jest.spyOn(SettingsCollection, 'replaceLicense'); + + await LicenseService.refreshLicense(); + + expect(licenseServiceSpy).toHaveBeenCalledWith(license.key); + expect(settingsSpy).toHaveBeenCalledWith(outdatedLicense); + }); + }); + describe('isProtectedOperation()', () => { it('should return true if operation is protected', () => { expect(LicenseService.isProtectedOperation('LoadPublicHolidays')).toBe(true); diff --git a/packages/server/src/services/LicenseService.ts b/packages/server/src/services/LicenseService.ts index 4d50b42..66b8751 100644 --- a/packages/server/src/services/LicenseService.ts +++ b/packages/server/src/services/LicenseService.ts @@ -37,6 +37,20 @@ export class LicenseService { }); } + static async refreshLicense(): Promise { + const license = await SettingsCollection.getLicense(); + if (license.key === '0') { + return; + } + try { + const refreshedLicense = await this.queryLicense(license.key); + await SettingsCollection.replaceLicense(refreshedLicense); + } catch { + license.validUntil = '2000-01-01'; + await SettingsCollection.replaceLicense(license); + } + } + static protectedOperations = [ 'LoadPublicHolidays', 'GetStatisticForDate', From bd20c72793f9a0fab27af144cd4d5f37ea8d2ac8 Mon Sep 17 00:00:00 2001 From: Victoria Teufel Date: Sat, 21 Mar 2020 20:46:16 +0100 Subject: [PATCH 6/6] Added license notice --- .../src/components/License/LicenseNotice.tsx | 37 +++++++++++++++++++ .../app/src/components/hoc/ProtectedRoute.tsx | 15 ++++++-- packages/app/src/i18n/de_DE.ts | 3 +- packages/app/src/i18n/en_US.ts | 3 +- packages/common/initialState.ts | 2 + 5 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 packages/app/src/components/License/LicenseNotice.tsx diff --git a/packages/app/src/components/License/LicenseNotice.tsx b/packages/app/src/components/License/LicenseNotice.tsx new file mode 100644 index 0000000..cdeb94d --- /dev/null +++ b/packages/app/src/components/License/LicenseNotice.tsx @@ -0,0 +1,37 @@ +import * as React from 'react'; +import * as t from 'common/types'; +import { useIntl } from 'react-intl'; +import * as moment from 'moment'; + +interface Props { + license: t.License; +} + +export const LicenseNotice = ({ license }: Props) => { + const intl = useIntl(); + + if (!license || !license.validUntil) { + return
; + } + + const validUntil = new Date(license.validUntil); + const diff = validUntil.getTime() - new Date().getTime(); + if (diff <= 0) { + return ( +
+ {intl.formatMessage({ id: 'LICENSE_EXPIRED' })} +
+ ); + } + + const oneWeek = 7 * 24 * 60 * 60 * 1000; + if (diff <= oneWeek) { + return ( +
+ {intl.formatMessage({ id: 'LICENSE_WILL_EXPIRE' }) + moment(license.validUntil).format('L')} +
+ ); + } + + return
; +}; diff --git a/packages/app/src/components/hoc/ProtectedRoute.tsx b/packages/app/src/components/hoc/ProtectedRoute.tsx index e628cfe..0631523 100644 --- a/packages/app/src/components/hoc/ProtectedRoute.tsx +++ b/packages/app/src/components/hoc/ProtectedRoute.tsx @@ -1,11 +1,12 @@ import { NavigationContainer } from 'components/Navigation/Navigation'; import { LoadingSpinner } from 'components/Spinner/LoadingSpinner'; import * as c from 'common/constants'; -import { initialUserState } from 'common/initialState'; +import { initialUserState, initialLicenseState } from 'common/initialState'; import * as React from 'react'; import { Redirect, Route, RouteProps } from 'react-router-dom'; import * as t from 'common/types'; import { Apollo } from './../../graphql'; +import { LicenseNotice } from './../License/LicenseNotice'; declare let MOCKLOGIN: boolean; interface Props extends RouteProps { @@ -15,12 +16,14 @@ interface Props extends RouteProps { interface State { user: t.User; + license: t.License; loading: boolean; } export class ProtectedRoute extends React.Component { state = { user: initialUserState, + license: initialLicenseState, loading: true }; @@ -29,7 +32,7 @@ export class ProtectedRoute extends React.Component { } logout = () => { - this.setState({ user: initialUserState }); + this.setState({ user: initialUserState, license: initialLicenseState }); localStorage.removeItem(c.LOCAL_STORAGE_KEY.TOKEN); Apollo.logout(); }; @@ -38,10 +41,15 @@ export class ProtectedRoute extends React.Component { const token = localStorage.getItem(c.LOCAL_STORAGE_KEY.TOKEN); if (token) { Apollo.verifyLogin(token) - .then(result => { + .then(async result => { if (result.data) { this.setState({ user: result.data.verifyLogin, loading: false }); } + + const licenseResult = await Apollo.getLicense(); + if (licenseResult.data) { + this.setState({ license: licenseResult.data.getLicense }); + } }) .catch(() => this.setState({ loading: false })); } else { @@ -58,6 +66,7 @@ export class ProtectedRoute extends React.Component { render={props => this.props.allowedRoles.includes(this.state.user.role) || MOCKLOGIN ? ( <> + diff --git a/packages/app/src/i18n/de_DE.ts b/packages/app/src/i18n/de_DE.ts index bfe735d..4a032e4 100644 --- a/packages/app/src/i18n/de_DE.ts +++ b/packages/app/src/i18n/de_DE.ts @@ -151,5 +151,6 @@ export const de_DE = { ADD_LICENSE: 'Lizenz hinzufügen', LICENSE_NOT_FOUND: 'Lizenz nicht gefunden!', LICENSE_EXPIRED: 'Lizenz abgelaufen!', - LICENSE_USER_LIMIT_EXCEEDED: 'Lizenz Benutzerlimit überschritten!' + LICENSE_USER_LIMIT_EXCEEDED: 'Lizenz Benutzerlimit überschritten!', + LICENSE_WILL_EXPIRE: 'Lizenz wird bald ablaufen: ' }; diff --git a/packages/app/src/i18n/en_US.ts b/packages/app/src/i18n/en_US.ts index bf69f29..9143e30 100644 --- a/packages/app/src/i18n/en_US.ts +++ b/packages/app/src/i18n/en_US.ts @@ -151,5 +151,6 @@ export const en_US = { ADD_LICENSE: 'Add License', LICENSE_NOT_FOUND: 'License not found!', LICENSE_EXPIRED: 'License expired!', - LICENSE_USER_LIMIT_EXCEEDED: 'License user limit exceeded!' + LICENSE_USER_LIMIT_EXCEEDED: 'License user limit exceeded!', + LICENSE_WILL_EXPIRE: 'License will expire soon: ' }; diff --git a/packages/common/initialState.ts b/packages/common/initialState.ts index bcba435..b92bfd8 100644 --- a/packages/common/initialState.ts +++ b/packages/common/initialState.ts @@ -19,3 +19,5 @@ export const initialUserState: t.User = { startDate: '', saldos: [] }; + +export const initialLicenseState: t.License = { key: '0', validUntil: '', userLimit: '1' };