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:
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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: [
|
||||
|
||||
@ -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}` } };
|
||||
|
||||
@ -47,6 +47,7 @@ export class AuthResolver {
|
||||
const { exists } = await this.authService.checkUserExists(
|
||||
checkUserExistsInput.email,
|
||||
);
|
||||
|
||||
return { exists };
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@ -59,6 +59,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
|
||||
picture: photos?.[0]?.value,
|
||||
workspaceInviteHash: state.workspaceInviteHash,
|
||||
};
|
||||
|
||||
done(null, user);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}`);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user