diff --git a/src/dao/index.ts b/src/dao/index.ts index 4011a4fa..6e955064 100644 --- a/src/dao/index.ts +++ b/src/dao/index.ts @@ -19,6 +19,7 @@ import { UserPhoneModel } from "../model/user/Phone"; import { UserSensitiveModel } from "../model/user/Sensitive"; import { UserEmailModel } from "../model/user/Email"; import { UserPmiModel } from "../model/user/Pmi"; +import { UserAgreementModel } from "../model/user/Agreement"; export const UserDAO = DAOImplement(UserModel) as ReturnType>; @@ -42,6 +43,8 @@ export const UserSensitiveDAO = DAOImplement(UserSensitiveModel) as ReturnType< export const UserPmiDAO = DAOImplement(UserPmiModel) as ReturnType>; +export const UserAgreementDAO = DAOImplement(UserAgreementModel) as ReturnType>; + export const RoomDAO = DAOImplement(RoomModel) as ReturnType>; export const RoomUserDAO = DAOImplement(RoomUserModel) as ReturnType>; diff --git a/src/model/index.ts b/src/model/index.ts index 26cbb958..bc92cc91 100644 --- a/src/model/index.ts +++ b/src/model/index.ts @@ -20,6 +20,7 @@ import { OAuthUsersModel } from "./oauth/oauth-users"; import { UserSensitiveModel } from "./user/Sensitive"; import { UserEmailModel } from "./user/Email"; import { UserPmiModel } from "./user/Pmi"; +import { UserAgreementModel } from "./user/Agreement"; import { PartnerModel } from "./partner/Partner"; import { PartnerRoomModel } from "./partner/PartnerRoom"; @@ -34,6 +35,7 @@ export type Model = | UserEmailModel | UserSensitiveModel | UserPmiModel + | UserAgreementModel | RoomModel | RoomUserModel | RoomPeriodicConfigModel diff --git a/src/model/user/Agreement.ts b/src/model/user/Agreement.ts new file mode 100644 index 00000000..eb86a4be --- /dev/null +++ b/src/model/user/Agreement.ts @@ -0,0 +1,26 @@ +import { Column, Entity, Index } from "typeorm"; +import { Content } from "../Content"; + +@Entity({ + name: "user_agreement", +}) +export class UserAgreementModel extends Content { + @Index("user_agreement_user_uuid_uindex", { + unique: true, + }) + @Column({ + length: 40, + }) + user_uuid: string; + + @Column({ + default: false, + }) + is_agree_collect_data: boolean; + + @Index("user_agreement_is_delete_index") + @Column({ + default: false, + }) + is_delete: boolean; +} diff --git a/src/thirdPartyService/TypeORMService.ts b/src/thirdPartyService/TypeORMService.ts index 2f2d15eb..59daf9a0 100644 --- a/src/thirdPartyService/TypeORMService.ts +++ b/src/thirdPartyService/TypeORMService.ts @@ -23,6 +23,7 @@ import { OAuthSecretsModel } from "../model/oauth/oauth-secrets"; import { OAuthUsersModel } from "../model/oauth/oauth-users"; import { UserEmailModel } from "../model/user/Email"; import { UserPmiModel } from "../model/user/Pmi"; +import { UserAgreementModel } from "../model/user/Agreement"; import { PartnerModel } from "../model/partner/Partner"; import { PartnerRoomModel } from "../model/partner/PartnerRoom"; @@ -44,6 +45,7 @@ export const dataSource = new DataSource({ UserEmailModel, UserSensitiveModel, UserPmiModel, + UserAgreementModel, RoomModel, RoomUserModel, RoomPeriodicConfigModel, diff --git a/src/v1/controller/user/Router.ts b/src/v1/controller/user/Router.ts index ab7fdf10..37093a5a 100644 --- a/src/v1/controller/user/Router.ts +++ b/src/v1/controller/user/Router.ts @@ -17,6 +17,9 @@ import { BindingGithub } from "./binding/platform/github/Binding"; import { BindingGoogle } from "./binding/platform/google/Binding"; import { BindingEmail } from "./binding/platform/email/Binding"; import { BindingApple } from "./binding/platform/apple/Binding"; +import { AgreementSet } from "./agreement/Set"; +import { AgreementGet } from "./agreement/Get"; +import { AgreementGetToRtc } from "./agreement/GetToRtc"; export const userRouters: Readonly>> = Object.freeze([ Rename, @@ -38,4 +41,7 @@ export const userRouters: Readonly>> = Object.fr DeleteAccount, UploadAvatarStart, UploadAvatarFinish, + AgreementSet, + AgreementGet, + AgreementGetToRtc ]); diff --git a/src/v1/controller/user/agreement/Get.ts b/src/v1/controller/user/agreement/Get.ts new file mode 100644 index 00000000..1de0f5a0 --- /dev/null +++ b/src/v1/controller/user/agreement/Get.ts @@ -0,0 +1,47 @@ +import { AbstractController, ControllerClassParams } from "../../../../abstract/controller"; +import { Status } from "../../../../constants/Project"; +import { Controller } from "../../../../decorator/Controller"; +import { FastifySchema, Response, ResponseError } from "../../../../types/Server"; + +import { ServiceUserAgreement } from "../../../service/user/UserAgreement"; + +@Controller({ + method: "post", + path: "user/agreement/get", + auth: true, +}) +export class AgreementGet extends AbstractController { + public static readonly schema: FastifySchema = {}; + + public readonly svc: { + userAgreement: ServiceUserAgreement; + }; + + public constructor(params: ControllerClassParams) { + super(params); + + this.svc = { + userAgreement: new ServiceUserAgreement(this.userUUID), + }; + } + + public async execute(): Promise> { + const isAgree = await this.svc.userAgreement.isAgreeCollectData(); + return { + status: Status.Success, + data: { + isAgree + } + } + } + + public errorHandler(error: Error): ResponseError { + return this.autoHandlerError(error); + } +} + +interface RequestType {} + +interface ResponseType { + isAgree: boolean; +} diff --git a/src/v1/controller/user/agreement/GetToRtc.ts b/src/v1/controller/user/agreement/GetToRtc.ts new file mode 100644 index 00000000..fb00ab42 --- /dev/null +++ b/src/v1/controller/user/agreement/GetToRtc.ts @@ -0,0 +1,108 @@ +import { AbstractController, ControllerClassParams } from "../../../../abstract/controller"; +import { Status } from "../../../../constants/Project"; +import { Controller } from "../../../../decorator/Controller"; +import { FastifySchema, Response, ResponseError } from "../../../../types/Server"; + +import { ServiceUserAgreement } from "../../../service/user/UserAgreement"; +import { RoomUserModel } from "../../../../model/room/RoomUser"; +import { dataSource } from "../../../../thirdPartyService/TypeORMService"; +import { UserAgreementModel } from "./../../../../model/user/Agreement"; + +@Controller({ + method: "get", + path: "private-polic/get", + auth: false, + skipAutoHandle: false, + enable: true +}) +export class AgreementGetToRtc extends AbstractController { + public static readonly schema: FastifySchema = { + querystring: { + type: "object", + required: ["uid"], + properties: { + uid: { + type: "string" + }, + room_uuid: { + type: "string" + } + }, + }, + }; + + public readonly svc: { + userAgreement: ServiceUserAgreement; + }; + + public constructor(params: ControllerClassParams) { + super(params); + + this.svc = { + userAgreement: new ServiceUserAgreement(this.userUUID), + }; + } + + public async execute(): Promise> { + const rtcUidstr = this.querystring.uid; + const room_uuid = this.querystring.room_uuid; + const rtcUids = rtcUidstr.split(","); + const userAgreementMap:Map = new Map(rtcUids.map(rtc_uid => [rtc_uid, false])); + const length = rtcUids.length; + if (length > 0) { + let i = 0; + while (i < length) { + const j = i + 50; + const batchedRtcUids = rtcUids.slice(i, j); + const roomUserInfos = await this.getRoomUserInfos(room_uuid, batchedRtcUids); + const userUuids = roomUserInfos.map(user => user.user_uuid); + if (userUuids.length > 0) { + const userAgreements = await this.getUserAgreements(userUuids); + for (const userInfo of roomUserInfos) { + const { rtc_uid, user_uuid } = userInfo; + const userAgreement = userAgreements.find(ua => ua.user_uuid === user_uuid); + if (userAgreement) { + userAgreementMap.set(rtc_uid, userAgreement.is_agree_collect_data); + } else { + userAgreementMap.set(rtc_uid, true); + } + } + } + i = j; + } + } + return { + status: Status.Success, + data: Object.fromEntries(userAgreementMap) + } + } + + private async getRoomUserInfos(room_uuid: string, rtc_uids: string[]): Promise { + return dataSource + .createQueryBuilder(RoomUserModel, "ru") + .where("ru.room_uuid = :room_uuid", { room_uuid }) + .andWhere("ru.rtc_uid IN (:...rtc_uids)", { rtc_uids }) + .getMany(); + } + private async getUserAgreements(userUuids: string[]): Promise { + return dataSource + .createQueryBuilder(UserAgreementModel, "ua") + .where("ua.user_uuid IN (:...userUuids)", { userUuids }) + .getMany(); + } + + public errorHandler(error: Error): ResponseError { + return this.autoHandlerError(error); + } +} + +interface RequestType { + querystring: { + uid: string; + room_uuid: string; + }; +} + +interface ResponseType { + [key: string]: boolean; +} diff --git a/src/v1/controller/user/agreement/Set.ts b/src/v1/controller/user/agreement/Set.ts new file mode 100644 index 00000000..22025601 --- /dev/null +++ b/src/v1/controller/user/agreement/Set.ts @@ -0,0 +1,61 @@ +import { AbstractController, ControllerClassParams } from "../../../../abstract/controller"; +import { Status } from "../../../../constants/Project"; +import { Controller } from "../../../../decorator/Controller"; +import { FastifySchema, Response, ResponseError } from "../../../../types/Server"; + +import { ServiceUserAgreement } from "../../../service/user/UserAgreement"; + +@Controller({ + method: "post", + path: "user/agreement/set", + auth: true, +}) +export class AgreementSet extends AbstractController { + public static readonly schema: FastifySchema = { + body: { + type: "object", + required: ["isAgree"], + properties: { + isAgree: { + type: "boolean" + }, + }, + }, + }; + + public readonly svc: { + userAgreement: ServiceUserAgreement; + }; + + public constructor(params: ControllerClassParams) { + super(params); + + this.svc = { + userAgreement: new ServiceUserAgreement(this.userUUID), + }; + } + + public async execute(): Promise> { + await this.svc.userAgreement.set(this.body.isAgree); + return { + status: Status.Success, + data: { + userUUID: this.userUUID + } + }; + } + + public errorHandler(error: Error): ResponseError { + return this.autoHandlerError(error); + } +} + +interface RequestType { + body: { + isAgree: boolean; + }; +} + +interface ResponseType { + userUUID: string; +} diff --git a/src/v1/service/user/UserAgreement.ts b/src/v1/service/user/UserAgreement.ts new file mode 100644 index 00000000..7f90c23c --- /dev/null +++ b/src/v1/service/user/UserAgreement.ts @@ -0,0 +1,65 @@ +import { UserAgreementDAO } from "../../../dao"; +import { DeleteResult, EntityManager, InsertResult } from "typeorm"; +import { UpdateResult } from "typeorm/query-builder/result/UpdateResult"; + +export class ServiceUserAgreement { + constructor(private readonly userUUID: string) {} + public async isAgreeCollectData(): Promise { + const bol = await ServiceUserAgreement.hasCollectData(this.userUUID); + if (bol) { + const isAgree = await ServiceUserAgreement.isAgreeCollectData(this.userUUID); + return isAgree; + } + return true; + } + public async hasCollectData(): Promise { + return await ServiceUserAgreement.hasCollectData(this.userUUID); + } + public static async isAgreeCollectData(userUUID: string): Promise { + const result = await UserAgreementDAO().findOne(["is_agree_collect_data"], { + user_uuid: userUUID, + }); + return Boolean(result && result.is_agree_collect_data); + } + public static async hasCollectData(userUUID: string): Promise { + const result = await UserAgreementDAO().findOne(["user_uuid"], { + user_uuid: userUUID, + }); + return Boolean(result); + } + public async set( + is_agree_collect_data: boolean, + t?: EntityManager, + ): Promise { + const has = await this.hasCollectData(); + if (!has) { + return await this.create(is_agree_collect_data, t); + } + return await this.update(is_agree_collect_data, t); + } + public async create( + is_agree_collect_data: boolean, + t?: EntityManager, + ): Promise { + return await UserAgreementDAO(t).insert({ + user_uuid: this.userUUID, + is_agree_collect_data, + }); + } + public async update(is_agree_collect_data: boolean, t?: EntityManager): Promise { + return await UserAgreementDAO(t).update( + { + is_agree_collect_data, + }, + { + user_uuid: this.userUUID, + }, + ); + } + + public async physicalDeletion(t?: EntityManager): Promise { + return await UserAgreementDAO(t).physicalDeletion({ + user_uuid: this.userUUID, + }); + } +} diff --git a/src/v1/service/user/__tests__/userAgreement.test.ts b/src/v1/service/user/__tests__/userAgreement.test.ts new file mode 100644 index 00000000..402be7af --- /dev/null +++ b/src/v1/service/user/__tests__/userAgreement.test.ts @@ -0,0 +1,183 @@ +import test from "ava"; +import { dataSource } from "../../../../thirdPartyService/TypeORMService"; +import { RoomUserDAO, UserAgreementDAO } from "../../../../dao"; +import { v4 } from "uuid"; +import { ServiceUserAgreement } from "../UserAgreement"; +import cryptoRandomString from "crypto-random-string"; +import { RoomUserModel } from "../../../../model/room/RoomUser"; +import { UserAgreementModel } from "../../../../model/user/Agreement"; + +const namespace = "[service][service-user][service-user-agreement]"; + +test.before(`${namespace} - initialize dataSource`, async () => { + await dataSource.initialize(); +}); + +test.after(`${namespace} - destroy dataSource`, async () => { + await dataSource.destroy(); +}); + +test(`${namespace} - set user collect agreement`, async ava => { + + const userUUID = v4(); + const serviceUserAgreement = new ServiceUserAgreement(userUUID); + + await serviceUserAgreement.set(true); + + let result = await UserAgreementDAO().findOne(["is_agree_collect_data"], { + user_uuid: userUUID, + }); + + ava.not(result, undefined); + + ava.is(result?.is_agree_collect_data, true); + + await serviceUserAgreement.set(false); + + result = await UserAgreementDAO().findOne(["is_agree_collect_data"], { + user_uuid: userUUID, + }); + + ava.is(result?.is_agree_collect_data, false); +}); + +test(`${namespace} - has agree collect data`, async ava => { + const userUUID = v4(); + + await UserAgreementDAO().insert({ + user_uuid: userUUID, + is_agree_collect_data: true, + }); + + const serviceUserAgreement = new ServiceUserAgreement(userUUID); + + const bol = await serviceUserAgreement.hasCollectData(); + + ava.is(bol, true); + + const userUUID1 = v4(); + + const bol1 = await ServiceUserAgreement.hasCollectData(userUUID1); + + ava.is(bol1, false); + +}); + +test(`${namespace} - delete user agreement`, async ava => { + const userUUID = v4(); + + await UserAgreementDAO().insert({ + user_uuid: userUUID, + is_agree_collect_data: true, + }); + + const serviceUserAgreement = new ServiceUserAgreement(userUUID); + await serviceUserAgreement.physicalDeletion(); + + const result = await UserAgreementDAO().findOne(["is_agree_collect_data"], { + user_uuid: userUUID, + }); + + ava.is(result, undefined); + + const bol = await serviceUserAgreement.hasCollectData(); + + ava.is(bol, false); + +}); +test(`${namespace} - get agree collect data`, async ava => { + + const userUUID = v4(); + await UserAgreementDAO().insert({ + user_uuid: userUUID, + is_agree_collect_data: true, + }); + + const serviceUserAgreement = new ServiceUserAgreement(userUUID); + const result = await serviceUserAgreement.isAgreeCollectData(); + + ava.not(result, undefined); + ava.is(result, true); + + const serviceUserAgreement1 = new ServiceUserAgreement(v4()); + + const result1 = await serviceUserAgreement1.hasCollectData(); + ava.is(result1, false); + + const result2 = await serviceUserAgreement.isAgreeCollectData(); + ava.is(result2, true); +}); +test(`${namespace} - get user by rtc_uuid collect agreement`, async ava => { + + const userUUID = v4(); + const roomUUID = v4(); + const rtcUUID = cryptoRandomString({ length: 6, type: "numeric" }); + const rtcUUID1 = cryptoRandomString({ length: 6, type: "numeric" }); + + await Promise.all([ + RoomUserDAO().insert({ + room_uuid: roomUUID, + user_uuid: userUUID, + rtc_uid: rtcUUID, + }), + UserAgreementDAO().insert({ + user_uuid: userUUID, + is_agree_collect_data: true, + }), + ]); + + const result = await RoomUserDAO().findOne(["user_uuid"], { + rtc_uid: rtcUUID, + room_uuid: roomUUID, + }); + + ava.not(result, undefined); + + ava.is(result?.user_uuid, userUUID); + + const result1 = await UserAgreementDAO().findOne(["user_uuid", "is_agree_collect_data"], { + user_uuid: userUUID, + }); + + ava.not(result1, undefined); + + ava.is(result?.user_uuid, result1?.user_uuid); + + const rtcUids = [rtcUUID, rtcUUID1]; + const userAgreementMap:Map = new Map(rtcUids.map(rtc_uid => [rtc_uid, false])); + const length = rtcUids.length; + if (length > 0) { + let i = 0; + while (i < length) { + const j = i + 50; + const batchedRtcUids = rtcUids.slice(i, j); + const roomUserInfos = await dataSource + .createQueryBuilder(RoomUserModel, "ru") + .where("ru.room_uuid = :room_uuid", { room_uuid:roomUUID }) + .andWhere("ru.rtc_uid IN (:...rtc_uids)", { rtc_uids: batchedRtcUids }) + .getMany(); + const userUuids = roomUserInfos.map(user => user.user_uuid); + if (userUuids.length > 0) { + const userAgreements = await dataSource + .createQueryBuilder(UserAgreementModel, "ua") + .where("ua.user_uuid IN (:...userUuids)", { userUuids }) + .getMany();; + for (const userInfo of roomUserInfos) { + const { rtc_uid, user_uuid } = userInfo; + const userAgreement = userAgreements.find(ua => ua.user_uuid === user_uuid); + if (userAgreement) { + userAgreementMap.set(rtc_uid, userAgreement.is_agree_collect_data); + } else { + userAgreementMap.set(rtc_uid, true); + } + } + } + i = j; + } + } + const obj = Object.fromEntries(userAgreementMap); + + ava.is(result1?.is_agree_collect_data, obj?.[rtcUUID]); + + ava.is(false, obj?.[rtcUUID1]); +}); \ No newline at end of file diff --git a/src/v2/dao/index.ts b/src/v2/dao/index.ts index 262beb03..9f558859 100644 --- a/src/v2/dao/index.ts +++ b/src/v2/dao/index.ts @@ -13,6 +13,7 @@ import { UserPhoneModel } from "../../model/user/Phone"; import { UserEmailModel } from "../../model/user/Email"; import { UserSensitiveModel } from "../../model/user/Sensitive"; import { UserPmiModel } from "../../model/user/Pmi"; +import { UserAgreementModel } from "../../model/user/Agreement"; import { RoomModel } from "../../model/room/Room"; import { RoomUserModel } from "../../model/room/RoomUser"; import { RoomPeriodicConfigModel } from "../../model/room/RoomPeriodicConfig"; @@ -189,6 +190,7 @@ export const userPhoneDAO = new DAO(UserPhoneModel); export const userEmailDAO = new DAO(UserEmailModel); export const userSensitiveDAO = new DAO(UserSensitiveModel); export const userPmiDAO = new DAO(UserPmiModel); +export const userAgreementDAO = new DAO(UserAgreementModel); export const roomDAO = new DAO(RoomModel); export const roomUserDAO = new DAO(RoomUserModel); export const roomPeriodicConfigDAO = new DAO(RoomPeriodicConfigModel);