diff --git a/apps/server/src/modules/alert/adapter/dto/component.dto.ts b/apps/server/src/modules/alert/adapter/dto/component.dto.ts new file mode 100644 index 00000000000..b5d80853327 --- /dev/null +++ b/apps/server/src/modules/alert/adapter/dto/component.dto.ts @@ -0,0 +1,53 @@ +export class ComponentDto { + constructor( + id: number, + name: string, + description: string, + link: string, + status: number, + order: number, + group_id: number, + created_at: Date, + updated_at: Date, + deleted_at: Date, + enabled: boolean, + status_name: string + ) { + this.id = id; + this.name = name; + this.description = description; + this.link = link; + this.status = status; + this.order = order; + this.group_id = group_id; + this.created_at = created_at; + this.updated_at = updated_at; + this.deleted_at = deleted_at; + this.enabled = enabled; + this.status_name = status_name; + } + + id: number; + + name: string; + + description: string; + + link: string; + + status: number; + + order: number; + + group_id: number; + + created_at: Date; + + updated_at: Date; + + deleted_at: Date; + + enabled: boolean; + + status_name: string; +} diff --git a/apps/server/src/modules/alert/adapter/dto/component.response.ts b/apps/server/src/modules/alert/adapter/dto/component.response.ts new file mode 100644 index 00000000000..eb568f4a2e0 --- /dev/null +++ b/apps/server/src/modules/alert/adapter/dto/component.response.ts @@ -0,0 +1,9 @@ +import { ComponentDto } from './component.dto'; + +export class ComponentResponse { + constructor(data: ComponentDto) { + this.data = data; + } + + data: ComponentDto; +} diff --git a/apps/server/src/modules/alert/adapter/dto/incident.dto.ts b/apps/server/src/modules/alert/adapter/dto/incident.dto.ts new file mode 100644 index 00000000000..7d8f80f7bf9 --- /dev/null +++ b/apps/server/src/modules/alert/adapter/dto/incident.dto.ts @@ -0,0 +1,89 @@ +export class IncidentDto { + constructor( + id: number, + component_id: number, + name: string, + status: number, + message: string, + created_at: Date, + updated_at: Date, + deleted_at: Date, + visible: number, + stickied: boolean, + occurred_at: Date, + user_id: number, + notifications: boolean, + is_resolved: boolean, + human_status: string, + latest_update_id: number, + latest_status: number, + latest_human_status: string, + latest_icon: string, + permalink: string, + duration: number + ) { + this.id = id; + this.component_id = component_id; + this.name = name; + this.status = status; + this.message = message; + this.created_at = created_at; + this.updated_at = updated_at; + this.deleted_at = deleted_at; + this.visible = visible; + this.stickied = stickied; + this.occurred_at = occurred_at; + this.user_id = user_id; + this.notifications = notifications; + this.is_resolved = is_resolved; + this.human_status = human_status; + this.latest_update_id = latest_update_id; + this.latest_status = latest_status; + this.latest_human_status = latest_human_status; + this.latest_icon = latest_icon; + this.permalink = permalink; + this.duration = duration; + } + + id: number; + + component_id: number; + + name: string; + + status: number; + + message: string; + + created_at: Date; + + updated_at: Date; + + deleted_at: Date; + + visible: number; + + stickied: boolean; + + occurred_at: Date; + + user_id: number; + + notifications: boolean; + + is_resolved: boolean; + + human_status: string; + + latest_update_id: number; + + latest_status: number; + + latest_human_status: string; + + latest_icon: string; + + permalink: string; + + duration: number; +} diff --git a/apps/server/src/modules/alert/adapter/dto/incidents.response.ts b/apps/server/src/modules/alert/adapter/dto/incidents.response.ts new file mode 100644 index 00000000000..862c1a7f629 --- /dev/null +++ b/apps/server/src/modules/alert/adapter/dto/incidents.response.ts @@ -0,0 +1,9 @@ +import { IncidentDto } from './incident.dto'; + +export class IncidentsResponse { + constructor(data: IncidentDto[]) { + this.data = data; + } + + data: IncidentDto[]; +} diff --git a/apps/server/src/modules/alert/adapter/dto/index.ts b/apps/server/src/modules/alert/adapter/dto/index.ts new file mode 100644 index 00000000000..fc93ba08715 --- /dev/null +++ b/apps/server/src/modules/alert/adapter/dto/index.ts @@ -0,0 +1,5 @@ +export * from './component.dto'; +export * from './component.response'; +export * from './incident.dto'; +export * from './incidents.response'; +export * from './messages.dto'; diff --git a/apps/server/src/modules/alert/adapter/dto/messages.dto.ts b/apps/server/src/modules/alert/adapter/dto/messages.dto.ts new file mode 100644 index 00000000000..e3af7e7e3c3 --- /dev/null +++ b/apps/server/src/modules/alert/adapter/dto/messages.dto.ts @@ -0,0 +1,12 @@ +import { Message } from '../../controller/dto'; + +export class MessagesDto { + constructor(messages: [], success: boolean) { + this.messages = messages; + this.success = success; + } + + messages: Message[]; + + success: boolean; +} diff --git a/apps/server/src/modules/alert/adapter/enum/importance.ts b/apps/server/src/modules/alert/adapter/enum/importance.ts new file mode 100644 index 00000000000..0644d22952d --- /dev/null +++ b/apps/server/src/modules/alert/adapter/enum/importance.ts @@ -0,0 +1,5 @@ +export enum Importance { + INGORE = -1, + ALL_INSTANCES = 0, + CURRENT_INSTANCE = 1, +} diff --git a/apps/server/src/modules/alert/adapter/enum/index.ts b/apps/server/src/modules/alert/adapter/enum/index.ts new file mode 100644 index 00000000000..35a6bea6b6b --- /dev/null +++ b/apps/server/src/modules/alert/adapter/enum/index.ts @@ -0,0 +1 @@ +export * from './importance'; diff --git a/apps/server/src/modules/alert/adapter/index.ts b/apps/server/src/modules/alert/adapter/index.ts new file mode 100644 index 00000000000..1bc0294fe06 --- /dev/null +++ b/apps/server/src/modules/alert/adapter/index.ts @@ -0,0 +1 @@ +export * from './status.adapter'; diff --git a/apps/server/src/modules/alert/adapter/status.adapter.spec.ts b/apps/server/src/modules/alert/adapter/status.adapter.spec.ts new file mode 100644 index 00000000000..c35c17bd001 --- /dev/null +++ b/apps/server/src/modules/alert/adapter/status.adapter.spec.ts @@ -0,0 +1,247 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { HttpService } from '@nestjs/axios'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ConfigService } from '@nestjs/config'; +import { ServerConfig } from '@modules/server'; +import { axiosResponseFactory } from '@shared/testing'; +import { of, throwError } from 'rxjs'; +import { AxiosError } from 'axios'; +import { StatusAdapter } from './status.adapter'; +import { ComponentDto, ComponentResponse, IncidentsResponse } from './dto'; +import { createComponent, createIncident } from '../testing'; + +describe('StatusAdapter', () => { + const incidentsPath = '/api/v1/incidents'; + const componentsPath = '/api/v1/components/'; + const incident = createIncident(2, 1, 0); + const incidentDefault = createIncident(1, 1, 0); + const incidentBrb = createIncident(2, 2, 0); + const incidentOpen = createIncident(3, 3, 0); + const incidentN21 = createIncident(4, 4, 0); + const incidentThr = createIncident(5, 5, 0); + const componentDefault = createComponent(1, 1); + const componentBrb = createComponent(2, 2); + const componentOpen = createComponent(3, 3); + const componentN21 = createComponent(4, 6); + const componentThr = createComponent(5, 7); + + let module: TestingModule; + let adapter: StatusAdapter; + let httpService: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + StatusAdapter, + { provide: HttpService, useValue: createMock() }, + { provide: ConfigService, useValue: createMock>({ get: () => 'test.url' }) }, + ], + }).compile(); + + adapter = module.get(StatusAdapter); + httpService = module.get(HttpService); + }); + + afterAll(async () => { + await module.close(); + }); + + it('should be defined', () => { + expect(adapter).toBeDefined(); + }); + + describe('getMessage', () => { + describe('when no incidents', () => { + const setup = () => { + jest.spyOn(httpService, 'get').mockReturnValue(of(axiosResponseFactory.build({ data: [] }))); + }; + it('should return empty data', async () => { + setup(); + + const data = await adapter.getMessage('default'); + + expect(data.success).toBe(false); + expect(data.messages.length).toBe(0); + }); + }); + + describe('when get incidents failed', () => { + const setup = () => { + jest.spyOn(httpService, 'get').mockReturnValue(throwError(() => 'error')); + }; + it('should return empty data', async () => { + setup(); + + const data = await adapter.getMessage('default'); + + expect(data.success).toBe(false); + expect(data.messages.length).toBe(0); + }); + }); + + describe('when get incidents return status 500', () => { + const setup = () => { + jest.spyOn(httpService, 'get').mockReturnValue(of(axiosResponseFactory.build({ status: 500 }))); + }; + it('should return empty data', async () => { + setup(); + + const data = await adapter.getMessage('default'); + + expect(data.success).toBe(false); + expect(data.messages.length).toBe(0); + }); + }); + + describe('when get components failed', () => { + const setup = () => { + jest.spyOn(httpService, 'get').mockImplementation((url) => { + if (url.match(incidentsPath)) { + const incidents = [incident]; + const incidentResponse = new IncidentsResponse(incidents); + const response = axiosResponseFactory.build({ data: incidentResponse }); + return of(response); + } + + if (url.match(componentsPath)) { + return throwError(() => new AxiosError()); + } + const response = axiosResponseFactory.build({ data: [] }); + return of(response); + }); + }; + it('should set importance to ignore', async () => { + setup(); + + const data = await adapter.getMessage('default'); + + expect(data.success).toBe(true); + expect(data.messages.length).toBe(0); + }); + }); + + describe('when get components return status 404', () => { + const setup = () => { + jest.spyOn(httpService, 'get').mockImplementation((url) => { + if (url.match(incidentsPath)) { + const incidents = [incident]; + const incidentResponse = new IncidentsResponse(incidents); + const response = axiosResponseFactory.build({ data: incidentResponse }); + return of(response); + } + + if (url.match(componentsPath)) { + return of(axiosResponseFactory.build({ status: 500 })); + } + const response = axiosResponseFactory.build({ data: [] }); + return of(response); + }); + }; + it('should set importance to ignore', async () => { + setup(); + + const data = await adapter.getMessage('default'); + + expect(data.success).toBe(true); + expect(data.messages.length).toBe(0); + }); + }); + + describe('when incidents for different instances provided', () => { + const setup = () => { + jest.spyOn(httpService, 'get').mockImplementation((url) => { + if (url.match(incidentsPath)) { + const incidents = [incidentDefault, incidentBrb, incidentOpen, incidentN21, incidentThr]; + const incidentResponse = new IncidentsResponse(incidents); + const response = axiosResponseFactory.build({ data: incidentResponse }); + return of(response); + } + + if (url.match(componentsPath)) { + const componentId = url.at(-1); + let component: ComponentDto; + if (componentId === '1') { + component = componentDefault; + } else if (componentId === '2') { + component = componentBrb; + } else if (componentId === '3') { + component = componentOpen; + } else if (componentId === '4') { + component = componentN21; + } else { + component = componentThr; + } + const componentResponse = new ComponentResponse(component); + const response = axiosResponseFactory.build({ data: componentResponse }); + return of(response); + } + const response = axiosResponseFactory.build({ data: [] }); + return of(response); + }); + }; + it.each(['default', 'brb', 'open', 'n21', 'thr', 'no_instance'])( + 'should return incident only for %s instance', + async (instance) => { + setup(); + + const data = await adapter.getMessage(instance); + + expect(data.success).toBe(true); + if (instance !== 'no_instance') { + expect(data.messages.length).toBe(1); + } else { + expect(data.messages.length).toBe(0); + } + } + ); + }); + + describe('when many incidents provided', () => { + const setup = () => { + jest.spyOn(httpService, 'get').mockImplementation((url) => { + if (url.match(incidentsPath)) { + const firstIncident = createIncident(1, 0, 1); + firstIncident.updated_at = new Date('2024-01-01 10:00:00'); + firstIncident.created_at = new Date('2024-01-01 10:00:00'); + firstIncident.name = '1'; + const secondIncident = createIncident(1, 0, 1); + secondIncident.updated_at = new Date('2024-01-01 10:00:00'); + secondIncident.created_at = new Date('2024-01-01 10:00:00'); + secondIncident.name = '2'; + const thirdIncident = createIncident(1, 0, 2); + thirdIncident.updated_at = new Date('2024-01-03 10:00:00'); + thirdIncident.created_at = new Date('2024-01-02 10:00:00'); + thirdIncident.name = '3'; + const fourthIncident = createIncident(1, 0, 2); + fourthIncident.updated_at = new Date('2024-01-03 10:00:00'); + fourthIncident.created_at = new Date('2024-01-01 10:00:00'); + fourthIncident.name = '4'; + const fifthIncident = createIncident(1, 0, 2); + fifthIncident.updated_at = new Date('2024-01-01 10:00:00'); + fifthIncident.created_at = new Date('2024-01-01 10:00:00'); + fifthIncident.name = '5'; + const incidents = [fifthIncident, fourthIncident, thirdIncident, firstIncident, secondIncident]; + const incidentResponse = new IncidentsResponse(incidents); + const response = axiosResponseFactory.build({ data: incidentResponse }); + return of(response); + } + const response = axiosResponseFactory.build({ data: [] }); + return of(response); + }); + }; + it('should return sorted incidents', async () => { + setup(); + + const data = await adapter.getMessage('default'); + + expect(data.success).toBe(true); + expect(data.messages.length).toBe(5); + expect(data.messages[0].title).toBe('1'); + expect(data.messages[1].title).toBe('2'); + expect(data.messages[2].title).toBe('3'); + expect(data.messages[3].title).toBe('4'); + expect(data.messages[4].title).toBe('5'); + }); + }); + }); +}); diff --git a/apps/server/src/modules/alert/adapter/status.adapter.ts b/apps/server/src/modules/alert/adapter/status.adapter.ts new file mode 100644 index 00000000000..e164bada4b2 --- /dev/null +++ b/apps/server/src/modules/alert/adapter/status.adapter.ts @@ -0,0 +1,172 @@ +import { HttpService } from '@nestjs/axios'; +import { Injectable, InternalServerErrorException } from '@nestjs/common'; +import { firstValueFrom } from 'rxjs'; +import { ErrorUtils } from '@src/core/error/utils'; +import { ConfigService } from '@nestjs/config'; +import { AlertConfig } from '../alert.config'; +import { Importance } from './enum'; +import { ComponentResponse, IncidentDto, IncidentsResponse, MessagesDto } from './dto'; +import { MessageMapper } from '../controller/mapper'; + +@Injectable() +export class StatusAdapter { + private readonly url: string; + + constructor( + private readonly httpService: HttpService, + private readonly configService: ConfigService + ) { + this.url = configService.get('ALERT_STATUS_URL'); + } + + public async getMessage(instance: string) { + const rawData = await this.getIncidentsData(instance); + const data = new MessagesDto([], false); + + if (rawData) { + rawData.forEach((element) => { + const message = MessageMapper.mapToMessage(element, this.url); + data.messages.push(message); + }); + data.success = true; + } else { + data.success = false; + } + + return data; + } + + private async getIncidentsData(instance: string) { + try { + return await this.getData(instance); + } catch (err) { + return null; + } + } + + private async getData(instance: string) { + const instanceSpecific: IncidentDto[] = []; + const noneSpecific: IncidentDto[] = []; + const rawData = await this.getIncidents(); + const statusEnum = { fixed: 4, danger: 2 }; + + const filteredData = rawData.data.filter((element) => element.status !== statusEnum.fixed); + const promises = filteredData.map(async (element) => { + const importance = await this.getImportance(instance, element.component_id); + if (importance !== Importance.ALL_INSTANCES && importance !== Importance.INGORE) { + instanceSpecific.push(element); + } else if (importance !== Importance.INGORE) { + noneSpecific.push(element); + } + }); + + await Promise.all(promises); + + instanceSpecific.sort(this.compareIncidents); + noneSpecific.sort(this.compareIncidents); + + return instanceSpecific.concat(noneSpecific); + } + + private async getImportance(instance: string, componentId: number): Promise { + if (componentId !== 0) { + return this.getImportanceForComponent(instance, componentId); + } + return Importance.ALL_INSTANCES; + } + + private async getImportanceForComponent(instance: string, componentId: number): Promise { + try { + const response = await this.getComponent(componentId); + const instanceNumber = this.getInstanceNumber(instance); + if (instanceNumber === response.data.group_id) { + return Importance.CURRENT_INSTANCE; + } + return Importance.INGORE; + } catch (err) { + return Importance.INGORE; + } + } + + private async getComponent(componentId: number): Promise { + try { + const request = this.httpService.get(`${this.url}/api/v1/components/${componentId}`); + + const resp = await firstValueFrom(request); + + if (resp.status !== 200) { + throw new Error(`invalid HTTP status code in a response from the server - ${resp.status} instead of 200`); + } + + return resp.data; + } catch (error) { + throw new InternalServerErrorException( + null, + ErrorUtils.createHttpExceptionOptions(error, 'StatusAdapter:getComponent') + ); + } + } + + private async getIncidents(): Promise { + try { + const request = this.httpService.get(`${this.url}/api/v1/incidents`, { + params: { sort: 'id' }, + }); + + const resp = await firstValueFrom(request); + + if (resp.status !== 200 && resp.status !== 202) { + throw new Error(`invalid HTTP status code in a response from the server - ${resp.status} instead of 202`); + } + + return resp.data; + } catch (error) { + throw new InternalServerErrorException( + null, + ErrorUtils.createHttpExceptionOptions(error, 'StatusAdapter:getIncidents') + ); + } + } + + private compareIncidents = (a: IncidentDto, b: IncidentDto) => { + const dateA = new Date(a.updated_at); + const dateB = new Date(b.updated_at); + const createdAtA = new Date(a.created_at); + const createdAtB = new Date(b.created_at); + + // sort by status; danger first + if (a.status > b.status) return 1; + if (b.status > a.status) return -1; + // sort by newest + if (dateA > dateB) return -1; + if (dateB > dateA) return 1; + if (createdAtA > createdAtB) return -1; + if (createdAtB > createdAtA) return 1; + + return 0; + }; + + private getInstanceNumber(instance: string) { + if (instance.toLowerCase() === 'default') { + return 1; + } + + if (instance.toLowerCase() === 'brb') { + return 2; + } + + if (instance.toLowerCase() === 'open') { + return 3; + } + + if (instance.toLowerCase() === 'n21') { + return 6; + } + + if (instance.toLowerCase() === 'thr') { + return 7; + } + + return 0; + } +} diff --git a/apps/server/src/modules/alert/alert.config.ts b/apps/server/src/modules/alert/alert.config.ts new file mode 100644 index 00000000000..7189222c8af --- /dev/null +++ b/apps/server/src/modules/alert/alert.config.ts @@ -0,0 +1,7 @@ +import { SchulcloudTheme } from '@shared/domain/types'; + +export interface AlertConfig { + ALERT_CACHE_INTERVAL_MIN: number; + SC_THEME: SchulcloudTheme; + ALERT_STATUS_URL: string | null; +} diff --git a/apps/server/src/modules/alert/alert.module.ts b/apps/server/src/modules/alert/alert.module.ts new file mode 100644 index 00000000000..f45782908ef --- /dev/null +++ b/apps/server/src/modules/alert/alert.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { HttpModule } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { AlertController } from './controller'; +import { AlertUc } from './uc'; +import { AlertCacheService } from './service'; +import { StatusAdapter } from './adapter'; +import { AlertConfig } from './alert.config'; + +@Module({ + imports: [HttpModule], + controllers: [AlertController], + providers: [AlertUc, AlertCacheService, StatusAdapter, ConfigService], +}) +export class AlertModule {} diff --git a/apps/server/src/modules/alert/controller/alert.controller.ts b/apps/server/src/modules/alert/controller/alert.controller.ts new file mode 100644 index 00000000000..6ce82bb13cc --- /dev/null +++ b/apps/server/src/modules/alert/controller/alert.controller.ts @@ -0,0 +1,19 @@ +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { Controller, Get } from '@nestjs/common'; +import { AlertUc } from '../uc'; +import { AlertResponse } from './dto'; + +@ApiTags('Alert') +@Controller('alert') +export class AlertController { + constructor(private readonly alertUc: AlertUc) {} + + @ApiOperation({ summary: 'Get allerts' }) + @ApiResponse({ status: 201, type: AlertResponse }) + @Get() + public async find() { + const messages = await this.alertUc.find(); + + return new AlertResponse(messages); + } +} diff --git a/apps/server/src/modules/alert/controller/api-test/alert.api.spec.ts b/apps/server/src/modules/alert/controller/api-test/alert.api.spec.ts new file mode 100644 index 00000000000..4913bdf0fad --- /dev/null +++ b/apps/server/src/modules/alert/controller/api-test/alert.api.spec.ts @@ -0,0 +1,102 @@ +import { INestApplication } from '@nestjs/common'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { HttpService } from '@nestjs/axios'; +import { Test, TestingModule } from '@nestjs/testing'; +import request from 'supertest'; +import { of } from 'rxjs'; +import { axiosResponseFactory } from '@shared/testing'; +import { SchulcloudTheme } from '@shared/domain/types'; +import { serverConfig, ServerTestModule } from '../../../server'; +import { createComponent, createIncident } from '../../testing'; +import { AlertResponse } from '../dto'; +import { ComponentDto, ComponentResponse, IncidentsResponse } from '../../adapter/dto'; + +describe('Alert Controller api', () => { + const alertPath = '/alert'; + const incidentsPath = '/api/v1/incidents'; + const componentsPath = '/api/v1/components/'; + const incident1 = createIncident(1, 0, 2); + const incident2 = createIncident(2, 1, 0); + const incident3 = createIncident(3, 2, 4); + const component1 = createComponent(1, 1); + const component2 = createComponent(2, 2); + + let app: INestApplication; + let httpService: DeepMocked; + + beforeEach(async () => { + const config = serverConfig(); + config.ALERT_STATUS_URL = 'test'; + config.SC_THEME = SchulcloudTheme.DEFAULT; + + const module: TestingModule = await Test.createTestingModule({ + imports: [ServerTestModule], + }) + .overrideProvider(HttpService) + .useValue(createMock()) + .compile(); + app = module.createNestApplication(); + await app.init(); + httpService = module.get(HttpService); + jest.useFakeTimers(); + }); + + afterAll(async () => { + await app.close(); + }); + + describe('[GET]', () => { + describe('when no incidents', () => { + const setup = () => { + jest.spyOn(httpService, 'get').mockImplementation(() => { + const response = axiosResponseFactory.build({ data: [] }); + return of(response); + }); + }; + + it('should return empty alert list', async () => { + setup(); + const response = await request(app.getHttpServer()).get(alertPath).expect(200); + + const { data } = response.body as AlertResponse; + expect(data.length).toBe(0); + }); + }); + + describe('when incidents available', () => { + const setup = () => { + jest.spyOn(httpService, 'get').mockImplementation((url) => { + if (url.match(incidentsPath)) { + const incidents = [incident1, incident2, incident3]; + const incidentResponse = new IncidentsResponse(incidents); + const response = axiosResponseFactory.build({ data: incidentResponse }); + return of(response); + } + + if (url.match(componentsPath)) { + const componentId = url.at(-1); + let component: ComponentDto; + if (componentId === '1') { + component = component1; + } else { + component = component2; + } + const componentResponse = new ComponentResponse(component); + const response = axiosResponseFactory.build({ data: componentResponse }); + return of(response); + } + const response = axiosResponseFactory.build({ data: [] }); + return of(response); + }); + }; + + it('should return filtered alert list by status and instance', async () => { + setup(); + const response = await request(app.getHttpServer()).get(alertPath).expect(200); + + const { data } = response.body as AlertResponse; + expect(data.length).toBe(2); + }); + }); + }); +}); diff --git a/apps/server/src/modules/alert/controller/dto/alert.response.ts b/apps/server/src/modules/alert/controller/dto/alert.response.ts new file mode 100644 index 00000000000..65b07eec0bd --- /dev/null +++ b/apps/server/src/modules/alert/controller/dto/alert.response.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Message } from './message'; + +export class AlertResponse { + constructor(data: Message[]) { + this.data = data; + } + + @ApiProperty({ type: [Message] }) + data: Message[]; +} diff --git a/apps/server/src/modules/alert/controller/dto/index.ts b/apps/server/src/modules/alert/controller/dto/index.ts new file mode 100644 index 00000000000..9474caa801d --- /dev/null +++ b/apps/server/src/modules/alert/controller/dto/index.ts @@ -0,0 +1,3 @@ +export * from './alert.response'; +export * from './message'; +export * from './message-origin'; diff --git a/apps/server/src/modules/alert/controller/dto/message-origin.ts b/apps/server/src/modules/alert/controller/dto/message-origin.ts new file mode 100644 index 00000000000..b7b4870fc9e --- /dev/null +++ b/apps/server/src/modules/alert/controller/dto/message-origin.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class MessageOrigin { + constructor(message_id: number, page: string) { + this.message_id = message_id; + this.page = page; + } + + @ApiProperty() + message_id: number; + + @ApiProperty() + page: string; +} diff --git a/apps/server/src/modules/alert/controller/dto/message.ts b/apps/server/src/modules/alert/controller/dto/message.ts new file mode 100644 index 00000000000..cf85f3a40aa --- /dev/null +++ b/apps/server/src/modules/alert/controller/dto/message.ts @@ -0,0 +1,45 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { MessageOrigin } from './message-origin'; + +export type MessageStatus = 'danger' | 'done' | 'info'; + +export class Message { + constructor( + title: string, + text: string, + timestamp: Date, + origin: MessageOrigin, + url: string, + status: MessageStatus, + createdAt: Date + ) { + this.title = title; + this.text = text; + this.timestamp = timestamp; + this.origin = origin; + this.url = url; + this.status = status; + this.createdAt = createdAt; + } + + @ApiProperty() + title: string; + + @ApiProperty() + text: string; + + @ApiProperty() + timestamp: Date; + + @ApiProperty() + origin: MessageOrigin; + + @ApiProperty() + url: string; + + @ApiProperty() + status: MessageStatus; + + @ApiProperty() + createdAt: Date; +} diff --git a/apps/server/src/modules/alert/controller/index.ts b/apps/server/src/modules/alert/controller/index.ts new file mode 100644 index 00000000000..13757baeddf --- /dev/null +++ b/apps/server/src/modules/alert/controller/index.ts @@ -0,0 +1 @@ +export * from './alert.controller'; diff --git a/apps/server/src/modules/alert/controller/mapper/index.ts b/apps/server/src/modules/alert/controller/mapper/index.ts new file mode 100644 index 00000000000..44e680ca395 --- /dev/null +++ b/apps/server/src/modules/alert/controller/mapper/index.ts @@ -0,0 +1 @@ +export * from './message.mapper'; diff --git a/apps/server/src/modules/alert/controller/mapper/message.mapper.spec.ts b/apps/server/src/modules/alert/controller/mapper/message.mapper.spec.ts new file mode 100644 index 00000000000..df5b62a4784 --- /dev/null +++ b/apps/server/src/modules/alert/controller/mapper/message.mapper.spec.ts @@ -0,0 +1,28 @@ +import { MessageMapper } from './message.mapper'; +import { IncidentDto } from '../../adapter/dto'; + +describe('MessageMapper', () => { + describe('map to message', () => { + describe('when empty object', () => { + it('should map to defaults', () => { + const message = MessageMapper.mapToMessage({} as IncidentDto, ''); + + expect(message.title).toBe(''); + expect(message.text).toBe(''); + expect(message.timestamp).toBe('1970-01-01 00:00:00'); + expect(message.origin.message_id).toBe(-1); + expect(message.origin.page).toBe('status'); + expect(message.url).toBe(''); + expect(message.status).toBe('info'); + }); + }); + }); + + describe('get status', () => { + it('should return correct status from number', () => { + const statuses = [1, 2, 4].map((nb) => MessageMapper.getStatus(nb)); + + expect(statuses).toEqual(['info', 'danger', 'done']); + }); + }); +}); diff --git a/apps/server/src/modules/alert/controller/mapper/message.mapper.ts b/apps/server/src/modules/alert/controller/mapper/message.mapper.ts new file mode 100644 index 00000000000..42303435e6e --- /dev/null +++ b/apps/server/src/modules/alert/controller/mapper/message.mapper.ts @@ -0,0 +1,28 @@ +import { IncidentDto } from '../../adapter/dto'; +import { Message, MessageOrigin, MessageStatus } from '../dto'; + +export class MessageMapper { + static mapToMessage(incident: IncidentDto, url: string): Message { + return new Message( + incident.name || '', + incident.message || '', + incident.updated_at || '1970-01-01 00:00:00', + new MessageOrigin(incident.id || -1, 'status'), + url, + this.getStatus(incident.status), + incident.created_at + ); + } + + static getStatus(number: number): MessageStatus { + if (number === 2) { + return 'danger'; + } + + if (number === 4) { + return 'done'; + } + + return 'info'; + } +} diff --git a/apps/server/src/modules/alert/index.ts b/apps/server/src/modules/alert/index.ts new file mode 100644 index 00000000000..e54c9cd86d9 --- /dev/null +++ b/apps/server/src/modules/alert/index.ts @@ -0,0 +1,2 @@ +export { AlertConfig } from './alert.config'; +export { AlertModule } from './alert.module'; diff --git a/apps/server/src/modules/alert/service/alert-cache.service.ts b/apps/server/src/modules/alert/service/alert-cache.service.ts new file mode 100644 index 00000000000..69f1849a343 --- /dev/null +++ b/apps/server/src/modules/alert/service/alert-cache.service.ts @@ -0,0 +1,66 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { StatusAdapter } from '../adapter'; +import { Message } from '../controller/dto'; +import { AlertConfig } from '../alert.config'; + +@Injectable() +export class AlertCacheService { + private readonly cacheInterval: number; + + private lastUpdatedTimestamp = 0; + + private messages: Message[] = []; + + private messageProviders: StatusAdapter[] = []; + + private readonly instance: string; + + constructor( + private readonly configService: ConfigService, + private readonly statusAdapter: StatusAdapter + ) { + this.instance = configService.get('SC_THEME'); + this.cacheInterval = configService.get('ALERT_CACHE_INTERVAL_MIN'); + + if (configService.get('ALERT_STATUS_URL')) { + this.addMessageProvider(statusAdapter, true); + } + } + + public async updateMessages() { + let success = false; + let newMessages: Message[] = []; + this.lastUpdatedTimestamp = Date.now(); + + const promises = this.messageProviders.map(async (provider) => { + const data = await provider.getMessage(this.instance); + if (!data.success) { + success = false; + return; + } + newMessages = newMessages.concat(data.messages); + success = true; + }); + + await Promise.all(promises); + + if (success) { + this.messages = newMessages; + } + } + + public async getMessages() { + if (this.lastUpdatedTimestamp < Date.now() - 1000 * 60 * this.cacheInterval) { + await this.updateMessages(); + } + + return this.messages || []; + } + + public addMessageProvider(provider: StatusAdapter, featureEnabled: boolean) { + if (featureEnabled) { + this.messageProviders.push(provider); + } + } +} diff --git a/apps/server/src/modules/alert/service/index.ts b/apps/server/src/modules/alert/service/index.ts new file mode 100644 index 00000000000..b6e6fe7676f --- /dev/null +++ b/apps/server/src/modules/alert/service/index.ts @@ -0,0 +1 @@ +export * from './alert-cache.service'; diff --git a/apps/server/src/modules/alert/testing/component.factory.ts b/apps/server/src/modules/alert/testing/component.factory.ts new file mode 100644 index 00000000000..a7e5e9fcb29 --- /dev/null +++ b/apps/server/src/modules/alert/testing/component.factory.ts @@ -0,0 +1,4 @@ +import { ComponentDto } from '../adapter/dto'; + +export const createComponent = (id: number, groupId: number) => + new ComponentDto(id, 'test', 'test', 'test', 1, 0, groupId, new Date(), new Date(), new Date(), true, 'test'); diff --git a/apps/server/src/modules/alert/testing/incident.factory.ts b/apps/server/src/modules/alert/testing/incident.factory.ts new file mode 100644 index 00000000000..3415b15421e --- /dev/null +++ b/apps/server/src/modules/alert/testing/incident.factory.ts @@ -0,0 +1,26 @@ +import { IncidentDto } from '../adapter/dto'; + +export const createIncident = (id: number, componentId: number, status: number) => + new IncidentDto( + 1, + componentId, + 'test', + status, + 'test', + new Date(), + new Date(), + new Date(), + 1, + false, + new Date(), + 1, + false, + false, + 'test', + 0, + 0, + 'test', + 'test', + 'test', + 0 + ); diff --git a/apps/server/src/modules/alert/testing/index.ts b/apps/server/src/modules/alert/testing/index.ts new file mode 100644 index 00000000000..71769082003 --- /dev/null +++ b/apps/server/src/modules/alert/testing/index.ts @@ -0,0 +1,2 @@ +export * from './incident.factory'; +export * from './component.factory'; diff --git a/apps/server/src/modules/alert/uc/alert.uc.ts b/apps/server/src/modules/alert/uc/alert.uc.ts new file mode 100644 index 00000000000..791fd6d70c3 --- /dev/null +++ b/apps/server/src/modules/alert/uc/alert.uc.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@nestjs/common'; +import { AlertCacheService } from '../service'; + +@Injectable() +export class AlertUc { + constructor(private readonly cacheService: AlertCacheService) {} + + public find() { + return this.cacheService.getMessages(); + } +} diff --git a/apps/server/src/modules/alert/uc/index.ts b/apps/server/src/modules/alert/uc/index.ts new file mode 100644 index 00000000000..7fdb35111a6 --- /dev/null +++ b/apps/server/src/modules/alert/uc/index.ts @@ -0,0 +1 @@ +export * from './alert.uc'; diff --git a/apps/server/src/modules/server/api/dto/config.response.ts b/apps/server/src/modules/server/api/dto/config.response.ts index a7133c730c9..23e6265d260 100644 --- a/apps/server/src/modules/server/api/dto/config.response.ts +++ b/apps/server/src/modules/server/api/dto/config.response.ts @@ -1,7 +1,7 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { LanguageType } from '@shared/domain/interface'; +import { SchulcloudTheme } from '@shared/domain/types'; import type { ServerConfig } from '../..'; -import { SchulcloudTheme } from '../../types/schulcloud-theme.enum'; import { Timezone } from '../../types/timezone.enum'; export class ConfigResponse { diff --git a/apps/server/src/modules/server/server.config.ts b/apps/server/src/modules/server/server.config.ts index a8b11b6a512..57611ee3cde 100644 --- a/apps/server/src/modules/server/server.config.ts +++ b/apps/server/src/modules/server/server.config.ts @@ -23,7 +23,8 @@ import { VideoConferenceConfiguration, type IVideoConferenceSettings } from '@mo import { LanguageType } from '@shared/domain/interface'; import type { CoreModuleConfig } from '@src/core'; import type { MailConfig } from '@src/infra/mail/interfaces/mail-config'; -import { SchulcloudTheme } from './types/schulcloud-theme.enum'; +import { AlertConfig } from '@modules/alert'; +import { SchulcloudTheme } from '@shared/domain/types'; import { Timezone } from './types/timezone.enum'; export enum NodeEnvType { @@ -59,7 +60,8 @@ export interface ServerConfig SynchronizationConfig, DeletionConfig, CollaborativeTextEditorConfig, - ProvisioningConfig { + ProvisioningConfig, + AlertConfig { NODE_ENV: NodeEnvType; SC_DOMAIN: string; ACCESSIBILITY_REPORT_EMAIL: string; @@ -229,6 +231,7 @@ const config: ServerConfig = { FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED: Configuration.get( 'FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED' ) as boolean, + ALERT_CACHE_INTERVAL_MIN: Configuration.get('ALERT_CACHE_INTERVAL_MIN') as number, }; export const serverConfig = () => config; diff --git a/apps/server/src/modules/server/server.module.ts b/apps/server/src/modules/server/server.module.ts index ea0e6d348d7..aebeeb9acd3 100644 --- a/apps/server/src/modules/server/server.module.ts +++ b/apps/server/src/modules/server/server.module.ts @@ -40,6 +40,7 @@ import { ALL_ENTITIES } from '@shared/domain/entity'; import { DB_PASSWORD, DB_URL, DB_USERNAME, createConfigModuleOptions } from '@src/config'; import { CoreModule } from '@src/core'; import { LoggerModule } from '@src/core/logger'; +import { AlertModule } from '@modules/alert/alert.module'; import { ServerConfigController, ServerController, ServerUc } from './api'; import { SERVER_CONFIG_TOKEN, serverConfig } from './server.config'; @@ -95,6 +96,7 @@ const serverModules = [ MeApiModule, MediaBoardApiModule, CollaborativeTextEditorApiModule, + AlertModule, ]; export const defaultMikroOrmOptions: MikroOrmModuleSyncOptions = { diff --git a/apps/server/src/shared/domain/types/index.ts b/apps/server/src/shared/domain/types/index.ts index b47a0d4821b..dacc63de9ee 100644 --- a/apps/server/src/shared/domain/types/index.ts +++ b/apps/server/src/shared/domain/types/index.ts @@ -10,3 +10,4 @@ export * from './school-purpose.enum'; export * from './system.type'; export * from './task.types'; export * from './value-of'; +export * from './schulcloud-theme.enum'; diff --git a/apps/server/src/modules/server/types/schulcloud-theme.enum.ts b/apps/server/src/shared/domain/types/schulcloud-theme.enum.ts similarity index 100% rename from apps/server/src/modules/server/types/schulcloud-theme.enum.ts rename to apps/server/src/shared/domain/types/schulcloud-theme.enum.ts diff --git a/config/default.schema.json b/config/default.schema.json index 461e2d44d5d..5850d8c00e4 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -545,6 +545,11 @@ "default": null, "description": "The url of status message provider (should end without a slash)." }, + "ALERT_CACHE_INTERVAL_MIN": { + "type": "number", + "default": 1, + "description": "Time between updating alerts (in minutes)" + }, "NEXBOARD_URL": { "type": "string", "format": "uri", diff --git a/src/externalServices/index.js b/src/externalServices/index.js deleted file mode 100644 index e30c29092f0..00000000000 --- a/src/externalServices/index.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Please add external api services to this location to get an overview over connections. - * For any request that is send please use const Api = require('./apiHelper'); - */ - -const statusApi = require('./statusApi'); - -module.exports = { - statusApi, -}; diff --git a/src/externalServices/statusApi.js b/src/externalServices/statusApi.js deleted file mode 100644 index 6b71611240a..00000000000 --- a/src/externalServices/statusApi.js +++ /dev/null @@ -1,22 +0,0 @@ -const { Configuration } = require('@hpi-schul-cloud/commons'); -const Api = require('./apiHelper'); - -const statusURL = Configuration.get('ALERT_STATUS_URL'); -const baseURL = statusURL.concat('/api/v1'); - -const statusApi = () => { - const api = new Api({ - baseURL, - }); - // TODO: if possible request only related time not all - const getIncidents = (sort = 'id') => api.get('/incidents', { params: { sort } }).then((response) => response.data); - - const getComponent = (componentId) => api.get(`/components/${componentId}`).then((response) => response.data); - - return { - getIncidents, - getComponent, - }; -}; - -module.exports = statusApi(); diff --git a/src/services/alert/MessageProvider/status/index.js b/src/services/alert/MessageProvider/status/index.js deleted file mode 100644 index e7846c7c598..00000000000 --- a/src/services/alert/MessageProvider/status/index.js +++ /dev/null @@ -1,89 +0,0 @@ -const logger = require('../../../../logger'); -const { statusApi } = require('../../../../externalServices'); - -const dict = { - default: 1, - brb: 2, - open: 3, - n21: 6, - thr: 7, -}; - -const importance = { - INGORE: -1, - ALL_INSTANCES: 0, - CURRENT_INSTANCE: 1, -}; - -/** - * Check if Message is instance specific - * @param {string} instance - * @param {number} componentId - * @returns {number} - */ -async function getInstance(instance, componentId) { - if (componentId !== 0) { - try { - const response = await statusApi.getComponent(componentId); - if (dict[instance] && response.data.group_id === dict[instance]) { - return importance.CURRENT_INSTANCE; - } - return importance.INGORE; - } catch (error) { - return importance.INGORE; - } - } else { - return importance.ALL_INSTANCES; - } -} - -function compare(a, b) { - const dateA = new Date(a.updated_at); - const dateB = new Date(b.updated_at); - const createdAtA = new Date(a.createdAt); - const createdAtB = new Date(b.createdAt); - - // sort by status; danger first - if (a.status > b.status) return 1; - if (b.status > a.status) return -1; - // sort by newest - if (dateA > dateB) return -1; - if (dateB > dateA) return 1; - if (createdAtA > createdAtB) return -1; - if (createdAtB > createdAtA) return 1; - - return 0; -} - -module.exports = { - async getData(instance) { - try { - const rawData = await statusApi.getIncidents(); - const instanceSpecific = []; - const noneSpecific = []; - const statusEnum = { fixed: 4, danger: 2 }; - - const filteredData = rawData.data.filter((element) => element.status !== statusEnum.fixed); - const promises = filteredData.map(async (element) => { - const isinstance = await getInstance(instance, element.component_id); - if (isinstance !== importance.ALL_INSTANCES && isinstance !== importance.INGORE) { - instanceSpecific.push(element); - } else if (isinstance !== importance.INGORE) { - noneSpecific.push(element); - } - }); - - await Promise.all(promises); - - // do some sorting - instanceSpecific.sort(compare); - noneSpecific.sort(compare); - - return instanceSpecific.concat(noneSpecific); - } catch (err) { - // return null on error - logger.error(err); - return null; - } - }, -}; diff --git a/src/services/alert/adapter/index.js b/src/services/alert/adapter/index.js deleted file mode 100644 index 24dd91301b2..00000000000 --- a/src/services/alert/adapter/index.js +++ /dev/null @@ -1,18 +0,0 @@ -/* eslint-disable no-unused-vars */ -class Adapter { - constructor() { - if (new.target === Adapter) { - throw new TypeError('Cannot construct Abstract instances directly'); - } - } - - /** - * Return - * @param {String} instances - */ - async getMessage(instances) { - throw new Error('You have to implement the method getMessage!'); - } -} - -module.exports = Adapter; diff --git a/src/services/alert/adapter/message.js b/src/services/alert/adapter/message.js deleted file mode 100644 index 8d63e7c8be5..00000000000 --- a/src/services/alert/adapter/message.js +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Unified message format - */ -class Message { - constructor(title, text, timestamp, page, messageId, url, status, createdAt) { - this.mTitle = title || ''; - this.mText = text || ''; - this.mTimestamp = timestamp || '1970-01-01 00:00:00'; - // Origin of Message - this.mPage = page || ''; - this.mMessageId = messageId || '-1'; - this.mUrl = url || ''; - this.mStatus = status || ''; - this.mCreatedAt = createdAt || ''; - } - - get getMessage() { - // Unified message format - const message = { - title: this.mTitle, - text: this.mText, - status: this.mStatus, - origin: { - page: this.mPage, - message_id: this.mMessageId, - }, - timestamp: this.mTimestamp, - url: this.mUrl, - createdAt: this.mCreatedAt, - }; - return message; - } - - /** - * Set Title of Message - */ - set title(value) { - this.mTitle = value; - } - - /** - * Set Text of Message - */ - set text(value) { - this.mText = value; - } - - /** - * Set Timestamp of Message - */ - set timestamp(value) { - this.mTimestamp = value; - } - - /** - * Set Origin of Message - */ - set page(value) { - this.mPage = value; - } - - /** - * Set Id of Message - */ - set messageId(value) { - this.mMessageId = value; - } - - /** - * Set URL to link to - */ - set url(value) { - this.mUrl = value; - } - - /** - * Set Status of message - */ - set status(value) { - this.mStatus = value; - } - - /** - * Set Status of message - */ - set createdAt(value) { - this.mCreatedAt = value; - } -} - -module.exports = Message; diff --git a/src/services/alert/adapter/status.js b/src/services/alert/adapter/status.js deleted file mode 100644 index 57a9c91548d..00000000000 --- a/src/services/alert/adapter/status.js +++ /dev/null @@ -1,48 +0,0 @@ -const { Configuration } = require('@hpi-schul-cloud/commons'); -const Status = require('../MessageProvider/status'); -const Adapter = require('./index'); -const Message = require('./message'); - -class StatusAdapter extends Adapter { - async getMessage(instance) { - const data = { - success: false, - messages: [], - }; - - const getStatus = (number) => { - switch (number) { - case 2: - return 'danger'; - case 4: - return 'done'; - default: - return 'info'; - } - }; - - // get raw data from Message Provider - const rawData = await Status.getData(instance); - // transform raw data in unified message format - if (rawData) { - rawData.forEach((element) => { - const message = new Message(); - message.title = element.name; - message.text = element.message; - message.status = getStatus(element.status); - message.page = 'status'; - message.messageId = element.id; - message.timestamp = element.updated_at; - message.createdAt = element.created_at; - message.url = Configuration.get('ALERT_STATUS_URL'); - data.messages.push(message.getMessage); - }); - data.success = true; - } else { - data.success = false; - } - return data; - } -} - -module.exports = StatusAdapter; diff --git a/src/services/alert/cache.js b/src/services/alert/cache.js deleted file mode 100644 index de45f6c0cc5..00000000000 --- a/src/services/alert/cache.js +++ /dev/null @@ -1,58 +0,0 @@ -const { SC_THEME } = require('../../../config/globals'); - -const MessageProvider = []; -let messages = null; -let lastUpdatedTimestamp = 0; - -class Cache { - /** - * @param {number} time how long message should remain cached in minutes - */ - constructor(time) { - this.time = time; - } - - async updateMessages() { - let success = false; - let newMessages = []; - - // set last updated always to avoid DoS in error state - // set last updated here to avoid updating cache simultaneously for multiple times - lastUpdatedTimestamp = Date.now(); - - const promises = MessageProvider.map(async (provider) => { - const data = await provider.getMessage(SC_THEME); - if (!data.success) { - success = false; - return; - } - newMessages = newMessages.concat(data.messages); - success = true; - }); - - await Promise.all(promises); - - if (success) { - messages = newMessages; - } - } - - async getMessages() { - if (lastUpdatedTimestamp < Date.now() - 1000 * 60 * this.time) { - if (!messages) { - await this.updateMessages(); - } else { - this.updateMessages(); - } - } - return messages || []; - } - - addMessageProvider(provider, featureEnabled) { - if (featureEnabled) { - MessageProvider.push(provider); - } - } -} - -module.exports = Cache; diff --git a/src/services/alert/docs/openapi.yaml b/src/services/alert/docs/openapi.yaml deleted file mode 100644 index cfabda27bf5..00000000000 --- a/src/services/alert/docs/openapi.yaml +++ /dev/null @@ -1,74 +0,0 @@ -security: - - jwtBearer: [] -info: - title: HPI Schul-Cloud Alert Service API - description: - This is the API specification for the HPI Schul-Cloud Alert service. - - contact: - name: support - email: info@dbildungscloud.de - license: - name: GPL-3.0 - url: 'https://github.com/hpi-schul-cloud/schulcloud-server/blob/master/LICENSE' - version: 1.0.0 -components: - securitySchemes: - jwtBearer: - type: http - scheme: bearer - bearerFormat: JWT - schemas: - alert: - description: TODO - alert_list: - description: TODO - -paths: - /alert: - get: - parameters: - - description: Number of results to return - in: query - name: $limit - schema: - type: integer - - description: Number of results to skip - in: query - name: $skip - schema: - type: integer - - description: Property to sort results - in: query - name: $sort - style: deepObject - schema: - type: object - - description: Query parameters to filter - in: query - name: filter - style: form - explode: true - schema: - $ref: '#/components/schemas/alert' - responses: - '200': - description: success - content: - application/json: - schema: - $ref: '#/components/schemas/alert_list' - '401': - description: not authenticated - '500': - description: general error - description: Retrieves a list of all resources from the service. - summary: '' - tags: - - alert - security: [] - -openapi: 3.0.2 -tags: - - name: alert - description: An alert service. diff --git a/src/services/alert/index.js b/src/services/alert/index.js deleted file mode 100644 index 42c2db3df4b..00000000000 --- a/src/services/alert/index.js +++ /dev/null @@ -1,43 +0,0 @@ -const hooks = require('feathers-hooks-common'); -const { Configuration } = require('@hpi-schul-cloud/commons'); -const { static: staticContent } = require('@feathersjs/express'); -const path = require('path'); - -const Cache = require('./cache'); - -// add Message Provider Adapter here -const StatusAdapter = require('./adapter/status'); - -const cache = new Cache(1); -// add Message Provider here -cache.addMessageProvider(new StatusAdapter(), Configuration.has('ALERT_STATUS_URL')); - -/** - * Service to get an array of alert messages from added Message Providers (e.g: status.hpi-schul-cloud.de) - */ -class AlertService { - async find() { - return cache.getMessages(); - } -} - -module.exports = function alert() { - const app = this; - - app.use('/alert/api', staticContent(path.join(__dirname, '/docs/openapi.yaml'))); - - app.use('/alert', new AlertService()); - const service = app.service('/alert'); - - service.hooks({ - before: { - all: [], - find: [], - get: [hooks.disallow()], - create: [hooks.disallow()], - update: [hooks.disallow()], - patch: [hooks.disallow()], - remove: [hooks.disallow()], - }, - }); -}; diff --git a/src/services/index.js b/src/services/index.js index 15c868e7bf2..930ad5fde2d 100644 --- a/src/services/index.js +++ b/src/services/index.js @@ -41,7 +41,6 @@ const webuntis = require('./webuntis'); const me = require('./me'); const help = require('./help'); const database = require('../utils/database'); -const alert = require('./alert'); const nexboard = require('./nexboard'); const etherpad = require('./etherpad'); const storageProvider = require('./storageProvider'); @@ -94,7 +93,6 @@ module.exports = function initializeServices() { app.configure(oauth2); app.configure(roster); app.configure(datasources); - app.configure(alert); app.configure(edusharing); app.configure(webuntis); app.configure(nexboard);