Skip to content

Commit f9e0b5e

Browse files
authored
Merge pull request #1315 from leggedrobotics/dev
Release 0.41.2
2 parents 59ff059 + 2e6bc45 commit f9e0b5e

File tree

70 files changed

+613
-304
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+613
-304
lines changed

backend/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "kleinkram-backend",
3-
"version": "0.41.0",
3+
"version": "0.41.2",
44
"description": "",
55
"author": "",
66
"private": true,
@@ -82,7 +82,7 @@
8282
"@types/node": "^22.13.5",
8383
"@types/supertest": "^6.0.0",
8484
"@typescript-eslint/eslint-plugin": "^8.25.0",
85-
"@typescript-eslint/parser": "^8.24.1",
85+
"@typescript-eslint/parser": "^8.25.0",
8686
"eslint": "^9.21.0",
8787
"eslint-config-prettier": "^10.0.1",
8888
"eslint-plugin-prettier": "^5.0.0",

backend/src/app.module.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { PrometheusModule } from '@willsoto/nestjs-prometheus';
2323
import { AuditLoggerMiddleware } from './routing/middlewares/audit-logger-middleware.service';
2424
import { appVersion } from './app-version';
2525
import { ActionModule } from './endpoints/action/action.module';
26+
import { VersionCheckerMiddlewareService } from './routing/middlewares/version-checker-middleware.service';
2627

2728
export interface AccessGroupConfig {
2829
emails: [{ email: string; access_groups: string[] }];
@@ -92,8 +93,13 @@ export class AppModule implements NestModule {
9293
* @param consumer
9394
*/
9495
configure(consumer: MiddlewareConsumer): void {
95-
consumer // enable default middleware for all routes
96-
.apply(APIKeyResolverMiddleware, AuditLoggerMiddleware)
96+
// Apply APIKeyResolverMiddleware and AuditLoggerMiddleware to all routes
97+
consumer
98+
.apply(
99+
APIKeyResolverMiddleware,
100+
AuditLoggerMiddleware,
101+
VersionCheckerMiddlewareService,
102+
)
97103
.forRoutes('*');
98104
}
99105
}

backend/src/endpoints/auth/auth-helper.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export const projectAccessUUIDQuery = (
1616
// we us randomized tokens to creat a signature for the subquery parameters
1717
// in this way we avoid conflicts with other subqueries or the main query
1818
// would be nice if typeorm did this out of the box, but it doesnt
19-
if (tok === undefined) tok = uuidv4().replace(/-/g, '');
19+
if (tok === undefined) tok = uuidv4().replaceAll('-', '');
2020

2121
const projectIdsQuery = query
2222
.subQuery()
@@ -37,7 +37,7 @@ export const missionAccessUUIDQuery = (
3737
// we us randomized tokens to creat a signature for the subquery parameters
3838
// in this way we avoid conflicts with other subqueries or the main query
3939
// would be nice if typeorm did this out of the box, but it doesnt
40-
if (tok === undefined) tok = uuidv4().replace(/-/g, '');
40+
if (tok === undefined) tok = uuidv4().replaceAll('-', '');
4141

4242
return query
4343
.subQuery()
@@ -56,7 +56,7 @@ export const getUserIsAdminSubQuery = (
5656
// we us randomized tokens to creat a signature for the subquery parameters
5757
// in this way we avoid conflicts with other subqueries or the main query
5858
// would be nice if typeorm did this out of the box, but it doesnt
59-
if (tok === undefined) tok = uuidv4().replace(/-/g, '');
59+
if (tok === undefined) tok = uuidv4().replaceAll('-', '');
6060

6161
const subQuery = query.subQuery().select('user.role').from(User, 'user');
6262
subQuery.where(`user.uuid = :userUUID_${tok}`, {
File renamed without changes.

backend/src/routing/interceptors/output-validation.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@ function validateResponseJSON<T extends object>(dto: ClassConstructor<T>) {
2424

2525
if (errors.length > 0) {
2626
logger.error(
27-
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
28-
`Response Validation failed: ${errors.length} errors`,
27+
`Response Validation failed: ${errors.length.toString()} errors`,
2928
);
3029
logger.error(`In response: `);
3130
logger.error(JSON.stringify(data, undefined, 2));
@@ -37,8 +36,7 @@ function validateResponseJSON<T extends object>(dto: ClassConstructor<T>) {
3736
`\n${errors.map((error) => error.toString()).join('\n')}`,
3837
);
3938
throw new InternalServerErrorException(
40-
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
41-
`Validation failed: ${errors.length} errors. Check backend logs for details.`,
39+
`Validation failed: ${errors.length.toString()} errors. Check backend logs for details.`,
4240
);
4341
}
4442
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { Injectable, NestMiddleware } from '@nestjs/common';
2+
import { NextFunction, Request, Response } from 'express';
3+
import logger from '../../logger';
4+
5+
/**
6+
*
7+
* A nest middleware that resolves the user from the API key in the request.
8+
*
9+
*/
10+
@Injectable()
11+
export class VersionCheckerMiddlewareService implements NestMiddleware {
12+
async use(
13+
request: Request,
14+
response: Response,
15+
next: NextFunction,
16+
): Promise<void> {
17+
let clientVersion = request.headers['kleinkram-client-version'] as
18+
| string
19+
| undefined;
20+
21+
const requestPath = request.originalUrl;
22+
23+
// ignore auth endpoints
24+
if (requestPath.startsWith('/auth/')) {
25+
next();
26+
return;
27+
}
28+
29+
// strip away everything after .dev
30+
if (clientVersion !== undefined) {
31+
clientVersion = clientVersion.split('.dev')[0];
32+
}
33+
34+
// verify if version is of semver format
35+
const validVersion = /^\d+\.\d+\.\d+$/;
36+
if (clientVersion !== undefined && !validVersion.test(clientVersion)) {
37+
this.rejectRequest(response, clientVersion);
38+
return;
39+
}
40+
41+
logger.debug(
42+
`Check Client Version for call to endpoint: ${requestPath} is: ${clientVersion}`,
43+
);
44+
45+
if (clientVersion === undefined) {
46+
this.rejectRequest(response, 'undefined');
47+
return;
48+
}
49+
50+
// forbidden client versions: allows for the following notations
51+
// - '<0.40.x': versions below 0.40.x are forbidden
52+
// - '0.41.x': version 0.41.x is forbidden
53+
const forbiddenClientVersions = ['<0.40.0', '0.41.0', '0.41.1'];
54+
55+
if (this.isVersionForbidden(clientVersion, forbiddenClientVersions)) {
56+
this.rejectRequest(response, clientVersion);
57+
return;
58+
}
59+
60+
next();
61+
}
62+
63+
private rejectRequest(response: Response, clientVersion: string): void {
64+
// reject request with 426
65+
response.status(426).json({
66+
statusCode: 426,
67+
message: `Client version ${clientVersion} is not a valid version.`,
68+
});
69+
70+
response.send();
71+
}
72+
73+
private isVersionForbidden(
74+
clientVersion: string,
75+
forbiddenVersions: string[],
76+
): boolean {
77+
for (const forbiddenVersion of forbiddenVersions) {
78+
if (forbiddenVersion.startsWith('<')) {
79+
const versionToCompare = forbiddenVersion.slice(1);
80+
if (this.isLessThan(clientVersion, versionToCompare)) {
81+
return true;
82+
}
83+
} else if (forbiddenVersion.endsWith('.x')) {
84+
const baseVersion = forbiddenVersion.slice(
85+
0,
86+
Math.max(0, forbiddenVersion.length - 2),
87+
);
88+
if (clientVersion.startsWith(baseVersion)) {
89+
return true;
90+
}
91+
} else if (clientVersion === forbiddenVersion) {
92+
return true;
93+
}
94+
}
95+
return false;
96+
}
97+
98+
private isLessThan(version1: string, version2: string): boolean {
99+
const v1Parts = version1.split('.').map(Number);
100+
const v2Parts = version2.split('.').map(Number);
101+
102+
const maxLength = Math.max(v1Parts.length, v2Parts.length);
103+
104+
for (let i = 0; i < maxLength; i++) {
105+
const part1 = v1Parts[i] || 0;
106+
const part2 = v2Parts[i] || 0;
107+
108+
if (part1 < part2) {
109+
return true;
110+
} else if (part1 > part2) {
111+
return false;
112+
}
113+
}
114+
115+
return false;
116+
}
117+
}

backend/src/serialization.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import { ProjectWithMissionsDto } from '@common/api/types/project/project-with-m
3030
import { GroupMembershipDto, UserDto } from '@common/api/types/user.dto';
3131
import { FileDto, FileWithTopicDto } from '@common/api/types/file/file.dto';
3232
import { TopicDto } from '@common/api/types/topic.dto';
33-
import logger from './logger';
3433

3534
export const missionEntityToDto = (mission: Mission): MissionDto => {
3635
if (!mission.project) {
@@ -83,9 +82,9 @@ export const missionEntityToDtoWithFiles = (
8382
}
8483

8584
return {
86-
...missionEntityToDtoWithCreator(mission),
87-
files: mission.files.map(fileEntityToDto),
88-
tags: mission.tags.map(tagEntityToDto),
85+
...(missionEntityToDtoWithCreator(mission) as MissionWithFilesDto),
86+
files: mission.files.map((element) => fileEntityToDto(element)),
87+
tags: mission.tags.map((element) => tagEntityToDto(element)),
8988
};
9089
};
9190

@@ -195,8 +194,8 @@ export const fileEntityToDtoWithTopic = (
195194
throw new Error('File topics are not set');
196195
}
197196
return {
198-
...fileEntityToDto(file),
199-
topics: file.topics.map(topicEntityToDto),
197+
...(fileEntityToDto(file) as FileWithTopicDto),
198+
topics: file.topics.map((element) => topicEntityToDto(element)),
200199
};
201200
};
202201

@@ -255,7 +254,7 @@ export const projectEntityToDtoWithMissionCount = (
255254
}
256255

257256
return {
258-
...projectEntityToDto(project),
257+
...(projectEntityToDto(project) as ProjectWithMissionCountDto),
259258
creator: userEntityToDto(project.creator),
260259
missionCount: project.missions?.length ?? 0,
261260
};
@@ -269,9 +268,11 @@ export const projectEntityToDtoWithMissions = (
269268
}
270269

271270
return {
272-
...projectEntityToDto(project),
271+
...(projectEntityToDto(project) as ProjectWithMissionsDto),
273272
creator: userEntityToDto(project.creator),
274-
requiredTags: project.requiredTags.map(tagTypeEntityToDto),
273+
requiredTags: project.requiredTags.map((element) =>
274+
tagTypeEntityToDto(element),
275+
),
275276
missions: project.missions?.map(missionEntityToFlatDto) ?? [],
276277
};
277278
};

backend/src/services/access.service.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export class AccessService {
8888
name: value.project?.name,
8989
uuid: value.project?.uuid,
9090
rights: value.rights,
91+
autoConvert: value.project?.autoConvert ?? false,
9192
}) as ProjectWithAccessRightsDto,
9293
) ?? [],
9394
};
@@ -341,7 +342,6 @@ export class AccessService {
341342
const data: AccessGroupDto[] = accessGroups.map(
342343
(accessGroup: AccessGroup): AccessGroupDto => {
343344
return {
344-
// eslint-disable-next-line unicorn/no-null
345345
creator: accessGroup.creator
346346
? userEntityToDto(accessGroup.creator)
347347
: null,
@@ -475,7 +475,7 @@ export class AccessService {
475475
);
476476

477477
return {
478-
data: access.map(projectAccessEntityToDto),
478+
data: access.map((element) => projectAccessEntityToDto(element)),
479479
count,
480480
take: count,
481481
skip: 0,
@@ -558,9 +558,9 @@ export class AccessService {
558558
// filter out the access rights that have not been modified
559559
const accessRightsChanges = newProjectAccess.filter((access) => {
560560
return !currentAccess.some(
561-
(currentAccess) =>
562-
currentAccess.accessGroup?.uuid === access.uuid &&
563-
currentAccess.rights === access.rights,
561+
(projectAccess) =>
562+
projectAccess.accessGroup?.uuid === access.uuid &&
563+
projectAccess.rights === access.rights,
564564
);
565565
});
566566

backend/src/services/action.service.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,9 @@ export class ActionService {
213213

214214
return {
215215
count,
216-
data: templates.map(actionTemplateEntityToDto),
216+
data: templates.map((element) =>
217+
actionTemplateEntityToDto(element),
218+
),
217219
skip,
218220
take,
219221
};
@@ -265,7 +267,7 @@ export class ActionService {
265267
const [actions, count] = await query.getManyAndCount();
266268
return {
267269
count,
268-
data: actions.map(actionEntityToDto),
270+
data: actions.map((element) => actionEntityToDto(element)),
269271
skip,
270272
take,
271273
};
@@ -312,7 +314,7 @@ export class ActionService {
312314

313315
return {
314316
count,
315-
data: actions.map(actionEntityToDto),
317+
data: actions.map((element) => actionEntityToDto(element)),
316318
skip,
317319
take,
318320
};

backend/src/services/dbdumper.service.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export class DBDumper {
4747
return dumpFile;
4848
} catch (error: any) {
4949
await unlinkAsync(dumpFile);
50+
5051
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
5152
throw new Error(`Failed to create database dump: ${error.message}`);
5253
}

0 commit comments

Comments
 (0)