fix and update
This commit is contained in:
@@ -16,8 +16,9 @@ export class FamilyController {
|
||||
private readonly jwt: JwtService
|
||||
) {}
|
||||
|
||||
private async auth(authorization?: string) {
|
||||
return getAuthorizedUserId(this.jwt, this.core, authorization);
|
||||
private async auth(authorization?: string, passiveActivityHeader?: string) {
|
||||
const touchActivity = passiveActivityHeader !== '1';
|
||||
return getAuthorizedUserId(this.jwt, this.core, authorization, touchActivity);
|
||||
}
|
||||
|
||||
@Post('groups')
|
||||
@@ -33,8 +34,12 @@ export class FamilyController {
|
||||
|
||||
@Get('users/:userId/groups')
|
||||
@ApiOperation({ summary: 'Список семей пользователя' })
|
||||
async listGroups(@Headers('authorization') authorization: string | undefined, @Param('userId') userId: string) {
|
||||
const requesterId = await this.auth(authorization);
|
||||
async listGroups(
|
||||
@Headers('authorization') authorization: string | undefined,
|
||||
@Headers('x-id-passive-activity') passiveActivity: string | undefined,
|
||||
@Param('userId') userId: string
|
||||
) {
|
||||
const requesterId = await this.auth(authorization, passiveActivity);
|
||||
if (requesterId !== userId) {
|
||||
throw new ForbiddenException('Можно просматривать только свои семьи');
|
||||
}
|
||||
@@ -50,8 +55,12 @@ export class FamilyController {
|
||||
|
||||
@Get('groups/:groupId')
|
||||
@ApiOperation({ summary: 'Получить семейную группу' })
|
||||
async getGroup(@Headers('authorization') authorization: string | undefined, @Param('groupId') groupId: string) {
|
||||
const requesterId = await this.auth(authorization);
|
||||
async getGroup(
|
||||
@Headers('authorization') authorization: string | undefined,
|
||||
@Headers('x-id-passive-activity') passiveActivity: string | undefined,
|
||||
@Param('groupId') groupId: string
|
||||
) {
|
||||
const requesterId = await this.auth(authorization, passiveActivity);
|
||||
return firstValueFrom(this.core.family.GetFamilyGroup({ requesterId, groupId }));
|
||||
}
|
||||
|
||||
@@ -181,8 +190,12 @@ export class FamilyController {
|
||||
|
||||
@Get('groups/:groupId/presence')
|
||||
@ApiOperation({ summary: 'Онлайн-статус участников семьи' })
|
||||
async getPresence(@Headers('authorization') authorization: string | undefined, @Param('groupId') groupId: string) {
|
||||
const requesterId = await this.auth(authorization);
|
||||
async getPresence(
|
||||
@Headers('authorization') authorization: string | undefined,
|
||||
@Headers('x-id-passive-activity') passiveActivity: string | undefined,
|
||||
@Param('groupId') groupId: string
|
||||
) {
|
||||
const requesterId = await this.auth(authorization, passiveActivity);
|
||||
return firstValueFrom(this.core.family.GetFamilyPresence({ requesterId, groupId }));
|
||||
}
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ export class MediaController {
|
||||
|
||||
@Post('documents/:documentId/photo/upload-url')
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Получить URL для фото документа', description: 'Presigned URL для загрузки скана/фото документа в MinIO.' })
|
||||
@ApiOperation({ summary: 'Получить URL для файла документа', description: 'Presigned URL для загрузки скана, фото или файла документа в MinIO.' })
|
||||
@ApiBody({ type: DocumentPhotoUploadDto })
|
||||
async createDocumentPhotoUploadUrl(
|
||||
@Headers('authorization') authorization: string | undefined,
|
||||
@@ -125,22 +125,31 @@ export class MediaController {
|
||||
@Body() dto: DocumentPhotoUploadDto
|
||||
) {
|
||||
const userId = await this.authUserId(authorization);
|
||||
return firstValueFrom(this.core.media.CreateDocumentPhotoUploadUrl({ userId, documentId, contentType: dto.contentType }));
|
||||
return firstValueFrom(
|
||||
this.core.media.CreateDocumentPhotoUploadUrl({
|
||||
userId,
|
||||
documentId,
|
||||
contentType: dto.contentType,
|
||||
fileName: dto.fileName
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@Get('users/:userId/documents/photo-url')
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Получить ссылку на фото документа', description: 'Временная ссылка на просмотр фото документа (15 минут).' })
|
||||
@ApiOperation({ summary: 'Получить ссылку на файл документа', description: 'Временная ссылка на просмотр файла документа (15 минут).' })
|
||||
@ApiParam({ name: 'userId', description: 'ID владельца документа' })
|
||||
@ApiQuery({ name: 'storageKey', description: 'Ключ объекта в MinIO' })
|
||||
@ApiQuery({ name: 'fileName', required: false, description: 'Имя файла для скачивания' })
|
||||
async getDocumentPhotoUrl(
|
||||
@Headers('authorization') authorization: string | undefined,
|
||||
@Param('userId') userId: string,
|
||||
@Query('storageKey') storageKey: string
|
||||
@Query('storageKey') storageKey: string,
|
||||
@Query('fileName') fileName?: string
|
||||
) {
|
||||
const requesterId = await this.authUserId(authorization);
|
||||
return firstValueFrom(
|
||||
this.core.media.GetDocumentPhotoAccessUrl({ requesterId, targetUserId: userId, storageKey })
|
||||
this.core.media.GetDocumentPhotoAccessUrl({ requesterId, targetUserId: userId, storageKey, fileName })
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -40,8 +40,12 @@ export class NotificationsController {
|
||||
|
||||
@Get('unread-count')
|
||||
@ApiOperation({ summary: 'Количество непрочитанных уведомлений' })
|
||||
async unreadCount(@Headers('authorization') authorization: string | undefined) {
|
||||
const userId = await getAuthorizedUserId(this.jwt, this.core, authorization);
|
||||
async unreadCount(
|
||||
@Headers('authorization') authorization: string | undefined,
|
||||
@Headers('x-id-passive-activity') passiveActivity: string | undefined
|
||||
) {
|
||||
const touchActivity = passiveActivity !== '1';
|
||||
const userId = await getAuthorizedUserId(this.jwt, this.core, authorization, touchActivity);
|
||||
return firstValueFrom(this.core.notifications.GetUnreadCount({ userId }));
|
||||
}
|
||||
|
||||
|
||||
@@ -10,9 +10,14 @@ interface RequesterProfile {
|
||||
canViewUserDocuments?: boolean;
|
||||
}
|
||||
|
||||
async function getRequesterProfile(jwt: JwtService, core: CoreGrpcService, authorization?: string): Promise<RequesterProfile> {
|
||||
async function getRequesterProfile(
|
||||
jwt: JwtService,
|
||||
core: CoreGrpcService,
|
||||
authorization?: string,
|
||||
touchActivity = true
|
||||
): Promise<RequesterProfile> {
|
||||
const payload = await verifyAccessToken(jwt, authorization);
|
||||
await assertSessionUnlocked(core, payload);
|
||||
await assertSessionUnlocked(core, payload, touchActivity);
|
||||
|
||||
if (payload.isSuperAdmin) {
|
||||
return { id: payload.sub, isSuperAdmin: true, canViewUserDocuments: true };
|
||||
@@ -51,7 +56,12 @@ export async function assertDocumentsWriteAccess(
|
||||
return requester.id;
|
||||
}
|
||||
|
||||
export async function getAuthorizedUserId(jwt: JwtService, core: CoreGrpcService, authorization?: string) {
|
||||
const requester = await getRequesterProfile(jwt, core, authorization);
|
||||
export async function getAuthorizedUserId(
|
||||
jwt: JwtService,
|
||||
core: CoreGrpcService,
|
||||
authorization?: string,
|
||||
touchActivity = true
|
||||
) {
|
||||
const requester = await getRequesterProfile(jwt, core, authorization, touchActivity);
|
||||
return requester.id;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,21 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsIn, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
const IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'] as const;
|
||||
|
||||
const DOCUMENT_ATTACHMENT_TYPES = [
|
||||
...IMAGE_TYPES,
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.ms-powerpoint',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'text/plain',
|
||||
'application/rtf',
|
||||
'application/vnd.oasis.opendocument.text'
|
||||
] as const;
|
||||
|
||||
export class AvatarUploadDto {
|
||||
@ApiProperty({ description: 'MIME-тип изображения', example: 'image/jpeg', enum: IMAGE_TYPES })
|
||||
@IsString({ message: 'Укажите MIME-тип изображения' })
|
||||
@@ -16,10 +31,17 @@ export class ConfirmAvatarDto {
|
||||
}
|
||||
|
||||
export class DocumentPhotoUploadDto {
|
||||
@ApiProperty({ description: 'MIME-тип изображения', example: 'image/jpeg', enum: IMAGE_TYPES })
|
||||
@IsString({ message: 'Укажите MIME-тип изображения' })
|
||||
@IsIn([...IMAGE_TYPES], { message: 'Допустимы только JPEG, PNG, WEBP или GIF' })
|
||||
@ApiProperty({ description: 'MIME-тип файла', example: 'application/pdf', enum: DOCUMENT_ATTACHMENT_TYPES })
|
||||
@IsString({ message: 'Укажите MIME-тип файла' })
|
||||
@IsIn([...DOCUMENT_ATTACHMENT_TYPES], {
|
||||
message: 'Допустимы изображения, PDF, Word, Excel, PowerPoint, TXT и ODT'
|
||||
})
|
||||
contentType!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Имя файла для определения расширения', example: 'passport.pdf' })
|
||||
@IsOptional()
|
||||
@IsString({ message: 'Имя файла должно быть строкой' })
|
||||
fileName?: string;
|
||||
}
|
||||
|
||||
export class ChatMediaUploadDto {
|
||||
|
||||
@@ -28,7 +28,11 @@ export async function verifyAccessToken(jwt: JwtService, authorization?: string)
|
||||
}
|
||||
}
|
||||
|
||||
export async function assertSessionUnlocked(core: CoreGrpcService, payload: AccessTokenPayload) {
|
||||
export async function assertSessionUnlocked(
|
||||
core: CoreGrpcService,
|
||||
payload: AccessTokenPayload,
|
||||
touchActivity = true
|
||||
) {
|
||||
if (!payload.sessionId) {
|
||||
return;
|
||||
}
|
||||
@@ -37,7 +41,7 @@ export async function assertSessionUnlocked(core: CoreGrpcService, payload: Acce
|
||||
core.auth.ValidateSession({
|
||||
userId: payload.sub,
|
||||
sessionId: payload.sessionId,
|
||||
touchActivity: true
|
||||
touchActivity
|
||||
})
|
||||
)) as { requiresPin: boolean; sessionId: string };
|
||||
|
||||
@@ -54,9 +58,10 @@ export async function assertSessionUnlocked(core: CoreGrpcService, payload: Acce
|
||||
export async function resolveAuthorizedPayload(
|
||||
jwt: JwtService,
|
||||
core: CoreGrpcService,
|
||||
authorization?: string
|
||||
authorization?: string,
|
||||
touchActivity = true
|
||||
): Promise<AccessTokenPayload> {
|
||||
const payload = await verifyAccessToken(jwt, authorization);
|
||||
await assertSessionUnlocked(core, payload);
|
||||
await assertSessionUnlocked(core, payload, touchActivity);
|
||||
return payload;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user