Compare commits
4 Commits
Author | SHA1 | Date |
---|---|---|
Vitalik | c880ed2713 | 9 months ago |
Vitalik | 752d330bf7 | 9 months ago |
Vitalik | 96eecc4263 | 9 months ago |
Vitalik | 0e08a9b801 | 9 months ago |
58 changed files with 1137 additions and 130 deletions
@ -0,0 +1,58 @@
@@ -0,0 +1,58 @@
|
||||
version: '3' |
||||
|
||||
services: |
||||
taskme-postgres: |
||||
image: postgres:11 |
||||
restart: always |
||||
|
||||
ports: |
||||
- 3303:5432 |
||||
|
||||
environment: |
||||
POSTGRES_PASSWORD: ${DATABASE_PASS} |
||||
POSTGRES_USER: ${DATABASE_USER} |
||||
POSTGRES_DB: ${DATABASE_DB} |
||||
|
||||
taskme-redis: |
||||
image: 'redis:4-alpine' |
||||
command: redis-server --requirepass ${REDIS_PASS} |
||||
ports: |
||||
- '6379:6379' |
||||
|
||||
taskme-minio: |
||||
hostname: taskme-minio |
||||
image: minio/minio:RELEASE.2021-09-18T18-09-59Z |
||||
container_name: taskme-minio |
||||
|
||||
volumes: |
||||
- './taskme/data/:/data' |
||||
- './taskme/config:/root/.minio' |
||||
|
||||
ports: |
||||
- 5003:9000 |
||||
- 5004:9001 |
||||
environment: |
||||
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY} |
||||
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY} |
||||
command: server --console-address ":9001" /data |
||||
|
||||
taskme-createbuckets: |
||||
image: minio/mc |
||||
depends_on: |
||||
- taskme-minio |
||||
entrypoint: > |
||||
/bin/sh -c " |
||||
sleep 10; |
||||
/usr/bin/mc config host add data http://${MINIO_HOST}:${MINIO_PORT} ${MINIO_ACCESS_KEY} ${MINIO_SECRET_KEY}; |
||||
/usr/bin/mc mb data/${MINIO_BUCKET}; |
||||
/usr/bin/mc policy set public data/${MINIO_BUCKET}; |
||||
exit 0; |
||||
" |
||||
taskme-imgproxy: |
||||
image: 'darthsim/imgproxy:latest' |
||||
ports: |
||||
- '5005:8080' |
||||
environment: |
||||
IMGPROXY_KEY: ${IMGPROXY_KEY} |
||||
IMGPROXY_SALT: ${IMGPROXY_SALT} |
||||
IMGPROXY_MAX_SRC_FILE_SIZE: 10485760 |
@ -0,0 +1,91 @@
@@ -0,0 +1,91 @@
|
||||
import { IUsersRepository } from 'src/domain/users/interfaces' |
||||
import { attempsKeys } from '../helpers' |
||||
import { IPs, Mailer, Users } from 'src/core' |
||||
import { IIPsRepository } from 'src/domain/ips/interfaces' |
||||
|
||||
export class BlockedIpAlert { |
||||
private blockedIp: string |
||||
private clearIp: string |
||||
private fingerprint: string |
||||
private blocked?: IPs.IIP |
||||
|
||||
private get targetUser() { |
||||
if (this.blocked) return this.blocked.user |
||||
return null |
||||
} |
||||
|
||||
constructor( |
||||
private readonly usersRepository: IUsersRepository, |
||||
private readonly mailerService: Mailer.IMailerService, |
||||
private readonly ipsRepository: IIPsRepository, |
||||
) {} |
||||
|
||||
public setIp(blockedIp: string) { |
||||
const decoded = attempsKeys.parseIp(blockedIp) |
||||
this.blockedIp = blockedIp |
||||
this.clearIp = decoded.ip |
||||
this.fingerprint = decoded.finreprint |
||||
return this |
||||
} |
||||
|
||||
public async sendAlerts() { |
||||
await this.preloadData() |
||||
await this.sendToAdmins() |
||||
await this.sendToTargetUser() |
||||
} |
||||
|
||||
private async preloadData() { |
||||
this.blocked = await this.ipsRepository.findOne({ |
||||
where: { ip: this.blockedIp }, |
||||
relations: ['user'], |
||||
}) |
||||
} |
||||
|
||||
private async sendToAdmins() { |
||||
const admins = await this.usersRepository.find({ where: { role: Users.Role.Admin } }) |
||||
const message = await this.generateTextForAdminAlert() |
||||
admins.map(it => { |
||||
this.mailerService.send({ |
||||
subject: 'Ip було заблоковано', |
||||
text: message, |
||||
to: it.email, |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
private generateTextForAdminAlert() { |
||||
let text = ` |
||||
Системне повідомлення про блокування IP \n |
||||
Ip: ${this.clearIp} \n |
||||
` |
||||
|
||||
if (this.fingerprint) { |
||||
text = `${text} Відбиток: ${this.fingerprint} \n` |
||||
} |
||||
|
||||
if (this.targetUser) { |
||||
text = `${text} Аккаунт користувача в якого намагались зайти: ${this.targetUser.email}, ${this.targetUser.phoneNumber}` |
||||
} |
||||
return text |
||||
} |
||||
|
||||
private sendToTargetUser() { |
||||
if (!this.targetUser) return null |
||||
|
||||
this.mailerService.send({ |
||||
subject: 'Ip було заблоковано', |
||||
text: this.generateTextForTargetUserAlert(), |
||||
to: this.targetUser.email, |
||||
}) |
||||
} |
||||
|
||||
private generateTextForTargetUserAlert() { |
||||
const text = ` |
||||
Системне повідомлення про блокування IP \n |
||||
До вашого аккаунта намагались увійти. \n
|
||||
Ip з якого відбувалась спроба: ${this.clearIp} \n |
||||
Ip було заблоковане. Якщо це ваше IP то вам потрібно звернутись до адміністратора додатку. |
||||
` |
||||
return text |
||||
} |
||||
} |
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
export * from './blocked-ip-alert' |
@ -1 +1,3 @@
@@ -1 +1,3 @@
|
||||
export const SESSIONS_REPOSITORY = Symbol('SESSIONS_REPOSITORY') |
||||
|
||||
export const SESSIONS_MODULE_OPTIONS = Symbol('SESSIONS_MODULE_OPTIONS') |
||||
|
@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
|
||||
export const attempsKeys = { |
||||
createByFingerprint: (ip: string, fingerprint: string) => { |
||||
return `attempt-${ip}:-:${fingerprint}` |
||||
}, |
||||
createByFingreprintIp: (ip: string, fingerprint: string) => { |
||||
return `${ip}///${fingerprint}` |
||||
}, |
||||
parseIp: (ip: string) => { |
||||
const paths = ip.split('///') |
||||
return { |
||||
ip: paths[0], |
||||
finreprint: paths[1] ? atob(paths[1]) : null, |
||||
} |
||||
}, |
||||
} |
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
export * from './attemps-keys.helper' |
@ -1 +1,2 @@
@@ -1 +1,2 @@
|
||||
export * from './sessions-module-options.interface' |
||||
export * from './sessions-repository.inteface' |
||||
|
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
export interface ISessionsModuleOptions { |
||||
maxAttempsByIp: number |
||||
maxAttempsByFingerprint: number |
||||
maxAttempsByTime: number |
||||
maxGapForTimeAttemps: number |
||||
} |
@ -0,0 +1,79 @@
@@ -0,0 +1,79 @@
|
||||
import { Inject, Injectable } from '@nestjs/common' |
||||
import { defaultTo } from 'lodash' |
||||
import { IPs, Sessions } from 'src/core' |
||||
import { RedisService } from 'src/libs' |
||||
import { attempsKeys } from '../helpers' |
||||
import { IPS_SERVICE } from 'src/core/consts' |
||||
import { EventEmitter2 } from '@nestjs/event-emitter' |
||||
import { Events } from 'src/core/enums' |
||||
import { SESSIONS_MODULE_OPTIONS } from '../consts' |
||||
import { ISessionsModuleOptions } from '../interfaces' |
||||
|
||||
@Injectable() |
||||
export class AuthAttemtionsByFingerprintService { |
||||
private readonly maxAtempts: number = 10 |
||||
|
||||
constructor( |
||||
@Inject(IPS_SERVICE) |
||||
private readonly ipsService: IPs.IIPsService, |
||||
private readonly redisService: RedisService, |
||||
private readonly eventEmitter: EventEmitter2, |
||||
|
||||
@Inject(SESSIONS_MODULE_OPTIONS) |
||||
options: ISessionsModuleOptions, |
||||
) { |
||||
this.maxAtempts = options.maxAttempsByFingerprint |
||||
} |
||||
|
||||
public async add(payload: Sessions.AddAuthAttempPayload) { |
||||
const key = this.createStoreKey(payload.ip, payload.fingerprint) |
||||
const attempsCount = await this.getAttemps(key) |
||||
|
||||
await this.increase(key, attempsCount) |
||||
|
||||
const isNeedBlock = await this.checkNeedToBlock(key) |
||||
if (isNeedBlock) { |
||||
await this.block(payload) |
||||
return true |
||||
} |
||||
} |
||||
|
||||
public async clear(key: string) { |
||||
await this.redisService.del(key) |
||||
} |
||||
|
||||
public async clearByPayload(ip: string, fingerprint: string) { |
||||
await this.clear(attempsKeys.createByFingerprint(ip, fingerprint)) |
||||
} |
||||
|
||||
private async increase(key: string, existCount = 0) { |
||||
await this.redisService.set(key, Number(existCount) + 1) |
||||
} |
||||
|
||||
private createStoreKey(ip: string, fingerpint: string) { |
||||
return attempsKeys.createByFingerprint(ip, fingerpint) |
||||
} |
||||
|
||||
private async getAttemps(key: string) { |
||||
const res = await this.redisService.get(key) |
||||
return defaultTo(Number(res), 0) |
||||
} |
||||
|
||||
private async checkNeedToBlock(key: string) { |
||||
const attempsCount = await this.getAttemps(key) |
||||
return Number(attempsCount) > this.maxAtempts |
||||
} |
||||
|
||||
private async block(payload: Sessions.AddAuthAttempPayload) { |
||||
await this.ipsService.store({ |
||||
ip: attempsKeys.createByFingreprintIp(payload.ip, payload.fingerprint), |
||||
listType: IPs.IPListType.Black, |
||||
userId: payload.targetUserId, |
||||
}) |
||||
this.eventEmitter.emit(Events.OnTooManyAuthAttempts, { |
||||
ip: attempsKeys.createByFingreprintIp(payload.ip, payload.fingerprint), |
||||
userId: payload.targetUserId, |
||||
}) |
||||
await this.clearByPayload(payload.ip, payload.fingerprint) |
||||
} |
||||
} |
@ -0,0 +1,60 @@
@@ -0,0 +1,60 @@
|
||||
import { Inject, Injectable } from '@nestjs/common' |
||||
import { EventEmitter2 } from '@nestjs/event-emitter' |
||||
import { IPs } from 'src/core' |
||||
import { IPS_SERVICE } from 'src/core/consts' |
||||
import { Events } from 'src/core/enums' |
||||
import { RedisService } from 'src/libs' |
||||
import { ISessionsModuleOptions } from '../interfaces' |
||||
import { SESSIONS_MODULE_OPTIONS } from '../consts' |
||||
|
||||
@Injectable() |
||||
export class AuthAttemptionsByIpsService { |
||||
private readonly maxAtempts: number = 40 |
||||
|
||||
constructor( |
||||
@Inject(IPS_SERVICE) |
||||
private readonly ipsService: IPs.IIPsService, |
||||
|
||||
private readonly redisService: RedisService, |
||||
|
||||
private readonly eventEmitter: EventEmitter2, |
||||
|
||||
@Inject(SESSIONS_MODULE_OPTIONS) |
||||
options: ISessionsModuleOptions, |
||||
) { |
||||
this.maxAtempts = options.maxAttempsByIp |
||||
} |
||||
|
||||
public async add(ip: string, targetUserId?: number) { |
||||
const attempts = await this.increment(ip) |
||||
|
||||
if (attempts < this.maxAtempts) return |
||||
|
||||
await this.ipsService.store({ ip, listType: IPs.IPListType.Black, userId: targetUserId }) |
||||
this.eventEmitter.emit(Events.OnTooManyAuthAttempts, { ip }) |
||||
|
||||
return true |
||||
} |
||||
|
||||
private async increment(ip: string) { |
||||
const storeKey = this.createStoreKey(ip) |
||||
const res = await this.redisService.get(storeKey) |
||||
|
||||
if (!res) { |
||||
await this.redisService.set(storeKey, 1) |
||||
return 1 |
||||
} |
||||
|
||||
const attemps = Number(res) + 1 |
||||
|
||||
await this.redisService.set(storeKey, attemps) |
||||
return attemps |
||||
} |
||||
|
||||
public async dropCount(ip: string) { |
||||
await this.redisService.set(this.createStoreKey(ip), 0) |
||||
} |
||||
private createStoreKey(ip: string) { |
||||
return `attempt-${ip}` |
||||
} |
||||
} |
@ -0,0 +1,97 @@
@@ -0,0 +1,97 @@
|
||||
import { Inject, Injectable } from '@nestjs/common' |
||||
import { EventEmitter2 } from '@nestjs/event-emitter' |
||||
import { IPs, Sessions } from 'src/core' |
||||
import { IPS_SERVICE } from 'src/core/consts' |
||||
import { Events } from 'src/core/enums' |
||||
import { ISessionsModuleOptions } from '../interfaces' |
||||
import { SESSIONS_MODULE_OPTIONS } from '../consts' |
||||
|
||||
type UserId = number |
||||
type AttempData = { |
||||
timestamp: number |
||||
count: number |
||||
} |
||||
|
||||
@Injectable() |
||||
export class AuthAttemptionsByTimeService { |
||||
private timestampMaxGap = 1000 * 10 // 10 minutes
|
||||
private maxAttemps = 3 |
||||
private db: Record<UserId, AttempData> = {} |
||||
|
||||
constructor( |
||||
@Inject(IPS_SERVICE) |
||||
private readonly ipsService: IPs.IIPsService, |
||||
private readonly eventEmitter: EventEmitter2, |
||||
|
||||
@Inject(SESSIONS_MODULE_OPTIONS) |
||||
options: ISessionsModuleOptions, |
||||
) { |
||||
this.timestampMaxGap = options.maxGapForTimeAttemps |
||||
this.maxAttemps = options.maxAttempsByTime |
||||
} |
||||
|
||||
public async add(payload: Sessions.AddAuthAttempPayload) { |
||||
const exist = this.db[payload.targetUserId] |
||||
if (!exist) { |
||||
this.saveFirstAttemp(payload.targetUserId) |
||||
return |
||||
} |
||||
|
||||
if (this.isAttempExpired(exist)) { |
||||
this.resetAttemps(payload.targetUserId) |
||||
return |
||||
} |
||||
|
||||
this.increaseCount(payload.targetUserId, Number(exist.count) + 1) |
||||
|
||||
if (this.isRechMaxAttemps(payload.targetUserId)) { |
||||
await this.ipsService.store({ |
||||
ip: payload.ip, |
||||
listType: IPs.IPListType.Black, |
||||
userId: payload.targetUserId, |
||||
}) |
||||
this.eventEmitter.emit(Events.OnTooManyAuthAttempts, { ip: payload.ip }) |
||||
return true |
||||
} |
||||
|
||||
return false |
||||
} |
||||
|
||||
private saveFirstAttemp(userId: number) { |
||||
this.resetAttemps(userId) |
||||
} |
||||
|
||||
private resetAttemps(userId: number) { |
||||
this.db[userId] = { |
||||
timestamp: new Date().getTime(), |
||||
count: 1, |
||||
} |
||||
} |
||||
|
||||
private isAttempExpired(attemp: AttempData) { |
||||
const now = new Date().getTime() |
||||
const gap = Number(now) - Number(attemp.timestamp) |
||||
return gap > this.timestampMaxGap |
||||
} |
||||
|
||||
private increaseCount(userId: number, newCount: number) { |
||||
this.db[userId] = { |
||||
timestamp: new Date().getTime(), |
||||
count: newCount, |
||||
} |
||||
} |
||||
|
||||
private isRechMaxAttemps(userId: number) { |
||||
const data = this.db[userId] |
||||
if (!data) return false |
||||
|
||||
if (data.count >= this.maxAttemps) return true |
||||
} |
||||
|
||||
public isUserBlocked(userId: number) { |
||||
const exist = this.db[userId] |
||||
if (exist) return false |
||||
if (this.isAttempExpired(exist)) return false |
||||
return this.isRechMaxAttemps(userId) |
||||
} |
||||
} |
@ -1,51 +1,86 @@
@@ -1,51 +1,86 @@
|
||||
import { Inject, Injectable } from '@nestjs/common' |
||||
import { EventEmitter2 } from '@nestjs/event-emitter' |
||||
import { IPs } from 'src/core' |
||||
import { IPS_SERVICE } from 'src/core/consts' |
||||
import { Events } from 'src/core/enums' |
||||
import { RedisService } from 'src/libs' |
||||
import { IPs, Mailer, Sessions } from 'src/core' |
||||
import { IPS_SERVICE, MAILER_SERVICE } from 'src/core/consts' |
||||
import { AuthAttemptionsByIpsService } from './auth-attemptions-by-ips.service' |
||||
import { AuthAttemtionsByFingerprintService } from './auth-attemptions-by-fingeprint.service' |
||||
import { USERS_REPOSITORY } from 'src/domain/users/consts' |
||||
import { IUsersRepository } from 'src/domain/users/interfaces' |
||||
import { attempsKeys } from '../helpers' |
||||
import { IPS_REPOSITORY } from 'src/domain/ips/consts' |
||||
import { IIPsRepository } from 'src/domain/ips/interfaces' |
||||
import { BlockedIpAlert } from '../classes' |
||||
import { AuthAttemptionsByTimeService } from './auth-attemptions-by-time.service' |
||||
|
||||
@Injectable() |
||||
export class AuthAttemptionsService { |
||||
private readonly maxAtempts = 15 |
||||
|
||||
export class AuthAttemptionsService implements Sessions.IAuthAttemptionsService { |
||||
constructor( |
||||
private readonly redisService: RedisService, |
||||
@Inject(IPS_SERVICE) private readonly ipsService: IPs.IIPsService, |
||||
private eventEmitter: EventEmitter2, |
||||
) {} |
||||
@Inject(IPS_SERVICE) |
||||
private readonly ipsService: IPs.IIPsService, |
||||
|
||||
public async add(ip: string, data: Record<string, any> = {}) { |
||||
const listType = await this.ipsService.getIpListType(ip) |
||||
if (listType === IPs.IPListType.White) return |
||||
@Inject(USERS_REPOSITORY) |
||||
private readonly usersRepository: IUsersRepository, |
||||
|
||||
const attempts = await this.increment(ip) |
||||
@Inject(MAILER_SERVICE) |
||||
private readonly mailerService: Mailer.IMailerService, |
||||
|
||||
if (attempts < this.maxAtempts) return |
||||
@Inject(IPS_REPOSITORY) |
||||
private readonly ipsRepository: IIPsRepository, |
||||
|
||||
await this.ipsService.store({ ip, listType: IPs.IPListType.Black }) |
||||
this.eventEmitter.emit(Events.OnTooManyAuthAttempts, { ip, ...data }) |
||||
} |
||||
private readonly authAttemptionsByIpsService: AuthAttemptionsByIpsService, |
||||
private readonly authAttemtionsByFingerprintService: AuthAttemtionsByFingerprintService, |
||||
private readonly authAttemptionsByTimeService: AuthAttemptionsByTimeService, |
||||
) {} |
||||
|
||||
public async add(payload: Sessions.AddAuthAttempPayload) { |
||||
const listType = await this.ipsService.getIpListType(payload.ip) |
||||
if (listType === IPs.IPListType.White) return |
||||
|
||||
private async increment(ip: string) { |
||||
const storeKey = this.createStoreKey(ip) |
||||
const res = await this.redisService.get(storeKey) |
||||
let blockedIp = null |
||||
|
||||
if (!res) { |
||||
await this.redisService.set(storeKey, 1) |
||||
return 1 |
||||
if (payload.targetUserId) { |
||||
const wasBlockedByTime = await this.authAttemptionsByTimeService.add(payload) |
||||
if (wasBlockedByTime) blockedIp = payload.ip |
||||
} |
||||
|
||||
const attemps = Number(res) + 1 |
||||
if (!blockedIp) { |
||||
const wasBlocked = await this.authAttemptionsByIpsService.add( |
||||
payload.ip, |
||||
payload.targetUserId, |
||||
) |
||||
if (wasBlocked) { |
||||
blockedIp = payload.ip |
||||
} |
||||
} |
||||
|
||||
if (!blockedIp && payload.fingerprint) { |
||||
const wasBlockedByFingreprirnt = await this.authAttemtionsByFingerprintService.add( |
||||
payload, |
||||
) |
||||
if (wasBlockedByFingreprirnt) { |
||||
blockedIp = attempsKeys.createByFingreprintIp(payload.ip, payload.fingerprint) |
||||
} |
||||
} |
||||
|
||||
await this.redisService.set(storeKey, attemps) |
||||
return attemps |
||||
if (blockedIp) { |
||||
new BlockedIpAlert(this.usersRepository, this.mailerService, this.ipsRepository) |
||||
.setIp(blockedIp) |
||||
.sendAlerts() |
||||
} |
||||
} |
||||
|
||||
public async dropCount(ip: string) { |
||||
await this.redisService.set(this.createStoreKey(ip), 0) |
||||
await this.authAttemptionsByIpsService.dropCount(ip) |
||||
await this.ipsService.removeIps([ip]) |
||||
} |
||||
private createStoreKey(ip: string) { |
||||
return `attempt-${ip}` |
||||
|
||||
public async checkAuthAttempIsAviable(targetUserId: number, ip: string) { |
||||
const isAble = this.authAttemptionsByTimeService.isUserBlocked(targetUserId) |
||||
if (isAble) return true |
||||
|
||||
await this.authAttemptionsByTimeService.add({ |
||||
targetUserId, |
||||
ip, |
||||
}) |
||||
return false |
||||
} |
||||
} |
||||
|
@ -1,7 +1,16 @@
@@ -1,7 +1,16 @@
|
||||
import { SessionsService } from './sessions.service' |
||||
import { AuthAttemptionsService } from './auth-attemptions.service' |
||||
import { ConfirmationCodesService } from './confirmation-codes.service' |
||||
import { AuthAttemptionsByIpsService } from './auth-attemptions-by-ips.service' |
||||
import { AuthAttemtionsByFingerprintService } from './auth-attemptions-by-fingeprint.service' |
||||
import { AuthAttemptionsByTimeService } from './auth-attemptions-by-time.service' |
||||
|
||||
export const SESSIONS_SERVICES = [AuthAttemptionsService, ConfirmationCodesService] |
||||
export const SESSIONS_SERVICES = [ |
||||
AuthAttemptionsService, |
||||
ConfirmationCodesService, |
||||
AuthAttemptionsByIpsService, |
||||
AuthAttemtionsByFingerprintService, |
||||
AuthAttemptionsByTimeService, |
||||
] |
||||
|
||||
export { SessionsService, ConfirmationCodesService } |
||||
export { SessionsService, ConfirmationCodesService, AuthAttemptionsByIpsService } |
||||
|
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
export * from './tasks-dedlines.cron' |
@ -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, |
||||
}) |
||||
} |
||||
} |
||||
} |
@ -1 +1,2 @@
@@ -1 +1,2 @@
|
||||
export * from './user-creator' |
||||
export * from './user-full-name' |
||||
|
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
import { Users } from 'src/core' |
||||
|
||||
export class UserFullName { |
||||
static getFromUser(user: Users.UserModel) { |
||||
if (!user?.info) return 'Uknow User' |
||||
|
||||
return `${user.info.firstName} ${user.info.lastName}` |
||||
} |
||||
} |
@ -1,18 +1,39 @@
@@ -1,18 +1,39 @@
|
||||
import { DynamicModule, Module } from '@nestjs/common' |
||||
|
||||
import { NotificationsModule } from 'src/domain' |
||||
import { MailerModule, NotificationsModule, UsersModule } from 'src/domain' |
||||
import { JwtModule } from 'src/libs' |
||||
import { AdminNotificationsController, AdminNotificationsSettingsController } from './controllers' |
||||
import { AdminNotificationsService, AdminNotificationsSettingsService } from './services' |
||||
import { |
||||
AdminNotificationsController, |
||||
AdminNotificationsSendController, |
||||
AdminNotificationsSettingsController, |
||||
} from './controllers' |
||||
import { |
||||
AdminNotificationsSendService, |
||||
AdminNotificationsService, |
||||
AdminNotificationsSettingsService, |
||||
} from './services' |
||||
|
||||
@Module({}) |
||||
export class AdminNotificationsModule { |
||||
static forRoot(): DynamicModule { |
||||
return { |
||||
module: AdminNotificationsModule, |
||||
imports: [NotificationsModule.forFeature(), JwtModule.forFeature()], |
||||
providers: [AdminNotificationsService, AdminNotificationsSettingsService], |
||||
controllers: [AdminNotificationsController, AdminNotificationsSettingsController], |
||||
imports: [ |
||||
NotificationsModule.forFeature(), |
||||
JwtModule.forFeature(), |
||||
UsersModule.forFeature(), |
||||
MailerModule.forFeature(), |
||||
], |
||||
providers: [ |
||||
AdminNotificationsService, |
||||
AdminNotificationsSettingsService, |
||||
AdminNotificationsSendService, |
||||
], |
||||
controllers: [ |
||||
AdminNotificationsController, |
||||
AdminNotificationsSettingsController, |
||||
AdminNotificationsSendController, |
||||
], |
||||
} |
||||
} |
||||
} |
||||
|
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
import { Body, Controller, Post } from '@nestjs/common' |
||||
import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger' |
||||
import { AdminNotificationsSendService } from '../services' |
||||
import { SendNotificationsPayloadDto, SendNotificationsResultDto } from '../dto' |
||||
import { RoleGuard } from 'src/domain/sessions/decorators' |
||||
import { Users } from 'src/core' |
||||
import { ReqUser } from 'src/shared' |
||||
|
||||
@ApiTags('Admin | Notifications') |
||||
@Controller('admin/notifications-send') |
||||
export class AdminNotificationsSendController { |
||||
constructor(private readonly adminNotificationsSendService: AdminNotificationsSendService) {} |
||||
|
||||
@ApiOperation({ |
||||
summary: 'Відправка нотифікацій групі користувачів', |
||||
}) |
||||
@ApiOkResponse({ |
||||
description: |
||||
'Повертає імена користувачів яким були успішно відправлені нотифікацій або не успішно', |
||||
type: SendNotificationsResultDto, |
||||
}) |
||||
@RoleGuard(Users.Role.Admin) |
||||
@Post() |
||||
send(@ReqUser() userId: number, @Body() dto: SendNotificationsPayloadDto) { |
||||
return this.adminNotificationsSendService.send(userId, dto) |
||||
} |
||||
} |
@ -1,2 +1,3 @@
@@ -1,2 +1,3 @@
|
||||
export * from './admin-notifications.controller' |
||||
export * from './admin-notifications-send.controller' |
||||
export * from './admin-notifications-settings.controller' |
||||
export * from './admin-notifications.controller' |
||||
|
@ -1,3 +1,4 @@
@@ -1,3 +1,4 @@
|
||||
export * from './save-admin-notifications-settings.dto' |
||||
export * from './add-user-device-payload.dto' |
||||
export * from './read-notifications-by-ids.dto' |
||||
export * from './save-admin-notifications-settings.dto' |
||||
export * from './send-notifications.dto' |
||||
|
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
import { DtoProperty, DtoPropertyOptional } from 'src/shared' |
||||
|
||||
export class SendNotificationsPayloadDto { |
||||
@DtoProperty() |
||||
usersIds: number[] |
||||
|
||||
@DtoProperty() |
||||
title: string |
||||
|
||||
@DtoProperty() |
||||
content: string |
||||
|
||||
@DtoPropertyOptional() |
||||
haveToSendEmail?: boolean |
||||
} |
||||
|
||||
export class SendNotificationsResultDto { |
||||
@DtoProperty() |
||||
successSentUsersNames: string[] |
||||
|
||||
@DtoProperty() |
||||
failedSentUsersNames: string[] |
||||
} |
@ -0,0 +1,72 @@
@@ -0,0 +1,72 @@
|
||||
import { Inject, Injectable } from '@nestjs/common' |
||||
import { Mailer, Notifications, Users } from 'src/core' |
||||
import { MAILER_SERVICE, NOTIFICATIONS_SERVICE, USERS_SERVICE } from 'src/core/consts' |
||||
import { SendNotificationsPayloadDto, SendNotificationsResultDto } from '../dto' |
||||
import { UserFullName } from 'src/domain/users/classes' |
||||
|
||||
@Injectable() |
||||
export class AdminNotificationsSendService { |
||||
constructor( |
||||
@Inject(NOTIFICATIONS_SERVICE) |
||||
private readonly notificationsService: Notifications.INotificationsService, |
||||
|
||||
@Inject(USERS_SERVICE) |
||||
private readonly usersService: Users.IUsersService, |
||||
|
||||
@Inject(MAILER_SERVICE) |
||||
private readonly mailerService: Mailer.IMailerService, |
||||
) {} |
||||
|
||||
public async send(userId: number, dto: SendNotificationsPayloadDto) { |
||||
const result: SendNotificationsResultDto = { |
||||
successSentUsersNames: [], |
||||
failedSentUsersNames: [], |
||||
} |
||||
|
||||
for await (const targetUserId of dto.usersIds) { |
||||
let user: Users.UserModel = null |
||||
|
||||
console.log('dto', dto) |
||||
|
||||
try { |
||||
user = await this.usersService.getOne(targetUserId, ['info']) |
||||
await this.sendNotificationToOne(targetUserId, dto, userId) |
||||
|
||||
if (dto.haveToSendEmail) await this.sendEmailToOne(user.email, dto) |
||||
|
||||
result.successSentUsersNames.push(UserFullName.getFromUser(user)) |
||||
} catch (e) { |
||||
console.log('error', e) |
||||
result.failedSentUsersNames.push(UserFullName.getFromUser(user)) |
||||
} |
||||
} |
||||
} |
||||
|
||||
private async sendNotificationToOne( |
||||
userId: number, |
||||
payload: Omit<SendNotificationsPayloadDto, 'usersIds'>, |
||||
authorId: number, |
||||
) { |
||||
await this.notificationsService.addNotification({ |
||||
userId, |
||||
title: payload.title, |
||||
content: payload.content, |
||||
group: Notifications.NotificationsGroup.Other, |
||||
data: { |
||||
sentByUserId: authorId, |
||||
}, |
||||
}) |
||||
} |
||||
|
||||
private async sendEmailToOne( |
||||
email: string, |
||||
payload: Omit<SendNotificationsPayloadDto, 'usersIds'>, |
||||
) { |
||||
console.log('send email', email) |
||||
await this.mailerService.send({ |
||||
to: email, |
||||
subject: payload.title, |
||||
text: payload.content, |
||||
}) |
||||
} |
||||
} |
@ -1,2 +1,3 @@
@@ -1,2 +1,3 @@
|
||||
export * from './admin-notifications.service' |
||||
export * from './admin-notifications-send.service' |
||||
export * from './admin-notifications-settings.service' |
||||
export * from './admin-notifications.service' |
||||
|
@ -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 +1,2 @@
@@ -1 +1,2 @@
|
||||
export * from './cron.abstract' |
||||
export * from './seeder.abstract' |
||||
|
@ -1,12 +1,13 @@
@@ -1,12 +1,13 @@
|
||||
export * from './api-description.decorator' |
||||
export * from './api-implict-decorator' |
||||
export * from './dto-property.decorator' |
||||
export * from './req-user.decorator' |
||||
export * from './req-user-role.decorator' |
||||
export * from './permissions-tabs-guard.decorator' |
||||
export * from './req-fingreprint.decorator' |
||||
export * from './req-pagination.decorator' |
||||
export * from './api-implict-decorator' |
||||
export * from './transform-string-to-boolean.decorator' |
||||
export * from './transform-string-to-number.decorator' |
||||
export * from './req-session-id.decorator' |
||||
export * from './req-user-role.decorator' |
||||
export * from './req-user.decorator' |
||||
export * from './transform-phone-number.decorator' |
||||
export * from './transform-remove-null-fields.decorator' |
||||
export * from './permissions-tabs-guard.decorator' |
||||
export * from './req-session-id.decorator' |
||||
export * from './api-description.decorator' |
||||
export * from './transform-string-to-boolean.decorator' |
||||
export * from './transform-string-to-number.decorator' |
||||
|
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common' |
||||
|
||||
export const ReqFingreprint = createParamDecorator((data: unknown, ctx: ExecutionContext) => { |
||||
const request = ctx.switchToHttp().getRequest() |
||||
|
||||
return request.fingerprint || null |
||||
}) |
@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
|
||||
{ |
||||
"extends": "./tsconfig.json", |
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"] |
||||
"extends": "./tsconfig.json", |
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts", "documentation", "dist"] |
||||
} |
||||
|
Loading…
Reference in new issue