Уведомления об обновлении приложения.
This commit is contained in:
2024-10-06 02:43:13 +04:00
parent 6ffe39a4a9
commit 2efceeaec4
13 changed files with 106 additions and 15 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "schedule-parser-next", "name": "schedule-parser-next",
"version": "1.3.0", "version": "1.3.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "schedule-parser-next", "name": "schedule-parser-next",
"version": "1.3.0", "version": "1.3.1",
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
"@nestjs/cache-manager": "^2.2.2", "@nestjs/cache-manager": "^2.2.2",

View File

@@ -1,6 +1,6 @@
{ {
"name": "schedule-parser-next", "name": "schedule-parser-next",
"version": "1.3.0", "version": "1.3.1",
"description": "", "description": "",
"author": "N08I40K", "author": "N08I40K",
"private": true, "private": true,

View File

@@ -44,4 +44,6 @@ model User {
role UserRole role UserRole
// //
fcm FCM? fcm FCM?
//
version String
} }

View File

@@ -83,6 +83,7 @@ export class AuthService {
}), }),
role: signUpDto.role as UserRole, role: signUpDto.role as UserRole,
group: group, group: group,
version: signUpDto.version ?? "1.0.0",
}; };
return this.usersService.create(input).then((user) => { return this.usersService.create(input).then((user) => {

View File

@@ -1,4 +1,5 @@
import { configDotenv } from "dotenv"; import { configDotenv } from "dotenv";
import * as process from "node:process";
configDotenv(); configDotenv();
@@ -12,9 +13,14 @@ export const httpsConstants = {
}; };
export const apiConstants = { export const apiConstants = {
port: process.env.API_PORT ?? 5050, port: +(process.env.API_PORT ?? 5050),
version: process.env.SERVER_VERSION!,
}; };
export const firebaseConstants = { export const firebaseConstants = {
serviceAccountPath: process.env.FIREBASE_ACCOUNT_PATH!, serviceAccountPath: process.env.FIREBASE_ACCOUNT_PATH!,
}; };
export const scheduleConstants = {
cacheInvalidateDelay: +(process.env.SERVER_CACHE_INVALIDATE_DELAY! ?? 5),
};

View File

@@ -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 { UserDto } from "./user.dto";
import { IsString } from "class-validator"; import { IsString } from "class-validator";
import { Expose } from "class-transformer"; import { Expose } from "class-transformer";
@@ -19,6 +24,7 @@ export class SignInResDto extends PickType(UserDto, ["id", "accessToken"]) {}
export class SignUpReqDto extends IntersectionType( export class SignUpReqDto extends IntersectionType(
SignInReqDto, SignInReqDto,
PickType(UserDto, ["role", "group"]), PickType(UserDto, ["role", "group"]),
PartialType(PickType(UserDto, ["version"])),
) {} ) {}
export class SignUpResDto extends SignInResDto {} export class SignUpResDto extends SignInResDto {}

17
src/dto/fcm.dto.ts Normal file
View File

@@ -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;
}

View File

@@ -6,6 +6,7 @@ import {
IsMongoId, IsMongoId,
IsObject, IsObject,
IsOptional, IsOptional,
IsSemVer,
IsString, IsString,
MaxLength, MaxLength,
MinLength, MinLength,
@@ -97,6 +98,11 @@ export class UserDto {
@IsOptional() @IsOptional()
@Expose() @Expose()
fcm: UserFcmDto | null; fcm: UserFcmDto | null;
@ApiProperty({ description: "Версия установленого приложения" })
@IsSemVer()
@Expose()
version: string;
} }
export class ClientUserResDto extends OmitType(UserDto, [ export class ClientUserResDto extends OmitType(UserDto, [
@@ -104,6 +110,7 @@ export class ClientUserResDto extends OmitType(UserDto, [
"salt", "salt",
"accessToken", "accessToken",
"fcm", "fcm",
"version",
]) { ]) {
static fromUserDto(userDto: UserDto): ClientUserResDto { static fromUserDto(userDto: UserDto): ClientUserResDto {
return plainToClass(ClientUserResDto, userDto, { return plainToClass(ClientUserResDto, userDto, {

View File

@@ -1,4 +1,6 @@
import { import {
BadRequestException,
Body,
Controller, Controller,
HttpCode, HttpCode,
HttpStatus, HttpStatus,
@@ -12,11 +14,13 @@ import { UserFromTokenPipe } from "../auth/auth.pipe";
import { UserDto } from "../dto/user.dto"; import { UserDto } from "../dto/user.dto";
import { ResultDto } from "../utility/validation/class-validator.interceptor"; import { ResultDto } from "../utility/validation/class-validator.interceptor";
import { FirebaseAdminService } from "./firebase-admin.service"; import { FirebaseAdminService } from "./firebase-admin.service";
import { FcmPostUpdateDto } from "../dto/fcm.dto";
import { isSemVer } from "class-validator";
@Controller("api/v1/fcm") @Controller("api/v1/fcm")
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
export class FirebaseAdminController { export class FirebaseAdminController {
private readonly defaultTopics = new Set(["schedule-update"]); private readonly defaultTopics = new Set(["schedule-update", "app-update"]);
constructor(private readonly firebaseAdminService: FirebaseAdminService) {} constructor(private readonly firebaseAdminService: FirebaseAdminService) {}
@@ -38,4 +42,37 @@ export class FirebaseAdminController {
this.defaultTopics, 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<void> {
await this.firebaseAdminService.sendByTopic("app-update", {
data: {
type: "app-update",
version: postUpdateDto.version,
downloadLink: postUpdateDto.downloadLink,
},
});
}
} }

View File

@@ -66,6 +66,8 @@ export class FirebaseAdminService implements OnModuleInit {
const currentTopics = new Set(fcm.topics); const currentTopics = new Set(fcm.topics);
for (const topic of topics) { for (const topic of topics) {
if (!fcm.topics.includes(topic)) continue;
await this.messaging.unsubscribeFromTopic(fcm.token, topic); await this.messaging.unsubscribeFromTopic(fcm.token, topic);
currentTopics.delete(topic); currentTopics.delete(topic);
} }
@@ -84,6 +86,8 @@ export class FirebaseAdminService implements OnModuleInit {
const currentTopics = new Set(fcm.topics); const currentTopics = new Set(fcm.topics);
for (const topic of topics) { for (const topic of topics) {
if (fcm.topics.includes(topic)) continue;
await this.messaging.subscribeToTopic(fcm.token, topic); await this.messaging.subscribeToTopic(fcm.token, topic);
currentTopics.add(topic); currentTopics.add(topic);
} }
@@ -96,4 +100,17 @@ export class FirebaseAdminService implements OnModuleInit {
data: { fcm: fcm }, data: { fcm: fcm },
}); });
} }
async updateApp(
userDto: UserDto,
version: string,
topics: Set<string>,
): Promise<void> {
await this.subscribe(userDto, topics).then(async (userDto) => {
await this.usersService.update({
where: { id: userDto.id },
data: { version: version },
});
});
}
} }

View File

@@ -26,12 +26,12 @@ async function bootstrap() {
const swaggerConfig = new DocumentBuilder() const swaggerConfig = new DocumentBuilder()
.setTitle("Schedule Parser") .setTitle("Schedule Parser")
.setDescription("Парсер расписания") .setDescription("Парсер расписания")
.setVersion("1.0") .setVersion(apiConstants.version)
.build(); .build();
const swaggerDocument = SwaggerModule.createDocument(app, swaggerConfig); const swaggerDocument = SwaggerModule.createDocument(app, swaggerConfig);
swaggerDocument.servers = [ swaggerDocument.servers = [
{ {
url: "http://localhost:3000", url: `https://localhost:${apiConstants.port}`,
description: "Локальный сервер для разработки", description: "Локальный сервер для разработки",
}, },
]; ];

View File

@@ -1,8 +1,4 @@
import { import { Inject, Injectable, NotFoundException } from "@nestjs/common";
Inject,
Injectable,
NotFoundException,
} from "@nestjs/common";
import { import {
ScheduleParser, ScheduleParser,
ScheduleParseResult, ScheduleParseResult,
@@ -23,6 +19,7 @@ import { cacheGetOrFill } from "../utility/cache.util";
import * as crypto from "crypto"; import * as crypto from "crypto";
import { ScheduleReplacerService } from "./schedule-replacer.service"; import { ScheduleReplacerService } from "./schedule-replacer.service";
import { FirebaseAdminService } from "../firebase-admin/firebase-admin.service"; import { FirebaseAdminService } from "../firebase-admin/firebase-admin.service";
import { scheduleConstants } from "../contants";
@Injectable() @Injectable()
export class ScheduleService { export class ScheduleService {
@@ -57,7 +54,8 @@ export class ScheduleService {
return { return {
cacheHash: this.cacheHash, cacheHash: this.cacheHash,
cacheUpdateRequired: cacheUpdateRequired:
(Date.now() - this.cacheUpdatedAt.valueOf()) / 1000 / 60 >= 5, (Date.now() - this.cacheUpdatedAt.valueOf()) / 1000 / 60 >=
scheduleConstants.cacheInvalidateDelay,
lastCacheUpdate: this.cacheUpdatedAt.valueOf(), lastCacheUpdate: this.cacheUpdatedAt.valueOf(),
lastScheduleUpdate: this.scheduleUpdatedAt.valueOf(), lastScheduleUpdate: this.scheduleUpdatedAt.valueOf(),
}; };

View File

@@ -15,7 +15,7 @@ export class PartialValidationPipe implements PipeTransform {
this.partialValidationPipe = new ValidationPipe({ this.partialValidationPipe = new ValidationPipe({
...options, ...options,
...{ ...{
skipUndefinedProperties: true, skipUndefinedProperties: false,
skipNullValues: false, skipNullValues: false,
}, },
}); });