Commit bf6b6f78 authored by Fabien Forestier's avatar Fabien Forestier
Browse files

Merge branch 'development' into 'master'

Version 2.1.0

See merge request refonte-data/service-organizations!7
parents 1ed2a7c9 8e12e256
Pipeline #2360 passed with stage
in 41 seconds
......@@ -17,6 +17,7 @@
coverage
node_modules
npm-debug.log
.vscode
# Don't commit *.env except the template
*.env
......
This diff is collapsed.
{
"name": "service-organizations",
"version": "2.0.2",
"version": "2.1.0",
"description": "description",
"author": "",
"license": "MIT",
......@@ -18,10 +18,10 @@
"test:cov": "jest --coverage",
"test:e2e": "jest --config ./test/jest-e2e.json",
"webpack": "webpack --config webpack.config.js",
"typeorm:generate": "cross-env MIGRATION=1 ts-node -r tsconfig-paths/register ./node_modules/.bin/typeorm migration:generate -n",
"typeorm:migrate": "cross-env MIGRATION=1 ts-node -r tsconfig-paths/register ./node_modules/.bin/typeorm migration:run",
"typeorm:revert": "cross-env MIGRATION=1 ts-node -r tsconfig-paths/register ./node_modules/.bin/typeorm migration:revert",
"typeorm:migrate-ci": "ts-node -r tsconfig-paths/register ./node_modules/.bin/typeorm migration:run"
"typeorm:generate": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js migration:generate -n",
"typeorm:migrate": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js migration:run",
"typeorm:revert": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js migration:revert",
"typeorm:migrate-ci": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js migration:run"
},
"dependencies": {
"@godaddy/terminus": "^4.1.0",
......@@ -43,7 +43,6 @@
"reflect-metadata": "^0.1.12",
"rxjs": "^6.2.2",
"serve-favicon": "^2.5.0",
"swagger-stats": "^0.95.7",
"typeorm": "^0.2.7",
"typescript": "^3.0.1",
"uuid": "^3.3.2"
......
import * as dotenv from 'dotenv';
import * as fs from 'fs';
import { config } from './config';
export class ConfigService {
......
......@@ -33,13 +33,6 @@ export class LinksService {
const sort = query.sort_by ? query.sort_by.split('.') : ['name', 'ASC'];
qb.orderBy(sort[0], sort[1]);
if ('filter' in query) {
const filterJSON = JSON.parse(query.filter);
if (filterJSON.organizationId) {
qb.where('"organizationId" = ' + filterJSON.organizationId);
}
}
const linksCount = await qb.getCount().catch((error) => {
this.logger.error(`Error while getting the count of links.`, `${error}`, `${LinksService.name} - ${this.findAll.name}`);
throw new InternalServerErrorException({ error, message: 'Error while getting the count of links.' });
......
......@@ -2,7 +2,6 @@ import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { ValidationPipe } from '@nestjs/common';
import * as swStats from 'swagger-stats';
import * as favicon from 'serve-favicon';
import * as path from 'path';
import * as cookieParser from 'cookie-parser';
......@@ -10,7 +9,7 @@ import { AppLogger } from './app-logger';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: false,
logger: new AppLogger(),
});
app.enableCors({ credentials: true, origin: true, exposedHeaders: 'Content-range' });
......@@ -22,20 +21,15 @@ async function bootstrap() {
.setBasePath('')
.setTitle('Organizations service API')
.setDescription('This service provides all the methods (CRUD) needed to manage organizations with their associated links.')
.setVersion('0.1')
.setVersion(process.env.npm_package_version)
.build();
const document = SwaggerModule.createDocument(app, options);
// Enable stats middleware based on the generated swagger document
app.use(swStats.getMiddleware(document));
// Set endpoint serving the swagger documentation
SwaggerModule.setup('api-doc', app, document);
app.useGlobalPipes(new ValidationPipe());
app.useLogger(app.get(AppLogger));
await app.listen(3000);
}
bootstrap();
import {MigrationInterface, QueryRunner} from "typeorm";
export class draftMode1564557674250 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "organization" ADD "published" boolean NOT NULL DEFAULT false`);
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "organization" DROP COLUMN "published"`);
}
}
import { Organization } from './organization.entity';
import { Link } from '../links/link.dto';
import { ApiModelProperty, ApiModelPropertyOptional } from '@nestjs/swagger';
import { IsString, IsOptional, IsArray, MaxLength } from 'class-validator';
import { IsString, IsOptional, IsArray, MaxLength, IsBoolean } from 'class-validator';
export class OrganizationDTO {
@ApiModelProperty()
......@@ -27,6 +27,10 @@ export class OrganizationDTO {
@IsOptional()
@IsArray()
links?: [Link];
@ApiModelProperty()
@IsBoolean()
published: boolean;
}
export interface OrganizationsRO {
......
import { Entity, PrimaryColumn, Column, OneToMany, Generated, BeforeInsert, BeforeUpdate } from 'typeorm';
import { LinkEntity } from '../links/link.entity';
import { ApiModelProperty } from '@nestjs/swagger';
import { IsBoolean } from 'class-validator';
@Entity('organization')
export class Organization {
......@@ -31,4 +32,9 @@ export class Organization {
@Column('text', { nullable: true })
@ApiModelProperty()
logo: string;
@Column({ default: false })
@ApiModelProperty()
@IsBoolean()
published: boolean;
}
import { Controller, Get, Body, Post, Param, Delete, Put, Query, Response, UploadedFile, HttpCode } from '@nestjs/common';
import { Controller, Get, Body, Post, Param, Delete, Put, Query, Response, UploadedFile, HttpCode, Req } from '@nestjs/common';
import { OrganizationsService } from './organizations.service';
import { ApiUseTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { OrganizationDTO } from './organization.dto';
import { Organization } from './organization.entity';
import { LinkEntity } from '../links/link.entity';
import { Groups } from '../decorators/groups.decorators';
import { ConfigService } from '../configuration/config.service';
@ApiUseTags('organizations')
@Controller('organizations')
......@@ -12,13 +13,17 @@ export class OrganizationsController {
constructor(
private organizationsService: OrganizationsService,
private _configService: ConfigService,
) { }
@ApiOperation({ title: 'Get all organizations' })
@ApiResponse({ status: 200, description: 'Return all organizations.', type: Organization, isArray: true })
@Get()
async findAll(@Query() query, @Response() res): Promise<Organization[]> {
const { organizations, organizationsCount } = await this.organizationsService.findAll(query);
async findAll(@Query() query, @Response() res, @Req() req): Promise<Organization[]> {
const userGroups = req.headers[this._configService.config.groupHeader] ?
req.headers[this._configService.config.groupHeader].split(',').map(e => e.trim()) :
[];
const { organizations, organizationsCount } = await this.organizationsService.findAll(userGroups, query);
res.append('Content-range', organizationsCount);
res.send(organizations);
return;
......@@ -27,14 +32,20 @@ export class OrganizationsController {
@ApiOperation({ title: 'Get one organization' })
@ApiResponse({ status: 200, description: 'Return one organization.', type: Organization })
@Get(':id')
findOne(@Param('id') id: number): Promise<Organization> {
return this.organizationsService.findOne(id);
findOne(@Param('id') id: string, @Req() req): Promise<Organization> {
const userGroups = req.headers[this._configService.config.groupHeader] ?
req.headers[this._configService.config.groupHeader].split(',').map(e => e.trim()) :
[];
return this.organizationsService.findOne(userGroups, id);
}
@ApiResponse({ status: 200, description: 'Return the links of an organization.', type: LinkEntity, isArray: true })
@Get(':id/links')
findLinks(@Param('id') id: number): Promise<LinkEntity[]> {
return this.organizationsService.findLinks(id);
findLinks(@Param('id') id: string, @Req() req): Promise<LinkEntity[]> {
const userGroups = req.headers[this._configService.config.groupHeader] ?
req.headers[this._configService.config.groupHeader].split(',').map(e => e.trim()) :
[];
return this.organizationsService.findLinks(userGroups, id);
}
@ApiOperation({ title: 'Create one organization' })
......@@ -53,7 +64,7 @@ export class OrganizationsController {
@ApiResponse({ status: 403, description: 'User does not have sufficient rights.' })
@Put(':id')
@Groups('admin')
update(@Param('id') id: number, @Body() organizationDTo: OrganizationDTO) {
update(@Param('id') id: string, @Body() organizationDTo: OrganizationDTO) {
return this.organizationsService.update(id, organizationDTo);
}
......@@ -64,7 +75,7 @@ export class OrganizationsController {
@HttpCode(204)
@Delete(':id')
@Groups('admin')
delete(@Param('id') id: number) {
delete(@Param('id') id: string) {
return this.organizationsService.delete(id);
}
}
import { Injectable, Logger, InternalServerErrorException, NotFoundException } from '@nestjs/common';
import { Injectable, Logger, InternalServerErrorException, NotFoundException, ForbiddenException } from '@nestjs/common';
import { Organization } from './organization.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { LinksService } from '../links/links.service';
import { OrganizationsRO, OrganizationDTO } from './organization.dto';
import { OrganizationRepository } from './organization.repository';
import * as uuidv4 from 'uuid/v4';
import { ConfigService } from '../configuration/config.service';
@Injectable()
export class OrganizationsService {
......@@ -15,18 +16,28 @@ export class OrganizationsService {
@InjectRepository(Organization)
private organizationRepository: OrganizationRepository,
private linksService: LinksService,
private _configService: ConfigService,
) {
this.logger = new Logger(OrganizationsService.name);
}
async findAll(query?): Promise<OrganizationsRO> {
async findAll(userGroups, query): Promise<OrganizationsRO> {
try {
this.logger.log('Entering function', `${OrganizationsService.name} - ${this.findAll.name}`);
const qb = await this.organizationRepository
.createQueryBuilder('organization')
.leftJoinAndSelect('organization.links', 'links');
qb.where('1 = 1');
const [key, value] = query.q ? query.q.split(':') : [null, null];
// Only admin can see drafts
if (!userGroups || !userGroups.includes(this._configService.config.groupNames.admin)) {
qb.where(`organization.published = true`);
key && value ? qb.andWhere(`organization.${key} = :value`, { value }) : null;
} else {
key && value ? qb.where(`organization.${key} = :value`, { value }) : null;
}
// Sorting
if ('sort_by' in query) {
......@@ -59,22 +70,35 @@ export class OrganizationsService {
}
}
async findOne(id) {
async findOne(userGroups, id) {
try {
this.logger.log('Entering function', `${OrganizationsService.name} - ${this.findOne.name}`);
const organization = await this.organizationRepository.findOne(id).catch((error) => {
this.logger.error(`Error while looking the organization with id ${id}.`, `${error}`, `${OrganizationsService.name} - ${this.findOne.name}`);
throw new InternalServerErrorException({ error, message: 'Error while looking the organization with id ${id}.' });
this.logger.error(
`Error while looking for the organization with id ${id}.`,
`${error}`,
`${OrganizationsService.name} - ${this.findOne.name}`,
);
throw new InternalServerErrorException({ error, message: 'Error while looking for the organization with id ${id}.' });
});
if (!organization) {
throw new NotFoundException({ message: 'No organization with such id have been found' });
}
// Only admin can see an unpublished data producer
if ((!userGroups || !userGroups.includes(this._configService.config.groupNames.admin)) && organization.published === false) {
throw new ForbiddenException({ message: 'You don\'t have access to this organization.' });
}
return organization;
} catch (error) {
throw error;
}
}
async findLinks(id) {
async findLinks(userGroups, id) {
try {
this.logger.log('Entering function', `${OrganizationsService.name} - ${this.findLinks.name}`);
......@@ -88,6 +112,12 @@ export class OrganizationsService {
if (!organization) {
throw new NotFoundException({ message: 'No organization with such id have been found' });
}
// Only admin can see an unpublished data producer
if ((!userGroups || !userGroups.includes(this._configService.config.groupNames.admin)) && organization.published === false) {
throw new ForbiddenException({ message: 'You don\'t have access to this organization' });
}
return organization.links;
} catch (error) {
throw error;
......@@ -128,7 +158,11 @@ export class OrganizationsService {
this.logger.log('Entering function', `${OrganizationsService.name} - ${this.update.name}`);
const toUpdate = await this.organizationRepository.findOne(id).catch((error) => {
this.logger.error(`Error while looking for the organization with the id ${id}.`, `${error}`, `${OrganizationsService.name} - ${this.update.name}`);
this.logger.error(
`Error while looking for the organization with the id ${id}.`,
`${error}`,
`${OrganizationsService.name} - ${this.update.name}`,
);
throw new InternalServerErrorException({ error, message: 'Error while looking for the organization.' });
});
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment