Compare commits

..

19 commits
v1.7.2 ... main

Author SHA1 Message Date
20ee77632e 1.7.6 2025-07-24 08:57:47 +02:00
817aa02f7f Merge pull request 'patches v1.7.6' (#129) from develop into main
Reviewed-on: #129
2025-07-24 06:56:39 +00:00
2f305ec21d Merge branch 'main' into develop 2025-07-24 06:56:07 +00:00
2277e55d64 fix: invite parameter check 2025-07-24 08:51:54 +02:00
051bf3d4ca fix: none existing membership view entry 2025-07-24 08:10:41 +02:00
d644a5732f fix: allow admin user creation on setup 2025-07-24 07:53:17 +02:00
f71c744faf 1.7.5 2025-07-19 11:09:12 +02:00
5c9f4a8a97 Merge pull request 'patches v1.7.5' (#127) from develop into main
Reviewed-on: #127
2025-07-19 09:07:10 +00:00
03be94aa10 Merge branch 'main' into develop 2025-07-19 09:07:02 +00:00
6bd4ca3508 package update and CVE close 2025-07-19 11:05:58 +02:00
97c19fa54f 1.7.4 2025-07-16 12:49:07 +02:00
01723d9bf2 Merge pull request 'patches v1.7.4' (#123) from develop into main
Reviewed-on: #123
2025-07-16 10:47:57 +00:00
5fdeae80e1 Merge branch 'main' into develop 2025-07-16 10:47:34 +00:00
399c4d84a1 change: relation of newsletter receivers to query 2025-07-14 16:12:12 +02:00
013cb9f95d fix: compatability with old querybuilder objects 2025-07-14 16:05:18 +02:00
cab3dd83d5 1.7.3 2025-07-14 14:29:54 +02:00
d74286a2a3 Merge pull request 'patches v1.7.3' (#120) from develop into main
Reviewed-on: #120
2025-07-14 12:29:06 +00:00
f7353fb164 Merge branch 'main' into develop 2025-07-14 12:28:59 +00:00
cc0a263325 fix: more specific columns for sort 2025-07-14 14:28:48 +02:00
26 changed files with 134 additions and 72 deletions

94
package-lock.json generated
View file

@ -1,19 +1,19 @@
{
"name": "ff-admin-server",
"version": "1.7.2",
"version": "1.7.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ff-admin-server",
"version": "1.7.2",
"version": "1.7.6",
"license": "AGPL-3.0-only",
"dependencies": {
"cors": "^2.8.5",
"crypto": "^1.0.1",
"dotenv": "^17.2.0",
"express": "^5.1.0",
"express-rate-limit": "^7.5.1",
"express-rate-limit": "^8.0.1",
"express-validator": "^7.2.1",
"handlebars": "^4.7.8",
"helmet": "^8.1.0",
@ -23,14 +23,14 @@
"lodash.clonedeep": "^4.5.0",
"lodash.uniqby": "^4.7.0",
"moment": "^2.30.1",
"morgan": "^1.10.0",
"morgan": "^1.10.1",
"ms": "^2.1.3",
"multer": "^2.0.1",
"multer": "^2.0.2",
"node-schedule": "^2.1.1",
"nodemailer": "^7.0.5",
"pdf-lib": "^1.17.1",
"pg": "^8.16.3",
"puppeteer": "^24.12.1",
"puppeteer": "^24.14.0",
"qrcode": "^1.5.4",
"reflect-metadata": "^0.2.2",
"rss-parser": "^3.13.0",
@ -52,7 +52,7 @@
"@types/morgan": "^1.9.10",
"@types/ms": "^2.1.0",
"@types/multer": "^2.0.0",
"@types/node": "^24.0.13",
"@types/node": "^24.0.15",
"@types/node-schedule": "^2.1.8",
"@types/nodemailer": "^6.4.17",
"@types/pg": "~8.15.4",
@ -744,9 +744,9 @@
}
},
"node_modules/@puppeteer/browsers": {
"version": "2.10.5",
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.5.tgz",
"integrity": "sha512-eifa0o+i8dERnngJwKrfp3dEq7ia5XFyoqB17S4gK8GhsQE4/P8nxOfQSE0zQHxzzLo/cmF+7+ywEQ7wK7Fb+w==",
"version": "2.10.6",
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.6.tgz",
"integrity": "sha512-pHUn6ZRt39bP3698HFQlu2ZHCkS/lPcpv7fVQcGBSzNNygw171UXAKrCUhy+TEMw4lEttOKDgNpb04hwUAJeiQ==",
"license": "Apache-2.0",
"dependencies": {
"debug": "^4.4.1",
@ -754,7 +754,7 @@
"progress": "^2.0.3",
"proxy-agent": "^6.5.0",
"semver": "^7.7.2",
"tar-fs": "^3.0.8",
"tar-fs": "^3.1.0",
"yargs": "^17.7.2"
},
"bin": {
@ -966,9 +966,9 @@
}
},
"node_modules/@types/node": {
"version": "24.0.13",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.13.tgz",
"integrity": "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ==",
"version": "24.0.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.15.tgz",
"integrity": "sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.8.0"
@ -1686,9 +1686,9 @@
}
},
"node_modules/chromium-bidi": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-5.1.0.tgz",
"integrity": "sha512-9MSRhWRVoRPDG0TgzkHrshFSJJNZzfY5UFqUMuksg7zL1yoZIZ3jLB0YAgHclbiAxPI86pBnwDX1tbzoiV8aFw==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-7.1.1.tgz",
"integrity": "sha512-L2BKQ0rSLADgbPMIdDh3wnYHs3EiUiMay2Sq0CTolheaADmWIf6Pe+T9LJRcnh5rcMz0U7MVk0cQVvKsGRMa1g==",
"license": "Apache-2.0",
"dependencies": {
"mitt": "^3.0.1",
@ -2483,10 +2483,13 @@
}
},
"node_modules/express-rate-limit": {
"version": "7.5.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz",
"integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==",
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.0.1.tgz",
"integrity": "sha512-aZVCnybn7TVmxO4BtlmnvX+nuz8qHW124KKJ8dumsBsmv5ZLxE0pYu7S2nwyRBGHHCAzdmnGyrc5U/rksSPO7Q==",
"license": "MIT",
"dependencies": {
"ip-address": "10.0.1"
},
"engines": {
"node": ">= 16"
},
@ -2497,6 +2500,15 @@
"express": ">= 4.11"
}
},
"node_modules/express-rate-limit/node_modules/ip-address": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/express-validator": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.1.tgz",
@ -3673,16 +3685,16 @@
}
},
"node_modules/morgan": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
"integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==",
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
"integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==",
"license": "MIT",
"dependencies": {
"basic-auth": "~2.0.1",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-finished": "~2.3.0",
"on-headers": "~1.0.2"
"on-headers": "~1.1.0"
},
"engines": {
"node": ">= 0.8.0"
@ -3722,9 +3734,9 @@
"license": "MIT"
},
"node_modules/multer": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.0.1.tgz",
"integrity": "sha512-Ug8bXeTIUlxurg8xLTEskKShvcKDZALo1THEX5E41pYCD2sCVub5/kIRIGqWNoqV6szyLyQKV6mD4QUrWE5GCQ==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz",
"integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==",
"license": "MIT",
"dependencies": {
"append-field": "^1.0.0",
@ -3972,9 +3984,9 @@
}
},
"node_modules/on-headers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
@ -4510,17 +4522,17 @@
}
},
"node_modules/puppeteer": {
"version": "24.12.1",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.12.1.tgz",
"integrity": "sha512-+vvwl+Xo4z5uXLLHG+XW8uXnUXQ62oY6KU6bEFZJvHWLutbmv5dw9A/jcMQ0fqpQdLydHmK0Uy7/9Ilj8ufwSQ==",
"version": "24.14.0",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.14.0.tgz",
"integrity": "sha512-GB7suRDkp9pUnxpNGAORICQCtw11KFbg6U2iJXVTflzJLK5D1qzq8xOOmLgN/QnDBpDMdpn96ri52XkuN83Giw==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "2.10.5",
"chromium-bidi": "5.1.0",
"@puppeteer/browsers": "2.10.6",
"chromium-bidi": "7.1.1",
"cosmiconfig": "^9.0.0",
"devtools-protocol": "0.0.1464554",
"puppeteer-core": "24.12.1",
"puppeteer-core": "24.14.0",
"typed-query-selector": "^2.12.0"
},
"bin": {
@ -4531,13 +4543,13 @@
}
},
"node_modules/puppeteer-core": {
"version": "24.12.1",
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.12.1.tgz",
"integrity": "sha512-8odp6d3ERKBa3BAVaYWXn95UxQv3sxvP1reD+xZamaX6ed8nCykhwlOiHSaHR9t/MtmIB+rJmNencI6Zy4Gxvg==",
"version": "24.14.0",
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.14.0.tgz",
"integrity": "sha512-NO9XpCl+i8oB0zJp81iPhzMo2QK8/JTj4ramSvTpGCo9CPCNo4AZ8qVOGpSgXzlcOfOT3VHOkzTfPo08GOE5jA==",
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "2.10.5",
"chromium-bidi": "5.1.0",
"@puppeteer/browsers": "2.10.6",
"chromium-bidi": "7.1.1",
"debug": "^4.4.1",
"devtools-protocol": "0.0.1464554",
"typed-query-selector": "^2.12.0",

View file

@ -1,6 +1,6 @@
{
"name": "ff-admin-server",
"version": "1.7.2",
"version": "1.7.6",
"description": "Feuerwehr/Verein Mitgliederverwaltung Server",
"main": "dist/index.js",
"scripts": {
@ -29,7 +29,7 @@
"crypto": "^1.0.1",
"dotenv": "^17.2.0",
"express": "^5.1.0",
"express-rate-limit": "^7.5.1",
"express-rate-limit": "^8.0.1",
"express-validator": "^7.2.1",
"handlebars": "^4.7.8",
"helmet": "^8.1.0",
@ -39,14 +39,14 @@
"lodash.clonedeep": "^4.5.0",
"lodash.uniqby": "^4.7.0",
"moment": "^2.30.1",
"morgan": "^1.10.0",
"morgan": "^1.10.1",
"ms": "^2.1.3",
"multer": "^2.0.1",
"multer": "^2.0.2",
"node-schedule": "^2.1.1",
"nodemailer": "^7.0.5",
"pdf-lib": "^1.17.1",
"pg": "^8.16.3",
"puppeteer": "^24.12.1",
"puppeteer": "^24.14.0",
"qrcode": "^1.5.4",
"reflect-metadata": "^0.2.2",
"rss-parser": "^3.13.0",
@ -68,7 +68,7 @@
"@types/morgan": "^1.9.10",
"@types/ms": "^2.1.0",
"@types/multer": "^2.0.0",
"@types/node": "^24.0.13",
"@types/node": "^24.0.15",
"@types/node-schedule": "^2.1.8",
"@types/nodemailer": "^6.4.17",
"@types/pg": "~8.15.4",

View file

@ -215,7 +215,8 @@ export async function getMembershipTotalStatisticsById(req: Request, res: Respon
const memberId = req.params.memberId;
let member = await MembershipService.getTotalStatisticsById(memberId);
res.json(MembershipFactory.mapToSingleTotalStatistic(member));
if (!member) res.sendStatus(204);
else res.json(MembershipFactory.mapToSingleTotalStatistic(member));
}
/**

View file

@ -50,6 +50,7 @@ import { memberEducations } from "./entity/club/member/memberEducations";
import { BackupAndResetDatabase1749296262915 } from "./migrations/1749296262915-BackupAndResetDatabase";
import { CreateSchema1749296280721 } from "./migrations/1749296280721-CreateSchema";
import { UpdateNewsletterQueryRelation1752502069178 } from "./migrations/1752502069178-updateNewsletterQueryRelation";
configCheck();
@ -108,7 +109,11 @@ const dataSource = new DataSource({
webapiPermission,
setting,
],
migrations: [BackupAndResetDatabase1749296262915, CreateSchema1749296280721],
migrations: [
BackupAndResetDatabase1749296262915,
CreateSchema1749296280721,
UpdateNewsletterQueryRelation1752502069178,
],
migrationsRun: true,
migrationsTransactionMode: "each",
subscribers: [],

View file

@ -40,7 +40,7 @@ export class newsletter {
@ManyToOne(() => query, {
nullable: true,
onDelete: "CASCADE",
onDelete: "SET NULL",
onUpdate: "RESTRICT",
cascade: ["insert"],
})

View file

@ -65,7 +65,7 @@ export default abstract class DynamicQueryBuilder {
count?: number;
noLimit?: boolean;
}): SelectQueryBuilder<ObjectLiteral> {
let affix = queryObj.id.replaceAll("-", "") ?? StringHelper.random(10);
let affix = queryObj.id?.replaceAll("-", "") ?? StringHelper.random(10);
let query = dataSource.getRepository(queryObj.table).createQueryBuilder(`${affix}_${queryObj.table}`);
this.buildDynamicQuery(query, queryObj, affix);
@ -118,7 +118,7 @@ export default abstract class DynamicQueryBuilder {
if (queryObject.join) {
for (const join of queryObject.join) {
let subaffix = join.id.replaceAll("-", "") ?? StringHelper.random(10);
let subaffix = join.id?.replaceAll("-", "") ?? StringHelper.random(10);
if (join.type == undefined) join.type = "defined";
if (join.type == "defined") {
query.innerJoin(`${alias}.${join.foreignColumn}`, `${subaffix}_${join.table}`);

View file

@ -0,0 +1,44 @@
import { MigrationInterface, QueryRunner, TableForeignKey } from "typeorm";
import { newsletter_table } from "./baseSchemaTables/newsletter";
export class UpdateNewsletterQueryRelation1752502069178 implements MigrationInterface {
name = "UpdateNewsletterQueryRelation1752502069178";
public async up(queryRunner: QueryRunner): Promise<void> {
const table = await queryRunner.getTable("newsletter");
const foreignKey = table?.foreignKeys.find((fk) => fk.columnNames.includes("recipientsByQueryId"));
if (foreignKey) {
await queryRunner.dropForeignKey("newsletter", foreignKey);
}
await queryRunner.createForeignKey(
newsletter_table,
new TableForeignKey({
columnNames: ["recipientsByQueryId"],
referencedColumnNames: ["id"],
referencedTableName: "query",
onDelete: "SET NULL",
onUpdate: "RESTRICT",
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
const table = await queryRunner.getTable("newsletter");
const foreignKey = table?.foreignKeys.find((fk) => fk.columnNames.includes("recipientsByQueryId"));
if (foreignKey) {
await queryRunner.dropForeignKey("newsletter", foreignKey);
}
await queryRunner.createForeignKey(
newsletter_table,
new TableForeignKey({
columnNames: ["recipientsByQueryId"],
referencedColumnNames: ["id"],
referencedTableName: "query",
onDelete: "CASCADE",
onUpdate: "RESTRICT",
})
);
}
}

View file

@ -50,7 +50,7 @@ router.post(
router.post(
"/finish",
ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "totp"]),
ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "secret", "routine"]),
async (req, res) => {
await finishInvite(req, res, true);
}

View file

@ -13,7 +13,7 @@ export default abstract class CalendarService {
.getRepository(calendar)
.createQueryBuilder("calendar")
.leftJoinAndSelect("calendar.type", "type")
.orderBy("starttime", "ASC")
.orderBy("calendar.starttime", "ASC")
.getMany()
.then((res) => {
return res;

View file

@ -77,7 +77,7 @@ export default abstract class MembershipService {
.getRepository(membershipTotalView)
.createQueryBuilder("membershipTotalView")
.where("membershipTotalView.memberId = :memberId", { memberId: memberId })
.getOneOrFail()
.getOne()
.then((res) => {
return res;
})

View file

@ -12,7 +12,7 @@ export default abstract class NewsletterService {
return await dataSource
.getRepository(newsletter)
.createQueryBuilder("newsletter")
.orderBy("createdAt", "DESC")
.orderBy("newsletter.createdAt", "DESC")
.offset(offset)
.limit(count)
.getManyAndCount()

View file

@ -14,7 +14,7 @@ export default abstract class ProtocolService {
.createQueryBuilder("protocol")
.offset(offset)
.limit(count)
.orderBy("date", "DESC")
.orderBy("protocol.date", "DESC")
.getManyAndCount()
.then((res) => {
return res;

View file

@ -13,7 +13,7 @@ export default abstract class AwardService {
return await dataSource
.getRepository(award)
.createQueryBuilder("award")
.orderBy("award", "ASC")
.orderBy("award.award", "ASC")
.getMany()
.then((res) => {
return res;

View file

@ -12,7 +12,7 @@ export default abstract class CalendarTypeService {
return await dataSource
.getRepository(calendarType)
.createQueryBuilder("calendarType")
.orderBy("type", "ASC")
.orderBy("calendarType.type", "ASC")
.getMany()
.then((res) => {
return res;

View file

@ -12,7 +12,7 @@ export default abstract class CommunicationTypeService {
return await dataSource
.getRepository(communicationType)
.createQueryBuilder("communicationType")
.orderBy("type", "ASC")
.orderBy("communicationType.type", "ASC")
.getMany()
.then((res) => {
return res;

View file

@ -11,7 +11,7 @@ export default abstract class EducationService {
return await dataSource
.getRepository(education)
.createQueryBuilder("education")
.orderBy("education", "ASC")
.orderBy("education.education", "ASC")
.getMany()
.then((res) => {
return res;

View file

@ -13,7 +13,7 @@ export default abstract class ExecutivePositionService {
return await dataSource
.getRepository(executivePosition)
.createQueryBuilder("executivePosition")
.orderBy("position", "ASC")
.orderBy("executivePosition.position", "ASC")
.getMany()
.then((res) => {
return res;

View file

@ -13,7 +13,7 @@ export default abstract class MembershipStatusService {
return await dataSource
.getRepository(membershipStatus)
.createQueryBuilder("membershipStatus")
.orderBy("status", "ASC")
.orderBy("membershipStatus.status", "ASC")
.getMany()
.then((res) => {
return res;

View file

@ -14,7 +14,7 @@ export default abstract class QualificationService {
return await dataSource
.getRepository(qualification)
.createQueryBuilder("qualification")
.orderBy("qualification", "ASC")
.orderBy("qualification.qualification", "ASC")
.getMany()
.then((res) => {
return res;

View file

@ -12,7 +12,7 @@ export default abstract class QueryStoreService {
return await dataSource
.getRepository(query)
.createQueryBuilder("queryStore")
.orderBy("title", "ASC")
.orderBy("queryStore.title", "ASC")
.getMany()
.then((res) => {
return res;

View file

@ -12,7 +12,7 @@ export default abstract class SalutationService {
return await dataSource
.getRepository(salutation)
.createQueryBuilder("salutation")
.orderBy("salutation", "ASC")
.orderBy("salutation.salutation", "ASC")
.getMany()
.then((res) => {
return res;

View file

@ -13,7 +13,7 @@ export default abstract class TemplateService {
return await dataSource
.getRepository(template)
.createQueryBuilder("template")
.orderBy("template", "ASC")
.orderBy("template.template", "ASC")
.getMany()
.then((res) => {
return res;

View file

@ -15,7 +15,7 @@ export default abstract class TemplateUsageService {
.leftJoinAndSelect("templateUsage.header", "headerTemplate")
.leftJoinAndSelect("templateUsage.body", "bodyTemplate")
.leftJoinAndSelect("templateUsage.footer", "footerTemplate")
.orderBy("scope", "ASC")
.orderBy("templateUsage.scope", "ASC")
.getMany()
.then((res) => {
return res;

View file

@ -13,7 +13,7 @@ export default abstract class RoleService {
.getRepository(role)
.createQueryBuilder("role")
.leftJoinAndSelect("role.permissions", "role_permissions")
.orderBy("role", "ASC")
.orderBy("role.role", "ASC")
.getMany()
.then((res) => {
return res;

View file

@ -16,8 +16,8 @@ export default abstract class UserService {
.leftJoinAndSelect("user.roles", "roles")
.leftJoinAndSelect("user.permissions", "permissions")
.leftJoinAndSelect("roles.permissions", "role_permissions")
.orderBy("firstname", "ASC")
.addOrderBy("lastname", "ASC")
.orderBy("user.firstname", "ASC")
.addOrderBy("user.lastname", "ASC")
.getMany()
.then((res) => {
return res;