From 2efceeaec4af12e29d093379d89df3bf5bfb3211 Mon Sep 17 00:00:00 2001 From: n08i40k Date: Sun, 6 Oct 2024 02:43:13 +0400 Subject: [PATCH] 1.3.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Уведомления об обновлении приложения. --- package-lock.json | 4 +- package.json | 2 +- prisma/schema.prisma | 2 + src/auth/auth.service.ts | 1 + src/contants.ts | 8 +++- src/dto/auth.dto.ts | 8 +++- src/dto/fcm.dto.ts | 17 ++++++++ src/dto/user.dto.ts | 7 ++++ .../firebase-admin.controller.ts | 39 ++++++++++++++++++- src/firebase-admin/firebase-admin.service.ts | 17 ++++++++ src/main.ts | 4 +- src/schedule/schedule.service.ts | 10 ++--- .../validation/partial-validation.pipe.ts | 2 +- 13 files changed, 106 insertions(+), 15 deletions(-) create mode 100644 src/dto/fcm.dto.ts diff --git a/package-lock.json b/package-lock.json index e46cef9..3a27871 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "schedule-parser-next", - "version": "1.3.0", + "version": "1.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "schedule-parser-next", - "version": "1.3.0", + "version": "1.3.1", "license": "UNLICENSED", "dependencies": { "@nestjs/cache-manager": "^2.2.2", diff --git a/package.json b/package.json index c1d3580..a233765 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "schedule-parser-next", - "version": "1.3.0", + "version": "1.3.1", "description": "", "author": "N08I40K", "private": true, diff --git a/prisma/schema.prisma b/prisma/schema.prisma index dc7ea6b..1202cef 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -44,4 +44,6 @@ model User { role UserRole // fcm FCM? + // + version String } diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index dae56c9..5da8b40 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -83,6 +83,7 @@ export class AuthService { }), role: signUpDto.role as UserRole, group: group, + version: signUpDto.version ?? "1.0.0", }; return this.usersService.create(input).then((user) => { diff --git a/src/contants.ts b/src/contants.ts index 66b25cc..00f5831 100644 --- a/src/contants.ts +++ b/src/contants.ts @@ -1,4 +1,5 @@ import { configDotenv } from "dotenv"; +import * as process from "node:process"; configDotenv(); @@ -12,9 +13,14 @@ export const httpsConstants = { }; export const apiConstants = { - port: process.env.API_PORT ?? 5050, + port: +(process.env.API_PORT ?? 5050), + version: process.env.SERVER_VERSION!, }; export const firebaseConstants = { serviceAccountPath: process.env.FIREBASE_ACCOUNT_PATH!, }; + +export const scheduleConstants = { + cacheInvalidateDelay: +(process.env.SERVER_CACHE_INVALIDATE_DELAY! ?? 5), +}; diff --git a/src/dto/auth.dto.ts b/src/dto/auth.dto.ts index 7d391cc..ca9965c 100644 --- a/src/dto/auth.dto.ts +++ b/src/dto/auth.dto.ts @@ -1,4 +1,9 @@ -import { ApiProperty, IntersectionType, PickType } from "@nestjs/swagger"; +import { + ApiProperty, + IntersectionType, + PartialType, + PickType, +} from "@nestjs/swagger"; import { UserDto } from "./user.dto"; import { IsString } from "class-validator"; import { Expose } from "class-transformer"; @@ -19,6 +24,7 @@ export class SignInResDto extends PickType(UserDto, ["id", "accessToken"]) {} export class SignUpReqDto extends IntersectionType( SignInReqDto, PickType(UserDto, ["role", "group"]), + PartialType(PickType(UserDto, ["version"])), ) {} export class SignUpResDto extends SignInResDto {} diff --git a/src/dto/fcm.dto.ts b/src/dto/fcm.dto.ts new file mode 100644 index 0000000..5b16e29 --- /dev/null +++ b/src/dto/fcm.dto.ts @@ -0,0 +1,17 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsSemVer, IsUrl } from "class-validator"; + +export class FcmPostUpdateDto { + @ApiProperty({ example: "1.6.0", description: "Версия приложения" }) + @IsSemVer() + // @Expose() + version: string; + + @ApiProperty({ + example: "https://download.host/app-release-1.6.0.apk", + description: "Ссылка на приложение", + }) + @IsUrl() + // @Expose() + downloadLink: string; +} diff --git a/src/dto/user.dto.ts b/src/dto/user.dto.ts index 03b9b07..5f2da85 100644 --- a/src/dto/user.dto.ts +++ b/src/dto/user.dto.ts @@ -6,6 +6,7 @@ import { IsMongoId, IsObject, IsOptional, + IsSemVer, IsString, MaxLength, MinLength, @@ -97,6 +98,11 @@ export class UserDto { @IsOptional() @Expose() fcm: UserFcmDto | null; + + @ApiProperty({ description: "Версия установленого приложения" }) + @IsSemVer() + @Expose() + version: string; } export class ClientUserResDto extends OmitType(UserDto, [ @@ -104,6 +110,7 @@ export class ClientUserResDto extends OmitType(UserDto, [ "salt", "accessToken", "fcm", + "version", ]) { static fromUserDto(userDto: UserDto): ClientUserResDto { return plainToClass(ClientUserResDto, userDto, { diff --git a/src/firebase-admin/firebase-admin.controller.ts b/src/firebase-admin/firebase-admin.controller.ts index 05cc0f7..59c3e48 100644 --- a/src/firebase-admin/firebase-admin.controller.ts +++ b/src/firebase-admin/firebase-admin.controller.ts @@ -1,4 +1,6 @@ import { + BadRequestException, + Body, Controller, HttpCode, HttpStatus, @@ -12,11 +14,13 @@ 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"; +import { FcmPostUpdateDto } from "../dto/fcm.dto"; +import { isSemVer } from "class-validator"; @Controller("api/v1/fcm") @UseGuards(AuthGuard) export class FirebaseAdminController { - private readonly defaultTopics = new Set(["schedule-update"]); + private readonly defaultTopics = new Set(["schedule-update", "app-update"]); constructor(private readonly firebaseAdminService: FirebaseAdminService) {} @@ -38,4 +42,37 @@ export class FirebaseAdminController { this.defaultTopics, ); } + + @Post("update-callback/:version") + @HttpCode(HttpStatus.OK) + @ResultDto(null) + async updateCallback( + @UserToken(UserFromTokenPipe) userDto: UserDto, + @Param("version") version: string, + ) { + if (!isSemVer(version)) { + throw new BadRequestException( + "version must be a Semantic Versioning Specification", + ); + } + + await this.firebaseAdminService.updateApp( + userDto, + version, + this.defaultTopics, + ); + } + + @Post("post-update") + @HttpCode(HttpStatus.OK) + @ResultDto(null) + async postUpdate(@Body() postUpdateDto: FcmPostUpdateDto): Promise { + await this.firebaseAdminService.sendByTopic("app-update", { + data: { + type: "app-update", + version: postUpdateDto.version, + downloadLink: postUpdateDto.downloadLink, + }, + }); + } } diff --git a/src/firebase-admin/firebase-admin.service.ts b/src/firebase-admin/firebase-admin.service.ts index bc64d8f..d7a8b19 100644 --- a/src/firebase-admin/firebase-admin.service.ts +++ b/src/firebase-admin/firebase-admin.service.ts @@ -66,6 +66,8 @@ export class FirebaseAdminService implements OnModuleInit { const currentTopics = new Set(fcm.topics); for (const topic of topics) { + if (!fcm.topics.includes(topic)) continue; + await this.messaging.unsubscribeFromTopic(fcm.token, topic); currentTopics.delete(topic); } @@ -84,6 +86,8 @@ export class FirebaseAdminService implements OnModuleInit { const currentTopics = new Set(fcm.topics); for (const topic of topics) { + if (fcm.topics.includes(topic)) continue; + await this.messaging.subscribeToTopic(fcm.token, topic); currentTopics.add(topic); } @@ -96,4 +100,17 @@ export class FirebaseAdminService implements OnModuleInit { data: { fcm: fcm }, }); } + + async updateApp( + userDto: UserDto, + version: string, + topics: Set, + ): Promise { + await this.subscribe(userDto, topics).then(async (userDto) => { + await this.usersService.update({ + where: { id: userDto.id }, + data: { version: version }, + }); + }); + } } diff --git a/src/main.ts b/src/main.ts index 596b7eb..2b4e98e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -26,12 +26,12 @@ async function bootstrap() { const swaggerConfig = new DocumentBuilder() .setTitle("Schedule Parser") .setDescription("Парсер расписания") - .setVersion("1.0") + .setVersion(apiConstants.version) .build(); const swaggerDocument = SwaggerModule.createDocument(app, swaggerConfig); swaggerDocument.servers = [ { - url: "http://localhost:3000", + url: `https://localhost:${apiConstants.port}`, description: "Локальный сервер для разработки", }, ]; diff --git a/src/schedule/schedule.service.ts b/src/schedule/schedule.service.ts index 8d74663..c681142 100644 --- a/src/schedule/schedule.service.ts +++ b/src/schedule/schedule.service.ts @@ -1,8 +1,4 @@ -import { - Inject, - Injectable, - NotFoundException, -} from "@nestjs/common"; +import { Inject, Injectable, NotFoundException } from "@nestjs/common"; import { ScheduleParser, ScheduleParseResult, @@ -23,6 +19,7 @@ import { cacheGetOrFill } from "../utility/cache.util"; import * as crypto from "crypto"; import { ScheduleReplacerService } from "./schedule-replacer.service"; import { FirebaseAdminService } from "../firebase-admin/firebase-admin.service"; +import { scheduleConstants } from "../contants"; @Injectable() export class ScheduleService { @@ -57,7 +54,8 @@ export class ScheduleService { return { cacheHash: this.cacheHash, cacheUpdateRequired: - (Date.now() - this.cacheUpdatedAt.valueOf()) / 1000 / 60 >= 5, + (Date.now() - this.cacheUpdatedAt.valueOf()) / 1000 / 60 >= + scheduleConstants.cacheInvalidateDelay, lastCacheUpdate: this.cacheUpdatedAt.valueOf(), lastScheduleUpdate: this.scheduleUpdatedAt.valueOf(), }; diff --git a/src/utility/validation/partial-validation.pipe.ts b/src/utility/validation/partial-validation.pipe.ts index 0a548f1..97cc75a 100644 --- a/src/utility/validation/partial-validation.pipe.ts +++ b/src/utility/validation/partial-validation.pipe.ts @@ -15,7 +15,7 @@ export class PartialValidationPipe implements PipeTransform { this.partialValidationPipe = new ValidationPipe({ ...options, ...{ - skipUndefinedProperties: true, + skipUndefinedProperties: false, skipNullValues: false, }, });