mirror of
https://github.com/n08i40k/schedule-parser-next.git
synced 2025-12-06 17:57:45 +03:00
1.3.0
Добавлена совместимость с Firebase Cloud Messaging. Сервис и контроллер модуля schedule-replacer были перенесены в модуль schedule.
This commit is contained in:
@@ -3,7 +3,7 @@ import { AuthModule } from "./auth/auth.module";
|
||||
import { UsersModule } from "./users/users.module";
|
||||
import { ScheduleModule } from "./schedule/schedule.module";
|
||||
import { CacheModule } from "@nestjs/cache-manager";
|
||||
import { ScheduleReplacerModule } from "./schedule-replacer/schedule-replacer.module";
|
||||
import { FirebaseAdminModule } from "./firebase-admin/firebase-admin.module";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -11,7 +11,7 @@ import { ScheduleReplacerModule } from "./schedule-replacer/schedule-replacer.mo
|
||||
UsersModule,
|
||||
ScheduleModule,
|
||||
CacheModule.register({ ttl: 5 * 60 * 1000, isGlobal: true }),
|
||||
ScheduleReplacerModule,
|
||||
FirebaseAdminModule,
|
||||
],
|
||||
controllers: [],
|
||||
providers: [],
|
||||
|
||||
@@ -34,8 +34,8 @@ import { UserToken } from "./auth.decorator";
|
||||
@Controller("api/v1/auth")
|
||||
export class AuthController {
|
||||
constructor(
|
||||
private readonly scheduleService: ScheduleService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly scheduleService: ScheduleService,
|
||||
) {}
|
||||
|
||||
@ApiExtraModels(SignInReqDto)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { forwardRef, Module } from "@nestjs/common";
|
||||
import { JwtModule } from "@nestjs/jwt";
|
||||
import { jwtConstants } from "../contants";
|
||||
import { AuthService } from "./auth.service";
|
||||
@@ -9,8 +9,8 @@ import { ScheduleModule } from "../schedule/schedule.module";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
UsersModule,
|
||||
ScheduleModule,
|
||||
forwardRef(() => UsersModule),
|
||||
forwardRef(() => ScheduleModule),
|
||||
JwtModule.register({
|
||||
global: true,
|
||||
secret: jwtConstants.secret,
|
||||
|
||||
@@ -14,3 +14,7 @@ export const httpsConstants = {
|
||||
export const apiConstants = {
|
||||
port: process.env.API_PORT ?? 5050,
|
||||
};
|
||||
|
||||
export const firebaseConstants = {
|
||||
serviceAccountPath: process.env.FIREBASE_ACCOUNT_PATH!,
|
||||
};
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { ApiProperty, OmitType, PickType } from "@nestjs/swagger";
|
||||
import {
|
||||
IsArray,
|
||||
IsEnum,
|
||||
IsJWT,
|
||||
IsMongoId,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsString,
|
||||
MaxLength,
|
||||
MinLength,
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
import { Expose, plainToClass } from "class-transformer";
|
||||
import { Expose, plainToClass, Type } from "class-transformer";
|
||||
|
||||
export enum UserRoleDto {
|
||||
STUDENT = "STUDENT",
|
||||
@@ -15,6 +19,25 @@ export enum UserRoleDto {
|
||||
ADMIN = "ADMIN",
|
||||
}
|
||||
|
||||
export class UserFcmDto {
|
||||
@ApiProperty({
|
||||
description: "Токен Firebase Cloud Messaging",
|
||||
})
|
||||
@IsString()
|
||||
@Expose()
|
||||
token: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: ["schedule-update"],
|
||||
description: "Топики на которые подписан пользователь",
|
||||
})
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@IsString()
|
||||
@Expose()
|
||||
topics: Array<string>;
|
||||
}
|
||||
|
||||
export class UserDto {
|
||||
@ApiProperty({
|
||||
example: "66e1b7e255c5d5f1268cce90",
|
||||
@@ -67,12 +90,20 @@ export class UserDto {
|
||||
@IsEnum(UserRoleDto)
|
||||
@Expose()
|
||||
role: UserRoleDto;
|
||||
|
||||
@ApiProperty({ description: "Данные Firebase Cloud Messaging" })
|
||||
@IsObject()
|
||||
@Type(() => UserFcmDto)
|
||||
@IsOptional()
|
||||
@Expose()
|
||||
fcm: UserFcmDto | null;
|
||||
}
|
||||
|
||||
export class ClientUserResDto extends OmitType(UserDto, [
|
||||
"password",
|
||||
"salt",
|
||||
"accessToken",
|
||||
"fcm",
|
||||
]) {
|
||||
static fromUserDto(userDto: UserDto): ClientUserResDto {
|
||||
return plainToClass(ClientUserResDto, userDto, {
|
||||
|
||||
20
src/firebase-admin/firebase-admin.controller.spec.ts
Normal file
20
src/firebase-admin/firebase-admin.controller.spec.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { FirebaseAdminController } from "./firebase-admin.controller";
|
||||
|
||||
describe("FirebaseAdminController", () => {
|
||||
let controller: FirebaseAdminController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [FirebaseAdminController],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<FirebaseAdminController>(
|
||||
FirebaseAdminController,
|
||||
);
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
});
|
||||
41
src/firebase-admin/firebase-admin.controller.ts
Normal file
41
src/firebase-admin/firebase-admin.controller.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
Controller,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Param,
|
||||
Post,
|
||||
UseGuards,
|
||||
} from "@nestjs/common";
|
||||
import { AuthGuard } from "../auth/auth.guard";
|
||||
import { UserToken } from "../auth/auth.decorator";
|
||||
import { UserFromTokenPipe } from "../auth/auth.pipe";
|
||||
import { UserDto } from "../dto/user.dto";
|
||||
import { ResultDto } from "../utility/validation/class-validator.interceptor";
|
||||
import { FirebaseAdminService } from "./firebase-admin.service";
|
||||
|
||||
@Controller("api/v1/fcm")
|
||||
@UseGuards(AuthGuard)
|
||||
export class FirebaseAdminController {
|
||||
private readonly defaultTopics = new Set(["schedule-update"]);
|
||||
|
||||
constructor(private readonly firebaseAdminService: FirebaseAdminService) {}
|
||||
|
||||
@Post("set-token/:token")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ResultDto(null)
|
||||
async setToken(
|
||||
@Param("token") token: string,
|
||||
@UserToken(UserFromTokenPipe) user: UserDto,
|
||||
): Promise<void> {
|
||||
if (user.fcm?.token === token) return;
|
||||
|
||||
const updatedUser = (
|
||||
await this.firebaseAdminService.updateToken(user, token)
|
||||
).userDto;
|
||||
|
||||
await this.firebaseAdminService.subscribe(
|
||||
updatedUser,
|
||||
this.defaultTopics,
|
||||
);
|
||||
}
|
||||
}
|
||||
12
src/firebase-admin/firebase-admin.module.ts
Normal file
12
src/firebase-admin/firebase-admin.module.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { forwardRef, Module } from "@nestjs/common";
|
||||
import { FirebaseAdminService } from "./firebase-admin.service";
|
||||
import { UsersModule } from "../users/users.module";
|
||||
import { FirebaseAdminController } from "./firebase-admin.controller";
|
||||
|
||||
@Module({
|
||||
imports: [forwardRef(() => UsersModule)],
|
||||
providers: [FirebaseAdminService],
|
||||
exports: [FirebaseAdminService],
|
||||
controllers: [FirebaseAdminController],
|
||||
})
|
||||
export class FirebaseAdminModule {}
|
||||
18
src/firebase-admin/firebase-admin.service.spec.ts
Normal file
18
src/firebase-admin/firebase-admin.service.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { FirebaseAdminService } from "./firebase-admin.service";
|
||||
|
||||
describe("FirebaseAdminService", () => {
|
||||
let service: FirebaseAdminService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [FirebaseAdminService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<FirebaseAdminService>(FirebaseAdminService);
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
99
src/firebase-admin/firebase-admin.service.ts
Normal file
99
src/firebase-admin/firebase-admin.service.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { forwardRef, Inject, Injectable, OnModuleInit } from "@nestjs/common";
|
||||
|
||||
import { initializeApp, App } from "firebase-admin/app";
|
||||
import { credential } from "firebase-admin";
|
||||
import {
|
||||
BaseMessage,
|
||||
getMessaging,
|
||||
Messaging,
|
||||
TopicMessage,
|
||||
} from "firebase-admin/messaging";
|
||||
|
||||
import { firebaseConstants } from "../contants";
|
||||
import { UsersService } from "../users/users.service";
|
||||
import { UserDto } from "../dto/user.dto";
|
||||
|
||||
@Injectable()
|
||||
export class FirebaseAdminService implements OnModuleInit {
|
||||
constructor(
|
||||
@Inject(forwardRef(() => UsersService))
|
||||
private readonly usersService: UsersService,
|
||||
) {}
|
||||
|
||||
private app: App;
|
||||
private messaging: Messaging;
|
||||
|
||||
onModuleInit() {
|
||||
this.app = initializeApp({
|
||||
credential: credential.cert(firebaseConstants.serviceAccountPath),
|
||||
});
|
||||
this.messaging = getMessaging(this.app);
|
||||
}
|
||||
|
||||
async sendByTopic(topic: string, message: BaseMessage): Promise<void> {
|
||||
const topicMessage = message as TopicMessage;
|
||||
topicMessage.topic = topic;
|
||||
|
||||
await this.messaging.send(topicMessage);
|
||||
}
|
||||
|
||||
async updateToken(
|
||||
user: UserDto,
|
||||
token: string,
|
||||
): Promise<{ userDto: UserDto; isNew: boolean }> {
|
||||
const isNew = user.fcm === null;
|
||||
|
||||
const fcm = !isNew ? user.fcm : { token: token, topics: [] };
|
||||
if (!isNew) {
|
||||
if (fcm.token === token) return { userDto: user, isNew: false };
|
||||
|
||||
for (const topic in fcm.topics)
|
||||
await this.messaging.subscribeToTopic(token, topic);
|
||||
fcm.token = token;
|
||||
}
|
||||
|
||||
return {
|
||||
userDto: await this.usersService.update({
|
||||
where: { id: user.id },
|
||||
data: { fcm: fcm },
|
||||
}),
|
||||
isNew: isNew,
|
||||
};
|
||||
}
|
||||
|
||||
async unsubscribe(user: UserDto, topics: Set<string>): Promise<UserDto> {
|
||||
const fcm = user.fcm;
|
||||
const currentTopics = new Set(fcm.topics);
|
||||
|
||||
for (const topic of topics) {
|
||||
await this.messaging.unsubscribeFromTopic(fcm.token, topic);
|
||||
currentTopics.delete(topic);
|
||||
}
|
||||
if (currentTopics.size === fcm.topics.length) return user;
|
||||
|
||||
fcm.topics = Array.from(currentTopics);
|
||||
|
||||
return await this.usersService.update({
|
||||
where: { id: user.id },
|
||||
data: { fcm: fcm },
|
||||
});
|
||||
}
|
||||
|
||||
async subscribe(user: UserDto, topics: Set<string>): Promise<UserDto> {
|
||||
const fcm = user.fcm;
|
||||
const currentTopics = new Set(fcm.topics);
|
||||
|
||||
for (const topic of topics) {
|
||||
await this.messaging.subscribeToTopic(fcm.token, topic);
|
||||
currentTopics.add(topic);
|
||||
}
|
||||
if (currentTopics.size === fcm.topics.length) return user;
|
||||
|
||||
fcm.topics = Array.from(currentTopics);
|
||||
|
||||
return await this.usersService.update({
|
||||
where: { id: user.id },
|
||||
data: { fcm: fcm },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ScheduleReplacerService } from "./schedule-replacer.service";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
import { ScheduleReplacerController } from "./schedule-replacer.controller";
|
||||
import { ScheduleModule } from "../schedule/schedule.module";
|
||||
import { AuthService } from "../auth/auth.service";
|
||||
import { UsersModule } from "../users/users.module";
|
||||
|
||||
@Module({
|
||||
imports: [ScheduleModule, UsersModule],
|
||||
providers: [AuthService, PrismaService, ScheduleReplacerService],
|
||||
exports: [ScheduleReplacerService],
|
||||
controllers: [ScheduleReplacerController],
|
||||
})
|
||||
export class ScheduleReplacerModule {}
|
||||
@@ -149,14 +149,8 @@ export class ScheduleParser {
|
||||
!downloadData.new &&
|
||||
this.lastResult &&
|
||||
this.xlsDownloader.getCacheMode() !== XlsDownloaderCacheMode.NONE
|
||||
) {
|
||||
console.debug(
|
||||
"Так как скачанный XLS не новый, присутствует уже готовый результат и кеширование не отключено...",
|
||||
);
|
||||
console.debug("будет возвращён предыдущий результат.");
|
||||
|
||||
)
|
||||
return this.lastResult;
|
||||
}
|
||||
|
||||
const workBook = XLSX.read(downloadData.fileData);
|
||||
const workSheet = workBook.Sheets[workBook.SheetNames[0]];
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
NotAcceptableException,
|
||||
ServiceUnavailableException,
|
||||
} from "@nestjs/common";
|
||||
import { ScheduleReplacerService } from "../../../schedule-replacer/schedule-replacer.service";
|
||||
import { ScheduleReplacerService } from "../../schedule-replacer.service";
|
||||
import { Error } from "mongoose";
|
||||
import * as crypto from "crypto";
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import { AuthRoles } from "../auth-role/auth-role.decorator";
|
||||
import { UserRoleDto } from "../dto/user.dto";
|
||||
import { ScheduleReplacerService } from "./schedule-replacer.service";
|
||||
import { ScheduleService } from "../schedule/schedule.service";
|
||||
import { ScheduleService } from "./schedule.service";
|
||||
import { FileInterceptor } from "@nestjs/platform-express";
|
||||
import {
|
||||
ApiExtraModels,
|
||||
@@ -7,6 +7,14 @@ import { plainToClass } from "class-transformer";
|
||||
export class ScheduleReplacerService {
|
||||
constructor(private readonly prismaService: PrismaService) {}
|
||||
|
||||
async hasByEtag(etag: string): Promise<boolean> {
|
||||
return (
|
||||
(await this.prismaService.scheduleReplace.count({
|
||||
where: { etag: etag },
|
||||
})) > 0
|
||||
);
|
||||
}
|
||||
|
||||
async getByEtag(etag: string): Promise<ScheduleReplacerDto | null> {
|
||||
const response = await this.prismaService.scheduleReplace.findUnique({
|
||||
where: { etag: etag },
|
||||
@@ -1,18 +1,16 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { forwardRef, Module } from "@nestjs/common";
|
||||
import { ScheduleService } from "./schedule.service";
|
||||
import { ScheduleController } from "./schedule.controller";
|
||||
import { UsersService } from "../users/users.service";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
import { ScheduleReplacerService } from "../schedule-replacer/schedule-replacer.service";
|
||||
import { FirebaseAdminModule } from "../firebase-admin/firebase-admin.module";
|
||||
import { UsersModule } from "src/users/users.module";
|
||||
import { ScheduleReplacerService } from "./schedule-replacer.service";
|
||||
import { ScheduleReplacerController } from "./schedule-replacer.controller";
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
ScheduleService,
|
||||
ScheduleReplacerService,
|
||||
UsersService,
|
||||
PrismaService,
|
||||
],
|
||||
controllers: [ScheduleController],
|
||||
imports: [forwardRef(() => UsersModule), FirebaseAdminModule],
|
||||
providers: [PrismaService, ScheduleService, ScheduleReplacerService],
|
||||
controllers: [ScheduleController, ScheduleReplacerController],
|
||||
exports: [ScheduleService],
|
||||
})
|
||||
export class ScheduleModule {}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { Inject, Injectable, NotFoundException } from "@nestjs/common";
|
||||
import {
|
||||
Inject,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from "@nestjs/common";
|
||||
import {
|
||||
ScheduleParser,
|
||||
ScheduleParseResult,
|
||||
@@ -17,7 +21,8 @@ import { Cache, CACHE_MANAGER } from "@nestjs/cache-manager";
|
||||
import { instanceToPlain } from "class-transformer";
|
||||
import { cacheGetOrFill } from "../utility/cache.util";
|
||||
import * as crypto from "crypto";
|
||||
import { ScheduleReplacerService } from "../schedule-replacer/schedule-replacer.service";
|
||||
import { ScheduleReplacerService } from "./schedule-replacer.service";
|
||||
import { FirebaseAdminService } from "../firebase-admin/firebase-admin.service";
|
||||
|
||||
@Injectable()
|
||||
export class ScheduleService {
|
||||
@@ -37,6 +42,7 @@ export class ScheduleService {
|
||||
constructor(
|
||||
@Inject(CACHE_MANAGER) private readonly cacheManager: Cache,
|
||||
private readonly scheduleReplacerService: ScheduleReplacerService,
|
||||
private readonly firebaseAdminService: FirebaseAdminService,
|
||||
) {
|
||||
const xlsDownloader = this.scheduleParser.getXlsDownloader();
|
||||
|
||||
@@ -77,8 +83,26 @@ export class ScheduleService {
|
||||
if (
|
||||
this.scheduleUpdatedAt.valueOf() === 0 ||
|
||||
this.cacheHash !== oldHash
|
||||
)
|
||||
) {
|
||||
if (this.scheduleUpdatedAt.valueOf() !== 0) {
|
||||
const isReplaced =
|
||||
await this.scheduleReplacerService.hasByEtag(
|
||||
schedule.etag,
|
||||
);
|
||||
|
||||
await this.firebaseAdminService.sendByTopic(
|
||||
"schedule-update",
|
||||
{
|
||||
data: {
|
||||
type: "schedule-update",
|
||||
replaced: isReplaced.toString(),
|
||||
etag: schedule.etag,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
this.scheduleUpdatedAt = new Date();
|
||||
}
|
||||
|
||||
return schedule;
|
||||
});
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { forwardRef, Module } from "@nestjs/common";
|
||||
import { UsersService } from "./users.service";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
import { UsersController } from "./users.controller";
|
||||
import { AuthService } from "../auth/auth.service";
|
||||
import { ScheduleModule } from "../schedule/schedule.module";
|
||||
import { AuthModule } from "../auth/auth.module";
|
||||
|
||||
@Module({
|
||||
imports: [ScheduleModule],
|
||||
providers: [PrismaService, UsersService, AuthService],
|
||||
imports: [forwardRef(() => ScheduleModule), forwardRef(() => AuthModule)],
|
||||
providers: [PrismaService, UsersService],
|
||||
exports: [UsersService],
|
||||
controllers: [UsersController],
|
||||
})
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import {
|
||||
ConflictException,
|
||||
forwardRef,
|
||||
Inject,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from "@nestjs/common";
|
||||
@@ -16,6 +18,7 @@ import { ScheduleService } from "../schedule/schedule.service";
|
||||
export class UsersService {
|
||||
constructor(
|
||||
private readonly prismaService: PrismaService,
|
||||
@Inject(forwardRef(() => ScheduleService))
|
||||
private readonly scheduleService: ScheduleService,
|
||||
) {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user