Browse Source

FEATURE | Notifications about tasks deadlines

ip-block
Vitalik 9 months ago
parent
commit
c880ed2713
  1. 88
      package-lock.json
  2. 1
      package.json
  3. 4
      src/app.module.ts
  4. 12
      src/core/enums/events.enum.ts
  5. 6
      src/core/namespaces/notifications.namespace.ts
  6. 30
      src/domain/notifications/services/notifications-events.service.ts
  7. 1
      src/domain/tasks/crons/index.ts
  8. 88
      src/domain/tasks/crons/tasks-dedlines.cron.ts
  9. 2
      src/domain/tasks/tasks.module.ts
  10. 10
      src/rest/app/auth/controllers/app-auth.controller.ts
  11. 4
      src/rest/app/auth/services/app-auth.service.ts
  12. 31
      src/shared/abstracts/cron.abstract.ts
  13. 1
      src/shared/abstracts/index.ts

88
package-lock.json generated

@ -26,6 +26,7 @@ @@ -26,6 +26,7 @@
"axios": "^0.27.2",
"class-transformer": "^0.4.0",
"class-validator": "^0.13.1",
"cron": "^3.1.6",
"dotenv": "^10.0.0",
"express-basic-auth": "^1.2.0",
"http": "0.0.1-security",
@ -3499,6 +3500,14 @@ @@ -3499,6 +3500,14 @@
"reflect-metadata": "^0.1.12"
}
},
"node_modules/@nestjs/schedule/node_modules/cron": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/cron/-/cron-1.8.2.tgz",
"integrity": "sha512-Gk2c4y6xKEO8FSAUTklqtfSr7oTq0CiPQeLBG5Fl0qoXpZyMcj1SG59YL+hqq04bu6/IuEA7lMkYDAplQNKkyg==",
"dependencies": {
"moment-timezone": "^0.5.x"
}
},
"node_modules/@nestjs/schematics": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-7.3.1.tgz",
@ -4796,6 +4805,11 @@ @@ -4796,6 +4805,11 @@
"integrity": "sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==",
"dev": true
},
"node_modules/@types/luxon": {
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.8.tgz",
"integrity": "sha512-jYvz8UMLDgy3a5SkGJne8H7VA7zPV2Lwohjx0V8V31+SqAjNmurWMkk9cQhfvlcnXWudBpK9xPM1n4rljOcHYQ=="
},
"node_modules/@types/mime": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
@ -7306,11 +7320,12 @@ @@ -7306,11 +7320,12 @@
}
},
"node_modules/cron": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/cron/-/cron-1.8.2.tgz",
"integrity": "sha512-Gk2c4y6xKEO8FSAUTklqtfSr7oTq0CiPQeLBG5Fl0qoXpZyMcj1SG59YL+hqq04bu6/IuEA7lMkYDAplQNKkyg==",
"version": "3.1.6",
"resolved": "https://registry.npmjs.org/cron/-/cron-3.1.6.tgz",
"integrity": "sha512-cvFiQCeVzsA+QPM6fhjBtlKGij7tLLISnTSvFxVdnFGLdz+ZdXN37kNe0i2gefmdD17XuZA6n2uPVwzl4FxW/w==",
"dependencies": {
"moment-timezone": "^0.5.x"
"@types/luxon": "~3.3.0",
"luxon": "~3.4.0"
}
},
"node_modules/cross-spawn": {
@ -13102,6 +13117,14 @@ @@ -13102,6 +13117,14 @@
"integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==",
"dev": true
},
"node_modules/luxon": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
"integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==",
"engines": {
"node": ">=12"
}
},
"node_modules/macos-release": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.0.tgz",
@ -14256,19 +14279,19 @@ @@ -14256,19 +14279,19 @@
}
},
"node_modules/moment": {
"version": "2.29.3",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz",
"integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==",
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"engines": {
"node": "*"
}
},
"node_modules/moment-timezone": {
"version": "0.5.34",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.34.tgz",
"integrity": "sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg==",
"version": "0.5.45",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.45.tgz",
"integrity": "sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==",
"dependencies": {
"moment": ">= 2.9.0"
"moment": "^2.29.4"
},
"engines": {
"node": "*"
@ -22770,6 +22793,16 @@ @@ -22770,6 +22793,16 @@
"requires": {
"cron": "1.8.2",
"uuid": "8.3.2"
},
"dependencies": {
"cron": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/cron/-/cron-1.8.2.tgz",
"integrity": "sha512-Gk2c4y6xKEO8FSAUTklqtfSr7oTq0CiPQeLBG5Fl0qoXpZyMcj1SG59YL+hqq04bu6/IuEA7lMkYDAplQNKkyg==",
"requires": {
"moment-timezone": "^0.5.x"
}
}
}
},
"@nestjs/schematics": {
@ -23823,6 +23856,11 @@ @@ -23823,6 +23856,11 @@
"integrity": "sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==",
"dev": true
},
"@types/luxon": {
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.8.tgz",
"integrity": "sha512-jYvz8UMLDgy3a5SkGJne8H7VA7zPV2Lwohjx0V8V31+SqAjNmurWMkk9cQhfvlcnXWudBpK9xPM1n4rljOcHYQ=="
},
"@types/mime": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
@ -25832,11 +25870,12 @@ @@ -25832,11 +25870,12 @@
}
},
"cron": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/cron/-/cron-1.8.2.tgz",
"integrity": "sha512-Gk2c4y6xKEO8FSAUTklqtfSr7oTq0CiPQeLBG5Fl0qoXpZyMcj1SG59YL+hqq04bu6/IuEA7lMkYDAplQNKkyg==",
"version": "3.1.6",
"resolved": "https://registry.npmjs.org/cron/-/cron-3.1.6.tgz",
"integrity": "sha512-cvFiQCeVzsA+QPM6fhjBtlKGij7tLLISnTSvFxVdnFGLdz+ZdXN37kNe0i2gefmdD17XuZA6n2uPVwzl4FxW/w==",
"requires": {
"moment-timezone": "^0.5.x"
"@types/luxon": "~3.3.0",
"luxon": "~3.4.0"
}
},
"cross-spawn": {
@ -30399,6 +30438,11 @@ @@ -30399,6 +30438,11 @@
"integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==",
"dev": true
},
"luxon": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
"integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA=="
},
"macos-release": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.0.tgz",
@ -31354,16 +31398,16 @@ @@ -31354,16 +31398,16 @@
}
},
"moment": {
"version": "2.29.3",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz",
"integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw=="
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="
},
"moment-timezone": {
"version": "0.5.34",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.34.tgz",
"integrity": "sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg==",
"version": "0.5.45",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.45.tgz",
"integrity": "sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==",
"requires": {
"moment": ">= 2.9.0"
"moment": "^2.29.4"
}
},
"moo": {

1
package.json

@ -42,6 +42,7 @@ @@ -42,6 +42,7 @@
"axios": "^0.27.2",
"class-transformer": "^0.4.0",
"class-validator": "^0.13.1",
"cron": "^3.1.6",
"dotenv": "^10.0.0",
"express-basic-auth": "^1.2.0",
"http": "0.0.1-security",

4
src/app.module.ts

@ -26,7 +26,7 @@ import { @@ -26,7 +26,7 @@ import {
PushNotificationsModule,
RedisModule,
} from './libs'
import { getEnv, stringToBoolean } from './shared'
import { Cron, getEnv, stringToBoolean } from './shared'
import { getRestModules } from './rest'
import { ScheduleModule } from '@nestjs/schedule'
import { TypeOrmModule } from '@nestjs/typeorm'
@ -35,6 +35,8 @@ import { ThrottlerModule } from '@nestjs/throttler' @@ -35,6 +35,8 @@ import { ThrottlerModule } from '@nestjs/throttler'
const oldDbName = getEnv('OLD_DATABASE_DB')
Cron.setup(getEnv('CRON_ENABLED') === 'true')
const imports = [
ThrottlerModule.forRoot({
ttl: 60,

12
src/core/enums/events.enum.ts

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import { Logs, Sessions, Versions } from '../namespaces'
import { Logs, Sessions, Tasks, Versions } from '../namespaces'
import { Socket } from 'socket.io'
export enum Events {
@ -26,6 +26,8 @@ export enum Events { @@ -26,6 +26,8 @@ export enum Events {
OnChangeTaxonomies = 'OnChangeTaxonomies',
OnErrorJoinUser = 'OnErrorJoinUser',
OnUpdateEntity = 'OnUpdateEntity',
OnTaskDeadlineSoon = 'OnTaskDeadlineSoon',
OnTaskDeadlineExpired = 'OnTaskDeadlineExpired',
}
export interface IEventsPayloads {
@ -134,4 +136,12 @@ export interface IEventsPayloads { @@ -134,4 +136,12 @@ export interface IEventsPayloads {
type: Versions.EntityType
entityId: number
}
[Events.OnTaskDeadlineSoon]: {
task: Tasks.TaskModel
}
[Events.OnTaskDeadlineExpired]: {
task: Tasks.TaskModel
}
}

6
src/core/namespaces/notifications.namespace.ts

@ -21,6 +21,12 @@ export namespace Notifications { @@ -21,6 +21,12 @@ export namespace Notifications {
/** День народження */
TodayBirthday = 'todayBirthday',
/** Термін виконання задачі підходить до кінця **/
TaskDeadlineSoon = 'taskDeadlineSoon',
/** Термін виконання задачі пройшов **/
TaskDeadlineExpired = 'taskDeadlineExpired',
}
export enum NotificationsGroup {

30
src/domain/notifications/services/notifications-events.service.ts

@ -129,4 +129,34 @@ export class NotificationsEventsHandlerService { @@ -129,4 +129,34 @@ export class NotificationsEventsHandlerService {
})
}
}
@OnEvent(Events.OnTaskDeadlineSoon)
public async onTaskDeadlineSoon(payload: IEventsPayloads['OnTaskDeadlineSoon']) {
const { task } = payload
await this.notificationsService.addNotification({
userId: task.executorId,
title: 'Термін виконання задачі завтра',
content: `Термін виконання задачі "${task.name}" закінчується завтра (${task.endDate}).`,
group: Notifications.NotificationsGroup.Tasks,
data: {
taskId: task.id,
type: Notifications.NotificationType.TaskDeadlineSoon,
},
})
}
@OnEvent(Events.OnTaskDeadlineExpired)
public async onTaskDeadlineExpired(payload: IEventsPayloads['OnTaskDeadlineExpired']) {
const { task } = payload
await this.notificationsService.addNotification({
userId: task.executorId,
title: 'Термін виконання задачі минув',
content: `Термін виконання задачі "${task.name}" минув. Запланований час закінчення - ${task.endDate}.`,
group: Notifications.NotificationsGroup.Tasks,
data: {
taskId: task.id,
type: Notifications.NotificationType.TaskDeadlineExpired,
},
})
}
}

1
src/domain/tasks/crons/index.ts

@ -0,0 +1 @@ @@ -0,0 +1 @@
export * from './tasks-dedlines.cron'

88
src/domain/tasks/crons/tasks-dedlines.cron.ts

@ -0,0 +1,88 @@ @@ -0,0 +1,88 @@
import { Inject, Injectable } from '@nestjs/common'
import { TASKS_REPOSITORY } from '../consts'
import { ITasksRepository } from '../interfaces'
import { Cron } from 'src/shared'
import * as moment from 'moment'
import { Tasks } from 'src/core'
import { EventEmitter2 } from '@nestjs/event-emitter'
import { Events } from 'src/core/enums'
@Injectable()
export class TasksDedlinesCron extends Cron {
constructor(
@Inject(TASKS_REPOSITORY)
private readonly tasksRepository: ITasksRepository,
private eventEmitter: EventEmitter2,
) {
super()
}
protected register(): void {
this.start('0 10 * * *', this.everyDayBody.bind(this))
this.start('0 10 * * 0', this.everyWeekBody.bind(this))
}
protected async everyDayBody() {
try {
await this.findPreDeadlineTasks()
} catch (e) {}
try {
await this.findExpiredTasks()
} catch (e) {}
}
protected async everyWeekBody() {
const now = moment(new Date()).format('YYYY-MM-DD')
const tasks = await this.tasksRepository
.createQueryBuilder('it')
.where('it.endDate < :now', { now })
.andWhere('it.doneDate IS NULL')
.andWhere('it.status = :status', { status: Tasks.Status.Active })
.getMany()
console.log('everyWeekBody', tasks)
for await (const task of tasks) {
this.eventEmitter.emit(Events.OnTaskDeadlineExpired, {
task,
})
}
}
protected async findPreDeadlineTasks() {
const tomorrowDate = moment(new Date()).add(1, 'days').format('YYYY-MM-DD')
const tasks = await this.tasksRepository
.createQueryBuilder('it')
.where('it.endDate = :endDate', { endDate: tomorrowDate })
.andWhere('it.doneDate IS NULL')
.andWhere('it.status = :status', { status: Tasks.Status.Active })
.getMany()
console.log('findPreDeadlineTasks', tasks)
for await (const task of tasks) {
this.eventEmitter.emit(Events.OnTaskDeadlineSoon, {
task,
})
}
}
protected async findExpiredTasks() {
const date = moment(new Date()).subtract(1, 'days').format('YYYY-MM-DD')
const tasks = await this.tasksRepository
.createQueryBuilder('it')
.where('it.endDate = :endDate', { endDate: date })
.andWhere('it.doneDate IS NULL')
.andWhere('it.status = :status', { status: Tasks.Status.Active })
.getMany()
console.log('findExpiredTasks', tasks)
for await (const task of tasks) {
this.eventEmitter.emit(Events.OnTaskDeadlineExpired, {
task,
})
}
}
}

2
src/domain/tasks/tasks.module.ts

@ -33,6 +33,7 @@ import { @@ -33,6 +33,7 @@ import {
TasksService,
TASKS_SERVICES,
} from './services'
import { TasksDedlinesCron } from './crons'
@Module({})
export class TasksModule {
@ -82,6 +83,7 @@ export class TasksModule { @@ -82,6 +83,7 @@ export class TasksModule {
return {
module: TasksModule,
imports: TasksModule.getImports(),
providers: [provideEntity(TASKS_REPOSITORY, Task), TasksDedlinesCron],
}
}

10
src/rest/app/auth/controllers/app-auth.controller.ts

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
import { Body, Controller, Ip, Post } from '@nestjs/common'
import { ApiBody, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'
import { AuthGuard } from 'src/domain/sessions/decorators'
import { ReqUser } from 'src/shared'
import { ReqFingreprint, ReqUser } from 'src/shared'
import {
ConfirmLoginCodePayloadDto,
LoginPayloadDto,
@ -44,8 +44,12 @@ export class AppAuthController { @@ -44,8 +44,12 @@ export class AppAuthController {
type: TokenPairDto,
})
@Post('confirm-code')
public async confirmCode(@Ip() ip: string, @Body() dto: ConfirmLoginCodePayloadDto) {
return await this.appAuthService.confirmCode(ip, dto)
public async confirmCode(
@Ip() ip: string,
@Body() dto: ConfirmLoginCodePayloadDto,
@ReqFingreprint() fingerprint: string,
) {
return await this.appAuthService.confirmCode(ip, dto, fingerprint)
}
@ApiOperation({ summary: 'Перевірка коду' })

4
src/rest/app/auth/services/app-auth.service.ts

@ -84,7 +84,7 @@ export class AppAuthService implements OnModuleInit { @@ -84,7 +84,7 @@ export class AppAuthService implements OnModuleInit {
)
}
public async confirmCode(ip: string, dto: ConfirmLoginCodePayloadDto) {
public async confirmCode(ip: string, dto: ConfirmLoginCodePayloadDto, fingerprint: string) {
const isCorrect = await this.confirmationCodesService.confirmCode(
dto.phoneNumber,
dto.code,
@ -97,7 +97,7 @@ export class AppAuthService implements OnModuleInit { @@ -97,7 +97,7 @@ export class AppAuthService implements OnModuleInit {
this.sessionsService.addAuthAttemption({
ip,
targetUserId: user.id,
fingerprint: dto.fingerprint,
fingerprint: fingerprint,
data: {
login: dto.phoneNumber,
},

31
src/shared/abstracts/cron.abstract.ts

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
import { OnModuleDestroy } from '@nestjs/common'
import { CronJob } from 'cron'
export abstract class Cron implements OnModuleDestroy {
static isEnabled = null
static setup(isEnabled: boolean) {
this.isEnabled = isEnabled
}
protected abstract register(): void
protected job: CronJob
onModuleInit() {
if (Cron.isEnabled === null) throw new Error('Should setup Cron by Cron.setup')
if (Cron.isEnabled) this.register()
}
onModuleDestroy() {
try {
if (this.job) this.job.stop()
} catch (e) {}
}
protected start(time: string, fn: () => void) {
this.job = new CronJob(time, fn, null, true, 'UTC+2')
this.job.start()
}
}

1
src/shared/abstracts/index.ts

@ -1 +1,2 @@ @@ -1 +1,2 @@
export * from './cron.abstract'
export * from './seeder.abstract'

Loading…
Cancel
Save