feat: better server lint (#2850)

* feat: add stylistic eslint plugin

* feat: add missing line return

* feat: secure line-break style

* feat: disallow break before else

* feat: line between class members

* feat: better new line lint rule
This commit is contained in:
Jérémy M
2023-12-06 12:19:00 +01:00
committed by GitHub
parent e388d90976
commit 9df83c9a5a
75 changed files with 318 additions and 21 deletions

View File

@ -11,24 +11,28 @@ export class ApiRestController {
@Get()
async handleApiGet(@Req() request: Request): Promise<object> {
const result = await this.apiRestService.get(request);
return result.data;
}
@Delete()
async handleApiDelete(@Req() request: Request): Promise<object> {
const result = await this.apiRestService.delete(request);
return result.data;
}
@Post()
async handleApiPost(@Req() request: Request): Promise<object> {
const result = await this.apiRestService.create(request);
return result.data;
}
@Put()
async handleApiPut(@Req() request: Request): Promise<object> {
const result = await this.apiRestService.update(request);
return result.data;
}
}

View File

@ -7,6 +7,7 @@ import { EnvironmentService } from 'src/integrations/environment/environment.ser
describe('ApiRestService', () => {
let service: ApiRestService;
const objectMetadataItem = { fields: [{ name: 'field', type: 'NUMBER' }] };
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [

View File

@ -24,6 +24,7 @@ enum FILTER_COMPARATORS {
const ALLOWED_DEPTH_VALUES = [1, 2];
const DEFAULT_DEPTH_VALUE = 2;
const DEFAULT_ORDER_DIRECTION = OrderByDirection.AscNullsFirst;
enum CONJUNCTIONS {
or = 'or',
and = 'and',
@ -275,16 +276,19 @@ export class ApiRestService {
const [objectMetadata] = objectMetadataItems.filter(
(object) => object.namePlural === parsedObject,
);
if (!objectMetadata) {
const [wrongObjectMetadata] = objectMetadataItems.filter(
(object) => object.nameSingular === parsedObject,
);
let hint = 'eg: companies';
if (wrongObjectMetadata) {
hint = `Did you mean '${wrongObjectMetadata.namePlural}'?`;
}
throw Error(`object '${parsedObject}' not found. ${hint}`);
}
return {
objectMetadataItems,
objectMetadataItem: objectMetadata,
@ -295,6 +299,7 @@ export class ApiRestService {
if (!(filterQuery.includes('(') && filterQuery.includes(')'))) {
return `${DEFAULT_FILTER_CONJUNCTION}(${filterQuery})`;
}
return filterQuery;
}
@ -302,6 +307,7 @@ export class ApiRestService {
const countOpenedBrackets = (filterQuery.match(/\(/g) || []).length;
const countClosedBrackets = (filterQuery.match(/\)/g) || []).length;
const diff = countOpenedBrackets - countClosedBrackets;
if (diff !== 0) {
const hint =
diff > 0
@ -309,8 +315,10 @@ export class ApiRestService {
: `${Math.abs(diff)} close bracket${
Math.abs(diff) > 1 ? 's are' : ' is'
}`;
throw Error(`'filter' invalid. ${hint} missing in the query`);
}
return;
}
@ -318,6 +326,7 @@ export class ApiRestService {
let parenthesisCounter = 0;
const predicates: string[] = [];
let currentPredicates = '';
for (const c of filterQuery) {
if (c === '(') {
parenthesisCounter++;
@ -337,6 +346,7 @@ export class ApiRestService {
if (currentPredicates.length) {
predicates.push(currentPredicates);
}
return predicates;
}
@ -345,11 +355,13 @@ export class ApiRestService {
const match = filterQuery.match(
`^(${Object.values(CONJUNCTIONS).join('|')})((.+))$`,
);
if (match) {
const conjunction = match[1];
const subResult = this.parseFilterQueryContent(filterQuery).map((elem) =>
this.parseStringFilter(elem, objectMetadataItem),
);
if (conjunction === CONJUNCTIONS.not) {
if (subResult.length > 1) {
throw Error(
@ -360,8 +372,10 @@ export class ApiRestService {
} else {
result[conjunction] = subResult;
}
return result;
}
return this.parseSimpleFilter(filterQuery, objectMetadataItem);
}
@ -376,6 +390,7 @@ export class ApiRestService {
}
const [fieldAndComparator, value] = filterString.split(':');
const [field, comparator] = fieldAndComparator.replace(']', '').split('[');
if (!Object.keys(FILTER_COMPARATORS).includes(comparator)) {
throw Error(
`'filter' invalid for '${filterString}', comparator ${comparator} not in ${Object.keys(
@ -384,9 +399,11 @@ export class ApiRestService {
);
}
const fields = field.split('.');
this.checkFields(objectMetadataItem, fields, 'filter');
const fieldType = this.getFieldType(objectMetadataItem, fields[0]);
const formattedValue = this.formatFieldValue(value, fieldType);
return fields.reverse().reduce(
(acc, currentValue) => {
return { [currentValue]: acc };
@ -402,35 +419,42 @@ export class ApiRestService {
if (fieldType === 'BOOLEAN') {
return value.toLowerCase() === 'true';
}
return value;
}
parseFilter(request, objectMetadataItem) {
const parsedObjectId = this.parseObject(request)[1];
if (parsedObjectId) {
return { id: { eq: parsedObjectId } };
}
const rawFilterQuery = request.query.filter;
if (typeof rawFilterQuery !== 'string') {
return {};
}
this.checkFilterQuery(rawFilterQuery);
const filterQuery = this.addDefaultConjunctionIfMissing(rawFilterQuery);
return this.parseStringFilter(filterQuery, objectMetadataItem);
}
parseOrderBy(request, objectMetadataItem) {
//?order_by=field_1[AscNullsFirst],field_2[DescNullsLast],field_3
const orderByQuery = request.query.order_by;
if (typeof orderByQuery !== 'string') {
return {};
}
const orderByItems = orderByQuery.split(',');
const result = {};
for (const orderByItem of orderByItems) {
// orderByItem -> field_1[AscNullsFirst]
if (orderByItem.includes('[') && orderByItem.includes(']')) {
const [field, direction] = orderByItem.replace(']', '').split('[');
// field -> field_1 ; direction -> AscNullsFirst
if (!(direction in OrderByDirection)) {
throw Error(
@ -448,6 +472,7 @@ export class ApiRestService {
}
}
this.checkFields(objectMetadataItem, Object.keys(result), 'order_by');
return <RecordOrderBy>result;
}
@ -482,21 +507,26 @@ export class ApiRestService {
parseLimit(request) {
const limitQuery = request.query.limit;
if (typeof limitQuery !== 'string') {
return 60;
}
const limitParsed = parseInt(limitQuery);
if (!Number.isInteger(limitParsed)) {
throw Error(`limit '${limitQuery}' is invalid. Should be an integer`);
}
return limitParsed;
}
parseCursor(request) {
const cursorQuery = request.query.last_cursor;
if (typeof cursorQuery !== 'string') {
return undefined;
}
return cursorQuery;
}
@ -511,6 +541,7 @@ export class ApiRestService {
parseObject(request) {
const queryAction = request.path.replace('/rest/', '').split('/');
if (queryAction.length > 2) {
throw Error(
`Query path '${request.path}' invalid. Valid examples: /rest/companies/id or /rest/companies`,
@ -519,14 +550,17 @@ export class ApiRestService {
if (queryAction.length === 1) {
return [queryAction[0], undefined];
}
return queryAction;
}
extractWorkspaceId(request: Request) {
const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request);
if (!token) {
throw Error('missing authentication token');
}
return verify(token, this.environmentService.getAccessTokenSecret())[
'workspaceId'
];
@ -537,6 +571,7 @@ export class ApiRestService {
typeof request.query.depth === 'string'
? parseInt(request.query.depth)
: DEFAULT_DEPTH_VALUE;
if (!ALLOWED_DEPTH_VALUES.includes(depth)) {
throw Error(
`'depth=${depth}' parameter invalid. Allowed values are ${ALLOWED_DEPTH_VALUES.join(
@ -544,6 +579,7 @@ export class ApiRestService {
)}`,
);
}
return depth;
}
@ -583,6 +619,7 @@ export class ApiRestService {
objectMetadata.objectMetadataItem,
),
};
return await this.callGraphql(request, data);
} catch (err) {
return { data: { error: `${err}` } };
@ -593,6 +630,7 @@ export class ApiRestService {
try {
const objectMetadata = await this.getObjectMetadata(request);
const id = this.parseObject(request)[1];
if (!id) {
return {
data: {
@ -606,6 +644,7 @@ export class ApiRestService {
id: this.parseObject(request)[1],
},
};
return await this.callGraphql(request, data);
} catch (err) {
return { data: { error: `${err}` } };
@ -626,6 +665,7 @@ export class ApiRestService {
data: request.body,
},
};
return await this.callGraphql(request, data);
} catch (err) {
return { data: { error: `${err}` } };
@ -637,6 +677,7 @@ export class ApiRestService {
const objectMetadata = await this.getObjectMetadata(request);
const depth = this.computeDepth(request);
const id = this.parseObject(request)[1];
if (!id) {
return {
data: {
@ -655,6 +696,7 @@ export class ApiRestService {
data: request.body,
},
};
return await this.callGraphql(request, data);
} catch (err) {
return { data: { error: `${err}` } };

View File

@ -47,6 +47,7 @@ export class AuthResolver {
const { exists } = await this.authService.checkUserExists(
checkUserExistsInput.email,
);
return { exists };
}

View File

@ -8,12 +8,14 @@ import { GoogleStrategy } from 'src/core/auth/strategies/google.auth.strategy';
@Injectable()
export class GoogleProviderEnabledGuard implements CanActivate {
constructor(private readonly environmentService: EnvironmentService) {}
canActivate(): boolean | Promise<boolean> | Observable<boolean> {
if (!this.environmentService.isAuthGoogleEnabled()) {
throw new NotFoundException('Google auth is not enabled');
}
new GoogleStrategy(this.environmentService);
return true;
}
}

View File

@ -89,10 +89,12 @@ export class AuthService {
const existingUser = await this.userRepository.findOneBy({
email: email,
});
assert(!existingUser, 'This user already exists', ForbiddenException);
if (password) {
const isPasswordValid = PASSWORD_REGEX.test(password);
assert(isPasswordValid, 'Password too weak', BadRequestException);
}
@ -115,6 +117,7 @@ export class AuthService {
domainName: '',
inviteHash: v4(),
});
workspace = await this.workspaceRepository.save(workspaceToCreate);
await this.workspaceManagerService.init(workspace.id);
}

View File

@ -34,6 +34,7 @@ export class TokenService {
async generateAccessToken(userId: string): Promise<AuthToken> {
const expiresIn = this.environmentService.getAccessTokenExpiresIn();
assert(expiresIn, '', InternalServerErrorException);
const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn));
@ -64,6 +65,7 @@ export class TokenService {
async generateRefreshToken(userId: string): Promise<AuthToken> {
const secret = this.environmentService.getRefreshTokenSecret();
const expiresIn = this.environmentService.getRefreshTokenExpiresIn();
assert(expiresIn, '', InternalServerErrorException);
const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn));
@ -94,6 +96,7 @@ export class TokenService {
async generateLoginToken(email: string): Promise<AuthToken> {
const secret = this.environmentService.getLoginTokenSecret();
const expiresIn = this.environmentService.getLoginTokenExpiresIn();
assert(expiresIn, '', InternalServerErrorException);
const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn));
const jwtPayload = {
@ -122,6 +125,7 @@ export class TokenService {
};
const secret = this.environmentService.getAccessTokenSecret();
let expiresIn: string | number;
if (expiresAt) {
expiresIn = Math.floor(
(new Date(expiresAt).getTime() - new Date().getTime()) / 1000,
@ -134,6 +138,7 @@ export class TokenService {
expiresIn,
jwtid: apiKeyId,
});
return { token };
}

View File

@ -59,6 +59,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
picture: photos?.[0]?.value,
workspaceInviteHash: state.workspaceInviteHash,
};
done(null, user);
}
}

View File

@ -41,6 +41,7 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
const workspace = await this.workspaceRepository.findOneBy({
id: payload.workspaceId ?? payload.sub,
});
if (!workspace) {
throw new UnauthorizedException();
}
@ -66,6 +67,7 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
}
let user;
if (payload.workspaceId) {
user = await this.userRepository.findOneBy({
id: payload.sub,

View File

@ -9,6 +9,7 @@ import { FileService } from 'src/core/file/services/file.service';
@Controller('files')
export class FileController {
constructor(private readonly fileService: FileService) {}
/**
* Serve files from local storage
* We recommend using an s3 bucket for production

View File

@ -77,6 +77,7 @@ export class FileUploadService {
const name = `${id}${ext ? `.${ext}` : ''}`;
const cropSizes = settings.storage.imageCropSizes[fileFolder];
if (!cropSizes) {
throw new Error(`No crop sizes found for ${fileFolder}`);
}

View File

@ -19,6 +19,7 @@ export class BeforeCreateOneRefreshToken<T extends RefreshToken>
// FIXME: These fields should be autogenerated, we need to run a migration for this
instance.input.id = uuidv4();
instance.input.updatedAt = new Date();
return instance;
}
}

View File

@ -72,6 +72,7 @@ export class UserService extends TypeOrmQueryService<User> {
const user = await this.userRepository.findOneBy({
id: userId,
});
assert(user, 'User not found');
await this.userRepository.delete(user.id);

View File

@ -30,6 +30,7 @@ const getHMACKey = (email?: string, key?: string | null) => {
if (!email || !key) return null;
const hmac = crypto.createHmac('sha256', key);
return hmac.update(email).digest('hex');
};
@ -47,7 +48,9 @@ export class UserResolver {
const user = await this.userService.findById(id, {
relations: [{ name: 'defaultWorkspace', query: {} }],
});
assert(user, 'User not found');
return user;
}
@ -66,6 +69,7 @@ export class UserResolver {
return null;
}
const key = this.environmentService.getSupportFrontHMACKey();
return getHMACKey(parent.email, key);
}

View File

@ -19,6 +19,7 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
async deleteWorkspace(id: string) {
const workspace = await this.workspaceRepository.findOneBy({ id });
assert(workspace, 'Workspace not found');
await this.workspaceManagerService.delete(id);

View File

@ -27,7 +27,9 @@ export class WorkspaceResolver {
@Query(() => Workspace)
async currentWorkspace(@AuthWorkspace() { id }: Workspace) {
const workspace = await this.workspaceService.findById(id);
assert(workspace, 'User not found');
return workspace;
}