diff --git a/.env.example b/.env.example
index 40612a5..a59e70d 100644
--- a/.env.example
+++ b/.env.example
@@ -1,38 +1,40 @@
-NODE_ENV = development
-PORT = 3001
-TZ=Europe/Amsterdam
-
-TYPEORM_CONNECTION = mysql
-TYPEORM_HOST = localhost
-TYPEORM_PORT = 5432
-TYPEORM_DATABASE = crm
-TYPEORM_USERNAME = parelpracht
-TYPEORM_PASSWORD =
-TYPEORM_SYNCHRONIZE = true
-TYPEORM_LOGGING = false
-TYPEORM_MIGRATIONS = dist/migration/**/*.js
-TYPEORM_SUBSCRIBERS = dist/subscriber/**/*.js
-
-SESSION_SECRET = secret
-
-MAIL_HOST =
-MAIL_PORT = 465
-MAIL_USER =
-MAIL_PASSWORD =
-MAIL_FROM =
-
-SERVER_HOST = http://localhost:3000
-
-# If these environment variables are left empty, LDAP will be disabled
-LDAP_URL =
-LDAP_BINDDN =
-LDAP_BINDCREDENTIALS =
-LDAP_SEARCHBASE =
-LDAP_SEARCHFILTER =
-
-# The following environment variables are used by the DirectMail plugin
-# This is optional and thus these variables can be removed (if not used)
-DIRECTMAIL_URL=
-DIRECTMAIL_USERNAME=parelpracht
-DIRECTMAIL_PASSWORD=
-DIRECTMAIL_PRODUCT_ID=
+NODE_ENV = development
+PORT = 3001
+TZ=Europe/Amsterdam
+
+TYPEORM_CONNECTION = mysql
+TYPEORM_HOST = 127.0.0.1
+TYPEORM_PORT = 3306
+TYPEORM_DATABASE = parelpracht
+TYPEORM_USERNAME = root
+TYPEORM_PASSWORD = parelpracht
+TYPEORM_SYNCHRONIZE = true
+TYPEORM_LOGGING = false
+TYPEORM_MIGRATIONS = dist/migration/**/*.js
+TYPEORM_SUBSCRIBERS = dist/subscriber/**/*.js
+TYPEORM_SSL_ENABLED = false
+TYPEORM_SSL_CACERTS = /etc/ssl/certs/ca-certificates.crt
+
+SESSION_SECRET = secret
+
+MAIL_HOST =
+MAIL_PORT = 465
+MAIL_USER =
+MAIL_PASSWORD =
+MAIL_FROM =
+
+SERVER_HOST = http://localhost:3000
+
+# If these environment variables are left empty, LDAP will be disabled
+LDAP_URL =
+LDAP_BINDDN =
+LDAP_BINDCREDENTIALS =
+LDAP_SEARCHBASE =
+LDAP_SEARCHFILTER =
+
+# The following environment variables are used by the DirectMail plugin
+# This is optional and thus these variables can be removed (if not used)
+DIRECTMAIL_URL=
+DIRECTMAIL_USERNAME=parelpracht
+DIRECTMAIL_PASSWORD=
+DIRECTMAIL_PRODUCT_ID=
diff --git a/.eslintrc.js b/.eslintrc.js
index dc7b769..15d3d4e 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -4,7 +4,7 @@ module.exports = {
plugins: ['@typescript-eslint', 'import'],
extends: ['airbnb-typescript/base'],
rules: {
- 'linebreak-style': ['error', 'windows'],
+ 'linebreak-style': ['error', 'unix'],
'lines-between-class-members': [
'error',
'always',
@@ -20,6 +20,7 @@ module.exports = {
'arrow-body-style': 'off',
'import/no-cycle': 'off',
'indent': 'off',
+ '@typescript-eslint/no-redeclare': 'off',
'@typescript-eslint/indent': [
'error',
2,
diff --git a/README.md b/README.md
index 98fe237..716525d 100644
--- a/README.md
+++ b/README.md
@@ -1,44 +1,48 @@
-
-
-
- ParelPracht
-
-
-ParelPracht is the successor of Goudglans, the custom Customer Relation Management system of Study Association GEWIS.
-This new system is built during the second lockdown of the corona pandemic.
-Its main goal is to automate tedious tasks and to keep a clear and concise overview of the current collaborations.
-This is achieved by creating nice structured insights tables and graphs and automating the generation of contracts, proposals and invoices.
-
-This is the back-end of ParelPracht. [The front-end can be found here](https://github.com/GEWIS/parelpracht-client).
-
-## Installation
-1. Clone the repository.
-2. Run `npm install`.
-3. Copy `.env.example` to `.env` and fill / replace the keys with their corresponding values. Note that the email-keys
- are important to be able to install the application (see step 5).
-4. Run `npm run dev`. This runs the client in development mode. Node will bind to port `3001`. You can find the API
- documentation at [http://localhost:3001/api/swagger-ui/](http://localhost:3001/api/swagger-ui/).
-5. Make a POST-request to `/v1/setup` with your credentials. The required payload can be found in the Swagger
- documentation. This request will create a local administrator account with the given credentials. You will receive an
- email (on the given address via the given mail server at step 3) to set your password.
-6. In the `VAT` table, add the desired VAT categories and percentages.
-
-You can also build the application with `npm run build`. This puts a production build in the `./build` directory.
-
-## Deployment
-1. Clone the repository in a folder called `parelpracht-client` and clone the backend repository in a folder called `parelpracht-server`. Make sure that both folders are in the same parent folder.
-2. Change the image locations to the correct locations in `docker-compose.yml` (for both the frontend and backend).
-3. Fill in the correct (environment) variables in `docker-compose.yml`.
-4. Run `docker-compose` in `./parelpracht-client`.
-
-## Copyright
-
-Copyright © 2022 The 39th board of GEWIS - Some rights reserved. Created by Roy Kakkenberg, Koen de Nooij, Jealy van den
-Aker, Max Opperman, Wouter van der Heijden en Irne Verwijst. You can use our software freely within the limits of
-our license. However, we worked very hard on this project and invested a lot of time in it, so we ask you to leave our
-copyright mark in place when modifying our software. Of course, you are free to add your own.
-
-## License
-[GNU AGPLv3](./LICENSE)
-
-
+
+
+
+ ParelPracht
+
+
+ParelPracht is the successor of Goudglans, the custom Customer Relation
+Management system of Study Association GEWIS. This new system is built during
+the second lockdown of the corona pandemic. Its main goal is to automate tedious
+tasks and to keep a clear and concise overview of the current collaborations.
+This is achieved by creating nice structured insights tables and graphs and
+automating the generation of contracts, proposals and invoices.
+
+This is the back-end of ParelPracht. The front-end can be found [here](https://github.com/GEWIS/parelpracht-client).
+
+## Development
+1. Clone the repository with `git clone git@github.com:GEWIS/parelpracht-server`
+2. Install the dependencies with `npm install`.
+3. Copy `.env.example` to `.env` and add the remaining environment variables.
+4. Start the application with `npm run dev`
+
+It is suggested to use a local MariaDB instance. If you do not have a local
+instance, you can use the docker compose file: `docker compose -f
+docker-compose-mariadb.yaml up -d`. The environment variables in the
+`.env.example` are adjusted to use this container configuration.
+
+## Setup
+When running the application, you will first need to create a superuser. This is
+done with the `/setup` endpoint.
+
+1. Go to the [swagger docs](http://localhost:3001/api/swagger-ui/).
+2. Navigate to `/setup` endpoint, and fill out the data for the request.
+3. Check the console for the confirmation link.
+
+Note: the confirmation link will only be logged in development mode. In
+production, an actual mail will be send with the confirmation link to the
+indicated email address.
+
+## Copyright
+Copyright © 2022 The 39th board of GEWIS - Some rights reserved. Created by Roy
+Kakkenberg, Koen de Nooij, Jealy van den Aker, Max Opperman, Wouter van der
+Heijden en Irne Verwijst. You can use our software freely within the limits of
+our license. However, we worked very hard on this project and invested a lot of
+time in it, so we ask you to leave our copyright mark in place when modifying
+our software. Of course, you are free to add your own.
+
+## License
+[GNU AGPLv3](./LICENSE)
diff --git a/docker-compose-mariadb.yaml b/docker-compose-mariadb.yaml
new file mode 100644
index 0000000..891bb0a
--- /dev/null
+++ b/docker-compose-mariadb.yaml
@@ -0,0 +1,10 @@
+services:
+ mariadb:
+ image: mariadb:latest
+ container_name: mariadb
+ environment:
+ MARIADB_ROOT_PASSWORD: parelpracht
+ MARIADB_DATABASE: parelpracht
+ ports:
+ - "3306:3306"
+
diff --git a/src/controllers/CompanyController.ts b/src/controllers/CompanyController.ts
index 0e20a18..22a7666 100644
--- a/src/controllers/CompanyController.ts
+++ b/src/controllers/CompanyController.ts
@@ -143,7 +143,7 @@ export class CompanyController extends Controller {
@Security('local', ['ADMIN'])
@Response(401)
public async deleteCompany(
- id: number, @Request() req: express.Request,
+ id: number,
): Promise {
return new CompanyService().deleteCompany(id);
}
diff --git a/src/controllers/ContactController.ts b/src/controllers/ContactController.ts
index 30aa63b..4e74deb 100644
--- a/src/controllers/ContactController.ts
+++ b/src/controllers/ContactController.ts
@@ -104,7 +104,7 @@ export class ContactController extends Controller {
@Security('local', ['GENERAL', 'ADMIN'])
@Response(401)
public async deleteContact(
- id: number, @Request() req: express.Request,
+ id: number,
): Promise {
return new ContactService().deleteContact(id);
}
diff --git a/src/controllers/ContractController.ts b/src/controllers/ContractController.ts
index f009291..7b0ed9f 100644
--- a/src/controllers/ContractController.ts
+++ b/src/controllers/ContractController.ts
@@ -169,7 +169,7 @@ export class ContractController extends Controller {
@Security('local', ['GENERAL', 'ADMIN'])
@Response(401)
public async deleteContract(
- id: number, @Request() req: express.Request,
+ id: number,
): Promise {
return new ContractService().deleteContract(id);
}
diff --git a/src/controllers/InvoiceController.ts b/src/controllers/InvoiceController.ts
index 2603a8a..fb55428 100644
--- a/src/controllers/InvoiceController.ts
+++ b/src/controllers/InvoiceController.ts
@@ -154,7 +154,7 @@ export class InvoiceController extends Controller {
@Security('local', ['GENERAL', 'ADMIN'])
@Response(401)
public async deleteInvoice(
- id: number, @Request() req: express.Request,
+ id: number,
): Promise {
return new InvoiceService().deleteInvoice(id);
}
diff --git a/src/controllers/ProductCategoryController.ts b/src/controllers/ProductCategoryController.ts
index 99b872f..4763059 100644
--- a/src/controllers/ProductCategoryController.ts
+++ b/src/controllers/ProductCategoryController.ts
@@ -1,5 +1,5 @@
import {
- Body, Controller, Delete, Get, Post, Put, Query, Request, Response, Route, Security, Tags,
+ Body, Controller, Delete, Get, Post, Put, Request, Response, Route, Security, Tags,
} from 'tsoa';
import express from 'express';
import { body } from 'express-validator';
@@ -98,7 +98,7 @@ export class ProductCategoryController extends Controller {
@Security('local', ['ADMIN'])
@Response(401)
public async deleteCategory(
- id: number, @Request() req: express.Request,
+ id: number,
): Promise {
return new ProductCategoryService().deleteCategory(id);
}
diff --git a/src/controllers/ProductController.ts b/src/controllers/ProductController.ts
index 74fd278..1ca72df 100644
--- a/src/controllers/ProductController.ts
+++ b/src/controllers/ProductController.ts
@@ -133,7 +133,7 @@ export class ProductController extends Controller {
@Security('local', ['GENERAL', 'ADMIN'])
@Response(401)
public async deleteProduct(
- id: number, @Request() req: express.Request,
+ id: number,
): Promise {
return new ProductService().deleteProduct(id);
}
diff --git a/src/controllers/RootController.ts b/src/controllers/RootController.ts
index 838292d..92d2d7c 100644
--- a/src/controllers/RootController.ts
+++ b/src/controllers/RootController.ts
@@ -1,8 +1,5 @@
import express from 'express';
-import {
- Body,
- Controller, Get, Post, Query, Request, Response, Route, Security,
-} from 'tsoa';
+import { Body, Controller, Get, Post, Query, Request, Response, Route, Security } from 'tsoa';
import { body } from 'express-validator';
import { WrappedApiError } from '../helpers/error';
import { validate } from '../helpers/validation';
@@ -23,13 +20,18 @@ export interface GeneralPrivateInfo {
export interface GeneralPublicInfo {
loginMethod: LoginMethods;
+ setupDone: boolean,
}
@Route('')
export class RootController extends Controller {
@Post('setup')
- public async postSetup(@Body() params: SetupParams): Promise {
- return new ServerSettingsService().initialSetup(params);
+ public async postSetup(@Body() params: SetupParams, @Request() req: express.Request): Promise {
+ const user = await new ServerSettingsService().initialSetup(params);
+ if (user === undefined) {
+ return;
+ }
+ await new AuthService().login(user, req);
}
@Get('authStatus')
@@ -105,7 +107,7 @@ export class RootController extends Controller {
@Get('getPublicGeneralInfo')
@Response(400)
- public getPublicGeneralInfo(): GeneralPublicInfo {
+ public async getPublicGeneralInfo(): Promise {
let loginMethod: LoginMethods;
if (ldapEnabled()) {
loginMethod = 'ldap';
@@ -113,8 +115,10 @@ export class RootController extends Controller {
loginMethod = 'local';
}
+ const setupDone: boolean = (await new ServerSettingsService().getSetting('SETUP_DONE'))?.value === 'true';
return {
loginMethod,
+ setupDone,
};
}
}
diff --git a/src/controllers/VATController.ts b/src/controllers/VATController.ts
index f6cf9c6..a1d77b2 100644
--- a/src/controllers/VATController.ts
+++ b/src/controllers/VATController.ts
@@ -1,5 +1,5 @@
import {
- Body, Controller, Get, Post, Put, Query, Request, Response, Route, Security, Tags,
+ Body, Controller, Get, Post, Put, Request, Response, Route, Security, Tags,
} from 'tsoa';
import express from 'express';
import { body } from 'express-validator';
diff --git a/src/database.ts b/src/database.ts
index 94e94b5..8c65000 100644
--- a/src/database.ts
+++ b/src/database.ts
@@ -1,4 +1,5 @@
import { DataSource } from 'typeorm';
+import fs from 'fs';
import { Company } from './entity/Company';
import { Contact } from './entity/Contact';
import { Contract } from './entity/Contract';
@@ -32,6 +33,13 @@ const AppDataSource = new DataSource({
type: process.env.TYPEORM_CONNECTION as 'postgres' | 'mariadb' | 'mysql',
username: process.env.TYPEORM_USERNAME,
password: process.env.TYPEORM_PASSWORD,
+ ...(process.env.TYPEORM_SSL_ENABLED === 'true' && process.env.TYPEORM_SSL_CACERTS
+ ? {
+ ssl: {
+ ca: fs.readFileSync(process.env.TYPEORM_SSL_CACERTS),
+ },
+ }
+ : {}),
synchronize: process.env.TYPEORM_SYNCHRONIZE === 'true',
logging: process.env.TYPEORM_LOGGING === 'true',
entities: [Company, Contact, Contract, IdentityApiKey, IdentityLDAP, IdentityLocal, Invoice, Product,
diff --git a/src/entity/activity/ContractActivity.ts b/src/entity/activity/ContractActivity.ts
index d2ceffd..33fff65 100644
--- a/src/entity/activity/ContractActivity.ts
+++ b/src/entity/activity/ContractActivity.ts
@@ -15,7 +15,7 @@ export class ContractActivity extends BaseActivity {
/** Contract related to this activity */
@ManyToOne(() => Contract, (contract) => contract.activities, {
onDelete: 'CASCADE',
- })
+ })
@JoinColumn({ name: 'contractId' })
contract!: Contract;
@@ -25,7 +25,7 @@ export class ContractActivity extends BaseActivity {
enum: ContractStatus,
nullable: true,
update: false,
- })
+ })
subType!: ContractStatus | null;
getRelatedEntity(): BaseEnt {
diff --git a/src/entity/activity/InvoiceActivity.ts b/src/entity/activity/InvoiceActivity.ts
index 05371f1..d21e6e1 100644
--- a/src/entity/activity/InvoiceActivity.ts
+++ b/src/entity/activity/InvoiceActivity.ts
@@ -17,7 +17,7 @@ export class InvoiceActivity extends BaseActivity {
/** Invoice related to this activity */
@ManyToOne(() => Invoice, (invoice) => invoice.activities, {
onDelete: 'CASCADE',
- })
+ })
@JoinColumn({ name: 'invoiceId' })
invoice!: Invoice;
@@ -27,7 +27,7 @@ export class InvoiceActivity extends BaseActivity {
enum: InvoiceStatus,
nullable: true,
update: false,
- })
+ })
subType!: InvoiceStatus | null;
getRelatedEntity(): BaseEnt {
diff --git a/src/entity/activity/ProductInstanceActivity.ts b/src/entity/activity/ProductInstanceActivity.ts
index 8df65da..fefe258 100644
--- a/src/entity/activity/ProductInstanceActivity.ts
+++ b/src/entity/activity/ProductInstanceActivity.ts
@@ -17,7 +17,7 @@ export class ProductInstanceActivity extends BaseActivity {
/** ProductInstance related to this activity */
@ManyToOne(() => ProductInstance, (productInstance) => productInstance.activities, {
onDelete: 'CASCADE',
- })
+ })
@JoinColumn({ name: 'productInstanceId' })
productInstance!: ProductInstance;
@@ -27,7 +27,7 @@ export class ProductInstanceActivity extends BaseActivity {
enum: ProductInstanceStatus,
nullable: true,
update: false,
- })
+ })
subType!: ProductInstanceStatus | null;
getRelatedEntity(): BaseEnt {
diff --git a/src/helpers/filters.ts b/src/helpers/filters.ts
index 6fa092a..29b01d6 100644
--- a/src/helpers/filters.ts
+++ b/src/helpers/filters.ts
@@ -1,4 +1,4 @@
-import { FindOptionsWhere, ILike, In } from 'typeorm';
+import { Brackets, FindOptionsWhere, ILike, In, SelectQueryBuilder } from 'typeorm';
import { BaseEnt } from '../entity/BaseEnt';
import { ListOrFilter, ListParams } from '../controllers/ListParams';
@@ -76,3 +76,62 @@ export function addQueryWhereClause(params: ListParams, searc
return conditions.length > 0 ? conditions : undefined;
}
+
+export function addQueryBuilderFilters(queryBuilder: SelectQueryBuilder, filters: ListOrFilter[]) {
+ filters.forEach(({ column, values }: ListOrFilter, index: number) => {
+ // only allows for one level deep relations
+ if (column.includes('.')) {
+ const alias = column.split('.')[0];
+
+ // only join if alias is not yet joined
+ const aliasExists = queryBuilder.expressionMap.aliases.some((a) => a.name === alias);
+ if (!aliasExists) {
+ if (alias === 'activities') {
+ // subquery to join only the latest related activity
+ queryBuilder.innerJoin(
+ `${queryBuilder.alias}.${alias}`,
+ alias,
+ `${alias}.createdAt = (
+ SELECT MAX(subQuery.updatedAt)
+ FROM ${queryBuilder.alias}_activity subQuery
+ WHERE subQuery.${queryBuilder.alias}Id = ${queryBuilder.alias}.id AND subQuery.subType IS NOT NULL
+ )`);
+ } else {
+ queryBuilder.innerJoin(`${queryBuilder.alias}.${alias}`, alias);
+ }
+ }
+ }
+ const paramName = `param_${index}`;
+ // avoid ambiguity in the where clause
+ if (!column.includes('.')) {
+ column = `${queryBuilder.alias}.${column}`;
+ }
+ queryBuilder.andWhere(`${column} IN (:...${paramName})`, { [paramName]: values });
+ });
+}
+
+export function addQueryBuilderSearch(queryBuilder: SelectQueryBuilder, searchString: string, searchFields: string[]) {
+ searchFields.forEach((field) => {
+ // only allows for one level deep relations
+ if (field.includes('.')) {
+ const alias = field.split('.')[0];
+
+ // only join if alias is not yet joined
+ const aliasExists = queryBuilder.expressionMap.aliases.some((a) => a.name === alias);
+ if (!aliasExists) {
+ queryBuilder.innerJoin(`${queryBuilder.alias}.${alias}`, alias);
+ }
+ }
+ });
+ queryBuilder.andWhere(
+ new Brackets((qb) => {
+ searchFields.forEach(field => {
+ // avoid ambiguity in the where clause
+ if (!field.includes('.')) {
+ field = `${queryBuilder.alias}.${field}`;
+ }
+ qb.orWhere(`LOWER(${field}) LIKE LOWER(:value)`, { value: `%${searchString}%` });
+ });
+ }),
+ );
+}
diff --git a/src/pdfgenerator/PdfGenerator.ts b/src/pdfgenerator/PdfGenerator.ts
index 601e682..28d5328 100644
--- a/src/pdfgenerator/PdfGenerator.ts
+++ b/src/pdfgenerator/PdfGenerator.ts
@@ -169,7 +169,7 @@ export default class PdfGenerator {
template = this.replaceAllSafe(template, '{{senderfunction}}', sender.function);
template = replaceAll(template, '{{dateday}}', date.getDate().toString());
- template = replaceAll(template, '{{datemonth}}', (date.getMonth()+1).toString());
+ template = replaceAll(template, '{{datemonth}}', (date.getMonth() + 1).toString());
template = replaceAll(template, '{{dateyear}}', date.getFullYear().toString());
if (useInvoiceAddress) {
@@ -196,7 +196,7 @@ export default class PdfGenerator {
let dueDate = new Date(date);
dueDate.setDate(date.getDate() + 30);
template = replaceAll(template, '{{dueday}}', dueDate.getDate().toString());
- template = replaceAll(template, '{{duemonth}}', (dueDate.getMonth()+1).toString());
+ template = replaceAll(template, '{{duemonth}}', (dueDate.getMonth() + 1).toString());
template = replaceAll(template, '{{dueyear}}', dueDate.getFullYear().toString());
return template;
@@ -416,7 +416,7 @@ export default class PdfGenerator {
let dueDate = new Date(invoice.startDate);
dueDate.setDate(invoice.startDate.getDate() + 30);
file = replaceAll(file, '{{dueday}}', dueDate.getDate().toString());
- file = replaceAll(file, '{{duemonth}}', (dueDate.getMonth()+1).toString());
+ file = replaceAll(file, '{{duemonth}}', (dueDate.getMonth() + 1).toString());
file = replaceAll(file, '{{dueyear}}', dueDate.getFullYear().toString());
file = replaceAll(file, '{{debtornumber}}', `C${settings.recipient.id}`);
@@ -478,7 +478,7 @@ export default class PdfGenerator {
let dueDate = new Date(params.date);
dueDate.setDate(params.date.getDate() + 30);
file = replaceAll(file, '{{dueday}}', dueDate.getDate().toString());
- file = replaceAll(file, '{{duemonth}}', (dueDate.getMonth()+1).toString());
+ file = replaceAll(file, '{{duemonth}}', (dueDate.getMonth() + 1).toString());
file = replaceAll(file, '{{dueyear}}', dueDate.getFullYear().toString());
file = replaceAll(file, '{{debtornumber}}', params.recipient.number);
diff --git a/src/routes.ts b/src/routes.ts
index 352d82b..8125983 100644
--- a/src/routes.ts
+++ b/src/routes.ts
@@ -1176,6 +1176,8 @@ const models: TsoaRoute.Models = {
"lastName": {"dataType":"string","required":true},
"function": {"dataType":"string","required":true},
"gender": {"ref":"Gender","required":true},
+ "password": {"dataType":"string","required":true},
+ "rememberMe": {"dataType":"boolean","required":true},
"replyToEmail": {"dataType":"string"},
"receiveEmails": {"dataType":"boolean"},
"sendEmailsToReplyToEmail": {"dataType":"boolean"},
@@ -1257,6 +1259,7 @@ const models: TsoaRoute.Models = {
"dataType": "refObject",
"properties": {
"loginMethod": {"ref":"LoginMethods","required":true},
+ "setupDone": {"dataType":"boolean","required":true},
},
"additionalProperties": false,
},
@@ -1287,7 +1290,7 @@ const models: TsoaRoute.Models = {
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
"Partial_UserParams_": {
"dataType": "refAlias",
- "type": {"dataType":"nestedObjectLiteral","nestedProperties":{"email":{"dataType":"string"},"firstName":{"dataType":"string"},"lastNamePreposition":{"dataType":"string"},"lastName":{"dataType":"string"},"function":{"dataType":"string"},"gender":{"ref":"Gender"},"replyToEmail":{"dataType":"string"},"receiveEmails":{"dataType":"boolean"},"sendEmailsToReplyToEmail":{"dataType":"boolean"},"comment":{"dataType":"string"},"ldapOverrideEmail":{"dataType":"boolean"},"roles":{"dataType":"array","array":{"dataType":"refEnum","ref":"Roles"}}},"validators":{}},
+ "type": {"dataType":"nestedObjectLiteral","nestedProperties":{"email":{"dataType":"string"},"firstName":{"dataType":"string"},"lastNamePreposition":{"dataType":"string"},"lastName":{"dataType":"string"},"function":{"dataType":"string"},"gender":{"ref":"Gender"},"password":{"dataType":"string"},"rememberMe":{"dataType":"boolean"},"replyToEmail":{"dataType":"string"},"receiveEmails":{"dataType":"boolean"},"sendEmailsToReplyToEmail":{"dataType":"boolean"},"comment":{"dataType":"string"},"ldapOverrideEmail":{"dataType":"boolean"},"roles":{"dataType":"array","array":{"dataType":"refEnum","ref":"Roles"}}},"validators":{}},
},
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
"TransferUserParams": {
@@ -1601,7 +1604,6 @@ export function RegisterRoutes(app: Router) {
function CompanyController_deleteCompany(request: any, response: any, next: any) {
const args = {
id: {"in":"path","name":"id","required":true,"dataType":"double"},
- req: {"in":"request","name":"req","required":true,"dataType":"object"},
};
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
@@ -2085,7 +2087,6 @@ export function RegisterRoutes(app: Router) {
function ContactController_deleteContact(request: any, response: any, next: any) {
const args = {
id: {"in":"path","name":"id","required":true,"dataType":"double"},
- req: {"in":"request","name":"req","required":true,"dataType":"object"},
};
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
@@ -2270,7 +2271,6 @@ export function RegisterRoutes(app: Router) {
function ContractController_deleteContract(request: any, response: any, next: any) {
const args = {
id: {"in":"path","name":"id","required":true,"dataType":"double"},
- req: {"in":"request","name":"req","required":true,"dataType":"object"},
};
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
@@ -2930,7 +2930,6 @@ export function RegisterRoutes(app: Router) {
function InvoiceController_deleteInvoice(request: any, response: any, next: any) {
const args = {
id: {"in":"path","name":"id","required":true,"dataType":"double"},
- req: {"in":"request","name":"req","required":true,"dataType":"object"},
};
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
@@ -3394,7 +3393,6 @@ export function RegisterRoutes(app: Router) {
function ProductCategoryController_deleteCategory(request: any, response: any, next: any) {
const args = {
id: {"in":"path","name":"id","required":true,"dataType":"double"},
- req: {"in":"request","name":"req","required":true,"dataType":"object"},
};
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
@@ -3579,7 +3577,6 @@ export function RegisterRoutes(app: Router) {
function ProductController_deleteProduct(request: any, response: any, next: any) {
const args = {
id: {"in":"path","name":"id","required":true,"dataType":"double"},
- req: {"in":"request","name":"req","required":true,"dataType":"object"},
};
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
@@ -4065,6 +4062,7 @@ export function RegisterRoutes(app: Router) {
function RootController_postSetup(request: any, response: any, next: any) {
const args = {
params: {"in":"body","name":"params","required":true,"ref":"SetupParams"},
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
};
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
diff --git a/src/services/AuthService.ts b/src/services/AuthService.ts
index b68fe54..5c1b92d 100644
--- a/src/services/AuthService.ts
+++ b/src/services/AuthService.ts
@@ -85,6 +85,17 @@ export default class AuthService {
});
}
+ login(user: User, req: express.Request) {
+ return new Promise((resolve, reject) => {
+ req.logIn(user, (err) => {
+ if (err) {
+ return reject(err);
+ }
+ return resolve();
+ });
+ });
+ }
+
async forgotPassword(userEmail: string): Promise {
let email = validator.normalizeEmail(userEmail);
if (email === false) {
diff --git a/src/services/ContractService.ts b/src/services/ContractService.ts
index 376d6ab..a99bb19 100644
--- a/src/services/ContractService.ts
+++ b/src/services/ContractService.ts
@@ -1,12 +1,11 @@
import {
- FindManyOptions, Repository,
+ Repository,
} from 'typeorm';
import { ListParams } from '../controllers/ListParams';
import { Contract } from '../entity/Contract';
import { User } from '../entity/User';
import { ApiError, HTTPStatus } from '../helpers/error';
import { ContractActivity } from '../entity/activity/ContractActivity';
-// eslint-disable-next-line import/no-cycle
import ActivityService, { FullActivityParams } from './ActivityService';
import ContactService from './ContactService';
import CompanyService from './CompanyService';
@@ -15,7 +14,7 @@ import { CompanyStatus } from '../entity/enums/CompanyStatus';
import { ActivityType } from '../entity/enums/ActivityType';
import RawQueries, { RecentContract } from '../helpers/rawQueries';
import { ContractStatus } from '../entity/enums/ContractStatus';
-import { addQueryWhereClause } from '../helpers/filters';
+import { addQueryBuilderFilters, addQueryBuilderSearch } from '../helpers/filters';
import { Roles } from '../entity/enums/Roles';
import { ContractSummary } from '../entity/Summaries';
import {
@@ -59,22 +58,25 @@ export default class ContractService {
}
async getAllContracts(params: ListParams): Promise {
- const findOptions: FindManyOptions = {
- order: {
- [params.sorting?.column ?? 'id']:
- params.sorting?.direction ?? 'ASC',
- },
- };
+ const queryBuilder = this.repo.createQueryBuilder('contract');
+
+ queryBuilder
+ .orderBy(`${queryBuilder.alias}.${params.sorting?.column ?? 'id'}`, params.sorting?.direction ?? 'ASC')
+ // initial where to allow chaining andWhere() function calls
+ .where('1 = 1')
+ ;
- findOptions.where = addQueryWhereClause(params, ['title', 'company.name', 'contact.firstName', 'contact.lastNamePreposition', 'contact.lastName']);
+ if (params.search) {
+ addQueryBuilderSearch(queryBuilder, params.search, ['title', 'company.name', 'contact.firstName', 'contact.lastNamePreposition', 'contact.lastName']);
+ }
+
+ if (params.filters && params.filters.length > 0) {
+ addQueryBuilderFilters(queryBuilder, params.filters);
+ }
return {
- list: await this.repo.find({
- ...findOptions,
- skip: params.skip,
- take: params.take,
- }),
- count: await this.repo.count(findOptions),
+ list: await queryBuilder.skip(params.skip).take(params.take).getMany(),
+ count: await queryBuilder.getCount(),
};
}
diff --git a/src/services/InvoiceService.ts b/src/services/InvoiceService.ts
index 3bd3987..1186886 100644
--- a/src/services/InvoiceService.ts
+++ b/src/services/InvoiceService.ts
@@ -1,12 +1,12 @@
import {
- FindManyOptions, Repository,
+ Repository,
} from 'typeorm';
import { ListParams } from '../controllers/ListParams';
import { Invoice } from '../entity/Invoice';
import { ProductInstance } from '../entity/ProductInstance';
import { User } from '../entity/User';
import { ApiError, HTTPStatus } from '../helpers/error';
-import { addQueryWhereClause } from '../helpers/filters';
+import { addQueryBuilderFilters, addQueryBuilderSearch } from '../helpers/filters';
import ProductInstanceService from './ProductInstanceService';
import ActivityService, { FullActivityParams } from './ActivityService';
import RawQueries, { ExpiredInvoice } from '../helpers/rawQueries';
@@ -61,23 +61,25 @@ export default class InvoiceService {
}
async getAllInvoices(params: ListParams): Promise {
- const findOptions: FindManyOptions = {
- order: {
- [params.sorting?.column ?? 'id']:
- params.sorting?.direction ?? 'ASC',
- },
- };
+ const queryBuilder = this.repo.createQueryBuilder('invoice');
+
+ queryBuilder
+ .orderBy(`${queryBuilder.alias}.${params.sorting?.column ?? 'id'}`, params.sorting?.direction ?? 'ASC')
+ // initial where() to allow chaining andWhere() function calls
+ .where('1 = 1')
+ ;
- findOptions.where = addQueryWhereClause(params, ['title', 'company.name']);
+ if (params.search) {
+ addQueryBuilderSearch(queryBuilder, params.search, ['title', 'company.name']);
+ }
+
+ if (params.filters && params.filters.length > 0) {
+ addQueryBuilderFilters(queryBuilder, params.filters);
+ }
return {
- list: await this.repo.find({
- ...findOptions,
- skip: params.skip,
- take: params.take,
- }),
- count: await this.repo.count(findOptions),
- lastSeen: await this.getTreasurerLastSeen(),
+ list: await queryBuilder.skip(params.skip).take(params.take).getMany(),
+ count: await queryBuilder.getCount(),
};
}
diff --git a/src/services/ServerSettingsService.ts b/src/services/ServerSettingsService.ts
index 2717e4d..a7ff1b6 100644
--- a/src/services/ServerSettingsService.ts
+++ b/src/services/ServerSettingsService.ts
@@ -7,6 +7,7 @@ import AuthService from './AuthService';
import UserService, { UserParams } from './UserService';
import { ldapEnabled } from '../auth';
import AppDataSource from '../database';
+import { User } from '../entity/User';
export interface SetupParams {
admin: UserParams,
@@ -29,7 +30,7 @@ export default class ServerSettingsService {
async initialSetup(
params: SetupParams,
- ): Promise {
+ ): Promise {
if ((await this.getSetting('SETUP_DONE'))?.value === 'true') {
throw new ApiError(HTTPStatus.Forbidden, 'Server is already set up');
}
@@ -39,9 +40,14 @@ export default class ServerSettingsService {
const adminUser = await new UserService()
.createAdminUser(admin);
- new AuthService().createIdentityLocal(adminUser!, ldapEnabled());
+ const authService = new AuthService();
+
+ const identity = await authService.createIdentityLocal(adminUser!, true);
+ await authService.resetPassword(params.admin.password, authService.getSetPasswordToken(adminUser!, identity));
await this.setSetting({ name: 'SETUP_DONE', value: 'true' });
+ return adminUser!;
}
+ return undefined;
}
}
diff --git a/src/services/StatisticsService.ts b/src/services/StatisticsService.ts
index b8acdcf..79f3977 100644
--- a/src/services/StatisticsService.ts
+++ b/src/services/StatisticsService.ts
@@ -40,13 +40,6 @@ function rangeToArray(start: number, end: number, step: number): number[] {
return result;
}
-function appendZeroesToStart(array: number[], newLength: number) {
- while (array.length < newLength) {
- array.splice(0, 0, 0);
- }
- return array;
-}
-
export default class StatisticsService {
public async getFinancialYears(firstYear?: number): Promise {
if (firstYear) return rangeToArray(firstYear, dateToFinancialYear(new Date()), 1);
diff --git a/src/services/UserService.ts b/src/services/UserService.ts
index 5e7c478..c29071f 100644
--- a/src/services/UserService.ts
+++ b/src/services/UserService.ts
@@ -17,6 +17,7 @@ import FileHelper, { uploadUserAvatarDirLoc, uploadUserBackgroundDirLoc } from '
import { IdentityLDAP } from '../entity/IdentityLDAP';
import { ldapEnabled } from '../auth';
import AppDataSource from '../database';
+import validator from 'validator';
export interface UserParams {
email: string;
@@ -25,6 +26,8 @@ export interface UserParams {
lastName: string;
function: string;
gender: Gender;
+ password: string;
+ rememberMe: boolean;
replyToEmail?: string;
receiveEmails?: boolean;
sendEmailsToReplyToEmail?: boolean;
@@ -164,6 +167,10 @@ export default class UserService {
async createAdminUser(params: UserParams): Promise {
if (ldapEnabled()) return Promise.resolve(undefined);
+ if (!this.validateUserParams(params)) {
+ throw new ApiError(HTTPStatus.BadRequest, 'The supplied parameters are not valid');
+ }
+
const adminUser = await this.repo.save({
email: params.email,
gender: params.gender,
@@ -210,6 +217,15 @@ export default class UserService {
return user!;
}
+ private validateUserParams(params: UserParams): boolean {
+ return validator.isStrongPassword(params.password) &&
+ validator.isEmail(params.email) &&
+ !validator.isEmpty(params.firstName) &&
+ !validator.isEmpty(params.lastName) &&
+ !validator.isEmpty(params.email) &&
+ !validator.isEmpty(params.gender);
+ }
+
async setupRoles() {
await this.roleRepo.save(
this.roleRepo.create([