Merge branch 'dev'

This commit is contained in:
faris Aljohari
2025-02-05 06:33:59 -06:00
252 changed files with 11317 additions and 1255 deletions

View File

@ -1,3 +1,5 @@
NODE_ENV=
ACCESS_KEY=
AZURE_POSTGRESQL_DATABASE=
@ -52,6 +54,18 @@ SMTP_SECURE=
SMTP_USER=
MAILTRAP_API_TOKEN=
MAILTRAP_INVITATION_TEMPLATE_UUID=
MAILTRAP_EDIT_USER_TEMPLATE_UUID=
MAILTRAP_DISABLE_TEMPLATE_UUID=
MAILTRAP_ENABLE_TEMPLATE_UUID=
MAILTRAP_DELETE_USER_TEMPLATE_UUID=
WEBSITES_ENABLE_APP_SERVICE_STORAGE=
PORT=

View File

@ -22,4 +22,11 @@ module.exports = {
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
settings: {
'import/resolver': {
node: {
caseSensitive: true,
},
},
},
};

View File

@ -4,5 +4,5 @@ export class AuthInterface {
uuid: string;
sessionId: string;
id: number;
roles?: string[];
role?: object;
}

View File

@ -11,6 +11,8 @@ import { UserSessionRepository } from '../../../../common/src/modules/session/re
import { UserSessionEntity } from '../../../../common/src/modules/session/entities';
import { ConfigService } from '@nestjs/config';
import { OAuth2Client } from 'google-auth-library';
import { PlatformType } from '@app/common/constants/platform-type.enum';
import { RoleType } from '@app/common/constants/role.type.enum';
@Injectable()
export class AuthService {
@ -29,33 +31,46 @@ export class AuthService {
email: string,
pass: string,
regionUuid?: string,
platform?: PlatformType,
): Promise<any> {
const user = await this.userRepository.findOne({
where: {
email,
region: regionUuid
? {
uuid: regionUuid,
}
: undefined,
region: regionUuid ? { uuid: regionUuid } : undefined,
},
relations: ['roles.roleType'],
relations: ['roleType'],
});
if (
platform === PlatformType.WEB &&
(user.roleType.type === RoleType.SPACE_OWNER ||
user.roleType.type === RoleType.SPACE_MEMBER)
) {
throw new UnauthorizedException('Access denied for web platform');
}
if (!user) {
throw new BadRequestException('Invalid credentials');
}
if (!user.isUserVerified) {
throw new BadRequestException('User is not verified');
}
if (user) {
const passwordMatch = this.helperHashService.bcryptCompare(
pass,
user.password,
);
if (passwordMatch) {
const { ...result } = user;
return result;
}
if (!user.isActive) {
throw new BadRequestException('User is not active');
}
return null;
if (!user.hasAcceptedAppAgreement) {
throw new BadRequestException('User has not accepted app agreement');
}
const passwordMatch = await this.helperHashService.bcryptCompare(
pass,
user.password,
);
if (!passwordMatch) {
throw new BadRequestException('Invalid credentials');
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { password, ...result } = user;
return result;
}
async createSession(data): Promise<UserSessionEntity> {
@ -85,10 +100,11 @@ export class AuthService {
email: user.email,
userId: user.userId,
uuid: user.uuid,
type: user.type,
sessionId: user.sessionId,
roles: user?.roles,
role: user?.role,
googleCode: user.googleCode,
hasAcceptedWebAgreement: user.hasAcceptedWebAgreement,
hasAcceptedAppAgreement: user.hasAcceptedAppAgreement,
};
if (payload.googleCode) {
const profile = await this.getProfile(payload.googleCode);

View File

@ -31,7 +31,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
userUuid: payload.uuid,
uuid: payload.uuid,
sessionId: payload.sessionId,
roles: payload?.roles,
role: payload?.role,
};
} else {
throw new BadRequestException('Unauthorized');

View File

@ -34,7 +34,7 @@ export class RefreshTokenStrategy extends PassportStrategy(
userUuid: payload.uuid,
uuid: payload.uuid,
sessionId: payload.sessionId,
roles: payload?.roles,
role: payload?.role,
};
} else {
throw new BadRequestException('Unauthorized');

View File

@ -9,6 +9,12 @@ import { EmailService } from './util/email.service';
import { ErrorMessageService } from 'src/error-message/error-message.service';
import { TuyaService } from './integrations/tuya/services/tuya.service';
import { SceneDeviceRepository } from './modules/scene-device/repositories';
import { SpaceRepository } from './modules/space';
import {
SpaceModelRepository,
SubspaceModelRepository,
} from './modules/space-model';
import { SubspaceRepository } from './modules/space/repositories/subspace.repository';
@Module({
providers: [
CommonService,
@ -16,6 +22,10 @@ import { SceneDeviceRepository } from './modules/scene-device/repositories';
ErrorMessageService,
TuyaService,
SceneDeviceRepository,
SpaceRepository,
SubspaceRepository,
SubspaceModelRepository,
SpaceModelRepository,
],
exports: [
CommonService,
@ -25,6 +35,10 @@ import { SceneDeviceRepository } from './modules/scene-device/repositories';
EmailService,
ErrorMessageService,
SceneDeviceRepository,
SpaceRepository,
SubspaceRepository,
SubspaceModelRepository,
SpaceModelRepository,
],
imports: [
ConfigModule.forRoot({

View File

@ -10,5 +10,14 @@ export default registerAs(
SMTP_USER: process.env.SMTP_USER,
SMTP_SENDER: process.env.SMTP_SENDER,
SMTP_PASSWORD: process.env.SMTP_PASSWORD,
MAILTRAP_API_TOKEN: process.env.MAILTRAP_API_TOKEN,
MAILTRAP_INVITATION_TEMPLATE_UUID:
process.env.MAILTRAP_INVITATION_TEMPLATE_UUID,
MAILTRAP_DISABLE_TEMPLATE_UUID: process.env.MAILTRAP_DISABLE_TEMPLATE_UUID,
MAILTRAP_ENABLE_TEMPLATE_UUID: process.env.MAILTRAP_ENABLE_TEMPLATE_UUID,
MAILTRAP_DELETE_USER_TEMPLATE_UUID:
process.env.MAILTRAP_DELETE_USER_TEMPLATE_UUID,
MAILTRAP_EDIT_USER_TEMPLATE_UUID:
process.env.MAILTRAP_EDIT_USER_TEMPLATE_UUID,
}),
);

View File

@ -2,6 +2,8 @@ export enum ActionExecutorEnum {
DEVICE_ISSUE = 'device_issue',
DELAY = 'delay',
RULE_TRIGGER = 'rule_trigger',
RULE_DISABLE = 'rule_disable',
RULE_ENABLE = 'rule_enable',
}
export enum EntityTypeEnum {

View File

@ -1,4 +1,47 @@
export class ControllerRoute {
static PROJECT = class {
public static readonly ROUTE = 'projects';
static ACTIONS = class {
public static readonly CREATE_PROJECT_SUMMARY = 'Create a new project';
public static readonly CREATE_PROJECT_DESCRIPTION =
'This endpoint allows you to create a new project by providing the required project details.';
public static readonly GET_PROJECT_SUMMARY = 'Retrieve project details';
public static readonly GET_PROJECT_DESCRIPTION =
'This endpoint retrieves the details of a project by its unique identifier (UUID).';
public static readonly UPDATE_PROJECT_SUMMARY = 'Update project details';
public static readonly UPDATE_PROJECT_DESCRIPTION =
'This endpoint updates the details of an existing project using its unique identifier (UUID).';
public static readonly LIST_PROJECTS_SUMMARY = 'List all projects';
public static readonly LIST_PROJECTS_DESCRIPTION =
'This endpoint retrieves a list of all existing projects, including their details.';
public static readonly DELETE_PROJECT_SUMMARY = 'Delete a project';
public static readonly DELETE_PROJECT_DESCRIPTION =
'This endpoint deletes an existing project by its unique identifier (UUID).';
public static readonly GET_USERS_BY_PROJECT_SUMMARY =
'Get users by project';
public static readonly GET_USERS_BY_PROJECT_DESCRIPTION =
'This endpoint retrieves all users associated with a specific project.';
public static readonly GET_USER_BY_UUID_IN_PROJECT_SUMMARY =
'Get user by uuid in project';
public static readonly GET_USER_BY_UUID_IN_PROJECT_DESCRIPTION =
'This endpoint retrieves a user by their unique identifier (UUID) associated with a specific project.';
};
};
static PROJECT_USER = class {
public static readonly ROUTE = '/projects/:projectUuid/user';
static ACTIONS = class {
public static readonly GET_USERS_BY_PROJECT_SUMMARY =
'Get users by project';
public static readonly GET_USERS_BY_PROJECT_DESCRIPTION =
'This endpoint retrieves all users associated with a specific project.';
};
};
static REGION = class {
public static readonly ROUTE = 'region';
static ACTIONS = class {
@ -10,7 +53,7 @@ export class ControllerRoute {
};
static COMMUNITY = class {
public static readonly ROUTE = 'communities';
public static readonly ROUTE = '/projects/:projectUuid/communities';
static ACTIONS = class {
public static readonly GET_COMMUNITY_BY_ID_SUMMARY =
'Get community by community community uuid';
@ -115,7 +158,8 @@ export class ControllerRoute {
};
static SPACE = class {
public static readonly ROUTE = '/communities/:communityUuid/spaces';
public static readonly ROUTE =
'/projects/:projectUuid/communities/:communityUuid/spaces';
static ACTIONS = class {
public static readonly CREATE_SPACE_SUMMARY = 'Create a new space';
public static readonly CREATE_SPACE_DESCRIPTION =
@ -156,7 +200,7 @@ export class ControllerRoute {
static SPACE_SCENE = class {
public static readonly ROUTE =
'/communities/:communityUuid/spaces/:spaceUuid/scenes';
'/projects/:projectUuid/communities/:communityUuid/spaces/:spaceUuid/scenes';
static ACTIONS = class {
public static readonly GET_TAP_TO_RUN_SCENE_BY_SPACE_SUMMARY =
'Retrieve Tap-to-Run Scenes by Space';
@ -167,7 +211,7 @@ export class ControllerRoute {
static SPACE_USER = class {
public static readonly ROUTE =
'/communities/:communityUuid/spaces/:spaceUuid/user';
'/projects/:projectUuid/communities/:communityUuid/spaces/:spaceUuid/user';
static ACTIONS = class {
public static readonly ASSOCIATE_SPACE_USER_SUMMARY =
'Associate a user to a space';
@ -183,7 +227,7 @@ export class ControllerRoute {
static SPACE_DEVICES = class {
public static readonly ROUTE =
'/communities/:communityUuid/spaces/:spaceUuid/devices';
'/projects/:projectUuid/communities/:communityUuid/spaces/:spaceUuid/devices';
static ACTIONS = class {
public static readonly LIST_SPACE_DEVICE_SUMMARY =
'List devices in a space';
@ -194,7 +238,7 @@ export class ControllerRoute {
static SUBSPACE = class {
public static readonly ROUTE =
'/communities/:communityUuid/spaces/:spaceUuid/subspaces';
'/projects/:projectUuid/communities/:communityUuid/spaces/:spaceUuid/subspaces';
static ACTIONS = class {
public static readonly CREATE_SUBSPACE_SUMMARY = 'Create Subspace';
public static readonly CREATE_SUBSPACE_DESCRIPTION =
@ -220,7 +264,7 @@ export class ControllerRoute {
static SUBSPACE_DEVICE = class {
public static readonly ROUTE =
'/communities/:communityUuid/spaces/:spaceUuid/subspaces/:subSpaceUuid/devices';
'/projects/:projectUuid/communities/:communityUuid/spaces/:spaceUuid/subspaces/:subSpaceUuid/devices';
static ACTIONS = class {
public static readonly LIST_SUBSPACE_DEVICE_SUMMARY =
@ -240,6 +284,32 @@ export class ControllerRoute {
};
};
static SPACE_MODEL = class {
public static readonly ROUTE = '/projects/:projectUuid/space-models';
static ACTIONS = class {
public static readonly CREATE_SPACE_MODEL_SUMMARY =
'Create a New Space Model';
public static readonly CREATE_SPACE_MODEL_DESCRIPTION =
'This endpoint allows you to create a new space model within a specified project. A space model defines the structure of spaces, including subspaces, products, and product items, and is uniquely identifiable within the project.';
public static readonly GET_SPACE_MODEL_SUMMARY = 'Get a New Space Model';
public static readonly GET_SPACE_MODEL_DESCRIPTION =
'Fetch a space model details';
public static readonly LIST_SPACE_MODEL_SUMMARY = 'List Space Models';
public static readonly LIST_SPACE_MODEL_DESCRIPTION =
'This endpoint allows you to retrieve a list of space models within a specified project. Each space model includes its structure, associated subspaces, products, and product items.';
public static readonly UPDATE_SPACE_MODEL_SUMMARY = 'Update Space Model';
public static readonly UPDATE_SPACE_MODEL_DESCRIPTION =
'This endpoint allows you to update a Space Model attributesas well as manage its associated Subspaces and Device';
public static readonly DELETE_SPACE_MODEL_SUMMARY = 'Delete Space Model';
public static readonly DELETE_SPACE_MODEL_DESCRIPTION =
'This endpoint allows you to delete a specified Space Model within a project. Deleting a Space Model disables the model and all its associated subspaces and tags, ensuring they are no longer active but remain in the system for auditing.';
};
};
static PRODUCT = class {
public static readonly ROUTE = 'products';
static ACTIONS = class {
@ -279,6 +349,10 @@ export class ControllerRoute {
public static readonly DELETE_USER_SUMMARY = 'Delete user by UUID';
public static readonly DELETE_USER_DESCRIPTION =
'This endpoint deletes a user identified by their UUID. Accessible only by users with the Super Admin role.';
public static readonly UPDATE_USER_WEB_AGREEMENT_SUMMARY =
'Update user web agreement by user UUID';
public static readonly UPDATE_USER_WEB_AGREEMENT_DESCRIPTION =
'This endpoint updates the web agreement for a user identified by their UUID.';
};
};
static AUTHENTICATION = class {
@ -334,6 +408,25 @@ export class ControllerRoute {
'This endpoint adds a new user role to the system based on the provided role data.';
};
};
static TERMS_AND_CONDITIONS = class {
public static readonly ROUTE = 'terms';
static ACTIONS = class {
public static readonly FETCH_TERMS_SUMMARY = 'Fetch Terms and Conditions';
public static readonly FETCH_TERMS_DESCRIPTION =
'This endpoint retrieves the terms and conditions for the application.';
};
};
static PRIVACY_POLICY = class {
public static readonly ROUTE = 'policy';
static ACTIONS = class {
public static readonly FETCH_POLICY_SUMMARY = 'Fetch Privacy Policy';
public static readonly FETCH_POLICY_DESCRIPTION =
'This endpoint retrieves the privacy policy for the application.';
};
};
static GROUP = class {
public static readonly ROUTE = 'group';
@ -685,4 +778,53 @@ export class ControllerRoute {
'This endpoint deletes a users subscription for device messages.';
};
};
static INVITE_USER = class {
public static readonly ROUTE = 'invite-user';
static ACTIONS = class {
public static readonly CREATE_USER_INVITATION_SUMMARY =
'Create user invitation';
public static readonly CREATE_USER_INVITATION_DESCRIPTION =
'This endpoint creates an invitation for a user to assign to role and spaces.';
public static readonly UPDATE_USER_INVITATION_SUMMARY =
'Update user invitation';
public static readonly UPDATE_USER_INVITATION_DESCRIPTION =
'This endpoint updates an invitation for a user to assign to role and spaces.';
public static readonly DISABLE_USER_INVITATION_SUMMARY =
'Disable user invitation';
public static readonly DISABLE_USER_INVITATION_DESCRIPTION =
'This endpoint disables an invitation for a user to assign to role and spaces.';
public static readonly DELETE_USER_INVITATION_SUMMARY =
'Delete user invitation';
public static readonly DELETE_USER_INVITATION_DESCRIPTION =
'This endpoint deletes an invitation for a user to assign to role and spaces.';
public static readonly ACTIVATION_CODE_SUMMARY =
'Activate Invitation Code';
public static readonly ACTIVATION_CODE_DESCRIPTION =
'This endpoint activate invitation code';
public static readonly CHECK_EMAIL_SUMMARY = 'Check email';
public static readonly CHECK_EMAIL_DESCRIPTION =
'This endpoint checks if an email already exists and have a project in the system.';
};
};
static PERMISSION = class {
public static readonly ROUTE = 'permission';
static ACTIONS = class {
public static readonly GET_PERMISSION_BY_ROLE_SUMMARY =
'Get permissions by role';
public static readonly GET_PERMISSION_BY_ROLE_DESCRIPTION =
'This endpoint retrieves the permissions associated with a specific role.';
};
};
}

View File

@ -0,0 +1,3 @@
export const SEND_EMAIL_API_URL_PROD = 'https://send.api.mailtrap.io/api/send/';
export const SEND_EMAIL_API_URL_DEV =
'https://sandbox.api.mailtrap.io/api/send/2634012';

View File

@ -0,0 +1,5 @@
export enum ModifyAction {
ADD = 'add',
UPDATE = 'update',
DELETE = 'delete',
}

View File

@ -0,0 +1,4 @@
export const ORPHAN_COMMUNITY_NAME = 'orphan-community';
export const ORPHAN_COMMUNITY_DESCRIPTION =
'Default community for orphan spaces';
export const ORPHAN_SPACE_NAME = 'orphan-space';

View File

@ -0,0 +1,53 @@
export const PermissionMapping = {
DEVICE_MANAGEMENT: {
DEVICE: [
'SINGLE_CONTROL',
'VIEW',
'DELETE',
'UPDATE',
'BATCH_CONTROL',
'LOCATION_VIEW',
'LOCATION_UPDATE',
],
FIRMWARE: ['CONTROL', 'VIEW'],
},
COMMUNITY_MANAGEMENT: {
COMMUNITY: ['VIEW', 'ADD', 'UPDATE', 'DELETE'],
},
SPACE_MANAGEMENT: {
SPACE: [
'VIEW',
'ADD',
'UPDATE',
'DELETE',
'MODEL_ADD',
'MODEL_DELETE',
'MODEL_VIEW',
'ASSIGN_USER_TO_SPACE',
'DELETE_USER_FROM_SPACE',
],
SUBSPACE: [
'VIEW',
'ADD',
'UPDATE',
'DELETE',
'ASSIGN_DEVICE_TO_SUBSPACE',
'DELETE_DEVICE_FROM_SUBSPACE',
],
},
DEVICE_WIZARD: {
DEVICE_WIZARD: ['VIEW_DEVICE_WIZARD'],
SPACE_DEVICE: ['VIEW_DEVICE_IN_SPACE', 'ASSIGN_DEVICE_TO_SPACE'],
SUBSPACE_DEVICE: ['VIEW_DEVICE_IN_SUBSPACE', 'UPDATE_DEVICE_IN_SUBSPACE'],
},
AUTOMATION_MANAGEMENT: {
AUTOMATION: ['VIEW', 'ADD', 'UPDATE', 'DELETE', 'CONTROL'],
SCENES: ['VIEW', 'ADD', 'UPDATE', 'DELETE', 'CONTROL'],
},
VISITOR_PASSWORD_MANAGEMENT: {
VISITOR_PASSWORD: ['VIEW', 'ADD', 'UPDATE', 'DELETE'],
},
USER_MANAGEMENT: {
USER: ['ADD'],
},
};

View File

@ -0,0 +1,4 @@
export enum PlatformType {
WEB = 'web',
MOBILE = 'mobile',
}

View File

@ -0,0 +1,37 @@
<div>
<p><strong>Syncrow Mobile Privacy Policy</strong></p>
<p>
Effective Date: 26/06/2022<br />
Updated: 26/06/2022
</p>
<p>
Syncrow and subsidiaries (“we”, “us”, “our”, “Syncrow”) are committed to
protecting your privacy. This Privacy Policy (“Policy”) describes our
practices in connection with information privacy on Personal Data we process
through your individual use of the following services, products, and related
mobile applications (collectively, the “Products”):
</p>
<ul>
<li>Syncrow Mobile Application</li>
</ul>
<p>
Before you use our Products, please carefully read through this Policy and
understand our purposes and practices of collection, processing of your
Personal Data, including how we use, store, share and transfer Personal
Data. In the Policy you will also find ways to execute your rights of
access, update, delete or protect your Personal Data.
</p>
<p>
When you accept this Policy when you register with your Personal Data, or if
you start to use our Products and does not expressly object to the contents
of this Policy, we will consider that you fully understand and agree with
this Policy. If you have any questions regarding this Policy, please do not
hesitate to contact us via:
</p>
<p>
For other branded mobile applications powered by Syncrow, our Clients
control all the Personal Data collected through our Products. We collect the
information under the direction of our Clients and the processing of such
information.
</p>
</div>

View File

@ -18,4 +18,5 @@ export enum ProductType {
PC = 'PC',
FOUR_S = '4S',
SIX_S = '6S',
SOS = 'SOS',
}

View File

@ -0,0 +1,165 @@
import { RoleType } from './role.type.enum';
export const RolePermissions = {
[RoleType.SUPER_ADMIN]: [
'DEVICE_SINGLE_CONTROL',
'DEVICE_VIEW',
'DEVICE_DELETE',
'DEVICE_UPDATE',
'DEVICE_BATCH_CONTROL',
'DEVICE_LOCATION_VIEW',
'DEVICE_LOCATION_UPDATE',
'COMMUNITY_VIEW',
'COMMUNITY_ADD',
'COMMUNITY_UPDATE',
'COMMUNITY_DELETE',
'FIRMWARE_CONTROL',
'FIRMWARE_VIEW',
'SPACE_VIEW',
'SPACE_ADD',
'SPACE_UPDATE',
'SPACE_DELETE',
'SPACE_MODEL_ADD',
'SPACE_MODEL_VIEW',
'SPACE_MODEL_UPDATE',
'SPACE_MODEL_DELETE',
'SPACE_ASSIGN_USER_TO_SPACE',
'SPACE_DELETE_USER_FROM_SPACE',
'SUBSPACE_VIEW',
'SUBSPACE_ADD',
'SUBSPACE_UPDATE',
'SUBSPACE_DELETE',
'SUBSPACE_ASSIGN_DEVICE_TO_SUBSPACE',
'SUBSPACE_DELETE_DEVICE_FROM_SUBSPACE',
'DEVICE_WIZARD_VIEW_DEVICE_WIZARD',
'SUBSPACE_DEVICE_VIEW_DEVICE_IN_SUBSPACE',
'SPACE_DEVICE_VIEW_DEVICE_IN_SPACE',
'SUBSPACE_DEVICE_UPDATE_DEVICE_IN_SUBSPACE',
'SPACE_DEVICE_ASSIGN_DEVICE_TO_SPACE',
'AUTOMATION_VIEW',
'AUTOMATION_ADD',
'AUTOMATION_UPDATE',
'AUTOMATION_DELETE',
'AUTOMATION_CONTROL',
'SCENES_VIEW',
'SCENES_ADD',
'SCENES_UPDATE',
'SCENES_DELETE',
'SCENES_CONTROL',
'VISITOR_PASSWORD_VIEW',
'VISITOR_PASSWORD_ADD',
'VISITOR_PASSWORD_UPDATE',
'VISITOR_PASSWORD_DELETE',
'USER_ADD',
'SPACE_MEMBER_ADD',
],
[RoleType.ADMIN]: [
'DEVICE_SINGLE_CONTROL',
'DEVICE_VIEW',
'DEVICE_DELETE',
'DEVICE_UPDATE',
'DEVICE_BATCH_CONTROL',
'DEVICE_LOCATION_VIEW',
'DEVICE_LOCATION_UPDATE',
'COMMUNITY_VIEW',
'COMMUNITY_ADD',
'COMMUNITY_UPDATE',
'COMMUNITY_DELETE',
'FIRMWARE_CONTROL',
'FIRMWARE_VIEW',
'SPACE_VIEW',
'SPACE_ADD',
'SPACE_UPDATE',
'SPACE_DELETE',
'SPACE_MODEL_ADD',
'SPACE_MODEL_VIEW',
'SPACE_MODEL_UPDATE',
'SPACE_MODEL_DELETE',
'SPACE_ASSIGN_USER_TO_SPACE',
'SPACE_DELETE_USER_FROM_SPACE',
'SUBSPACE_VIEW',
'SUBSPACE_ADD',
'SUBSPACE_UPDATE',
'SUBSPACE_DELETE',
'SUBSPACE_ASSIGN_DEVICE_TO_SUBSPACE',
'SUBSPACE_DELETE_DEVICE_FROM_SUBSPACE',
'DEVICE_WIZARD_VIEW_DEVICE_WIZARD',
'SUBSPACE_DEVICE_VIEW_DEVICE_IN_SUBSPACE',
'SPACE_DEVICE_VIEW_DEVICE_IN_SPACE',
'SUBSPACE_DEVICE_UPDATE_DEVICE_IN_SUBSPACE',
'SPACE_DEVICE_ASSIGN_DEVICE_TO_SPACE',
'AUTOMATION_VIEW',
'AUTOMATION_ADD',
'AUTOMATION_UPDATE',
'AUTOMATION_DELETE',
'AUTOMATION_CONTROL',
'SCENES_VIEW',
'SCENES_ADD',
'SCENES_UPDATE',
'SCENES_DELETE',
'SCENES_CONTROL',
'VISITOR_PASSWORD_VIEW',
'VISITOR_PASSWORD_ADD',
'VISITOR_PASSWORD_UPDATE',
'VISITOR_PASSWORD_DELETE',
'USER_ADD',
'SPACE_MEMBER_ADD',
],
[RoleType.SPACE_MEMBER]: [
'DEVICE_SINGLE_CONTROL',
'DEVICE_VIEW',
'SPACE_VIEW',
'SUBSPACE_VIEW',
'DEVICE_WIZARD_VIEW_DEVICE_WIZARD',
'SUBSPACE_DEVICE_VIEW_DEVICE_IN_SUBSPACE',
'SPACE_DEVICE_VIEW_DEVICE_IN_SPACE',
'AUTOMATION_VIEW',
'AUTOMATION_CONTROL',
'SCENES_VIEW',
'SCENES_CONTROL',
],
[RoleType.SPACE_OWNER]: [
'DEVICE_SINGLE_CONTROL',
'DEVICE_VIEW',
'DEVICE_DELETE',
'DEVICE_UPDATE',
'DEVICE_BATCH_CONTROL',
'DEVICE_LOCATION_VIEW',
'DEVICE_LOCATION_UPDATE',
'FIRMWARE_CONTROL',
'FIRMWARE_VIEW',
'SPACE_VIEW',
'SPACE_ADD',
'SPACE_UPDATE',
'SPACE_DELETE',
'SPACE_ASSIGN_USER_TO_SPACE',
'SPACE_DELETE_USER_FROM_SPACE',
'SUBSPACE_VIEW',
'SUBSPACE_ADD',
'SUBSPACE_UPDATE',
'SUBSPACE_DELETE',
'SUBSPACE_ASSIGN_DEVICE_TO_SUBSPACE',
'SUBSPACE_DELETE_DEVICE_FROM_SUBSPACE',
'DEVICE_WIZARD_VIEW_DEVICE_WIZARD',
'SUBSPACE_DEVICE_VIEW_DEVICE_IN_SUBSPACE',
'SPACE_DEVICE_VIEW_DEVICE_IN_SPACE',
'SUBSPACE_DEVICE_UPDATE_DEVICE_IN_SUBSPACE',
'SPACE_DEVICE_ASSIGN_DEVICE_TO_SPACE',
'AUTOMATION_VIEW',
'AUTOMATION_ADD',
'AUTOMATION_UPDATE',
'AUTOMATION_DELETE',
'AUTOMATION_CONTROL',
'SCENES_VIEW',
'SCENES_ADD',
'SCENES_UPDATE',
'SCENES_DELETE',
'SCENES_CONTROL',
'VISITOR_PASSWORD_VIEW',
'VISITOR_PASSWORD_ADD',
'VISITOR_PASSWORD_UPDATE',
'VISITOR_PASSWORD_DELETE',
'USER_ADD',
'SPACE_MEMBER_ADD',
],
};

View File

@ -1,4 +1,6 @@
export enum RoleType {
SUPER_ADMIN = 'SUPER_ADMIN',
ADMIN = 'ADMIN',
SPACE_OWNER = 'SPACE_OWNER',
SPACE_MEMBER = 'SPACE_MEMBER',
}

View File

@ -0,0 +1,44 @@
<div>
<p>User Agreement</p>
<p>Terms and Conditions</p>
<p><span>Last updated:</span> {{lastUpdated}}</p>
<p>
Please read these Terms and Conditions ("Terms", "Terms and Conditions")
carefully before using the
<a href="{{websiteUrl}}">{{websiteUrl}}</a> website and the {{mobileApp}}
mobile application (the "Service") operated by {{companyName}}.
</p>
<p>
Your access to and use of the Service is conditioned on your acceptance of
and compliance with these Terms. These Terms apply to all visitors, users,
and others who access or use the Service.
</p>
<p>Content</p>
<p>
Our Service allows you to post, link, store, share and otherwise make
available certain information, text, graphics, videos, or other material
("Content"). You are responsible for the Content you post.
</p>
<p>Links To Other Websites</p>
<p>
Our Service may contain links to third-party websites or services that are
not owned or controlled by {{companyName}}.
</p>
<p>
{{companyName}} has no control over, and assumes no responsibility for, the
content, privacy policies, or practices of any third-party websites or
services.
</p>
<p>Changes</p>
<p>
We reserve the right, at our sole discretion, to modify or replace these
Terms at any time. If a revision is material, we will try to provide at
least 30 days' notice prior to any new terms taking effect. What constitutes
a material change will be determined at our sole discretion.
</p>
<p>Contact Us</p>
<p>
If you have any questions about these Terms, please
<a href="mailto:{{contactEmail}}">contact us</a>.
</p>
</div>

View File

@ -0,0 +1,7 @@
export const termsAndConditionsData = {
lastUpdated: '25/01/2025',
websiteUrl: 'https://www.Syncrow.ae',
mobileApp: 'Syncrow Mobile App',
companyName: 'Syncrow',
contactEmail: 'contact@Syncrow.ae',
};

View File

@ -0,0 +1,4 @@
export enum UserStatusEnum {
ACTIVE = 'active',
INVITED = 'invited',
}

View File

@ -12,10 +12,10 @@ import {
SpaceEntity,
SpaceLinkEntity,
SubspaceEntity,
TagEntity,
} from '../modules/space/entities';
import { UserSpaceEntity } from '../modules/user/entities';
import { DeviceUserPermissionEntity } from '../modules/device/entities';
import { UserRoleEntity } from '../modules/user/entities';
import { RoleTypeEntity } from '../modules/role-type/entities';
import { UserNotificationEntity } from '../modules/user/entities';
import { DeviceNotificationEntity } from '../modules/device/entities';
@ -26,8 +26,18 @@ import { CommunityEntity } from '../modules/community/entities';
import { DeviceStatusLogEntity } from '../modules/device-status-log/entities';
import { SceneEntity, SceneIconEntity } from '../modules/scene/entities';
import { SceneDeviceEntity } from '../modules/scene-device/entities';
import { SpaceProductEntity } from '../modules/space/entities/space-product.entity';
import { ProjectEntity } from '../modules/project/entities';
import {
SpaceModelEntity,
SubspaceModelEntity,
TagModel,
} from '../modules/space-model/entities';
import {
InviteUserEntity,
InviteUserSpaceEntity,
} from '../modules/Invite-user/entities';
import { InviteSpaceEntity } from '../modules/space/entities/invite-space.entity';
import { AutomationEntity } from '../modules/automation/entities';
@Module({
imports: [
TypeOrmModule.forRootAsync({
@ -42,6 +52,7 @@ import { SpaceProductEntity } from '../modules/space/entities/space-product.enti
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_NAME'),
entities: [
ProjectEntity,
UserEntity,
UserSessionEntity,
UserOtpEntity,
@ -53,10 +64,9 @@ import { SpaceProductEntity } from '../modules/space/entities/space-product.enti
SpaceEntity,
SpaceLinkEntity,
SubspaceEntity,
SpaceProductEntity,
TagEntity,
UserSpaceEntity,
DeviceUserPermissionEntity,
UserRoleEntity,
RoleTypeEntity,
UserNotificationEntity,
DeviceNotificationEntity,
@ -67,6 +77,13 @@ import { SpaceProductEntity } from '../modules/space/entities/space-product.enti
SceneEntity,
SceneIconEntity,
SceneDeviceEntity,
SpaceModelEntity,
SubspaceModelEntity,
TagModel,
InviteUserEntity,
InviteUserSpaceEntity,
InviteSpaceEntity,
AutomationEntity,
],
namingStrategy: new SnakeNamingStrategy(),
synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))),

View File

@ -187,19 +187,19 @@ export class DeviceStatusFirebaseService {
code,
value,
}));
const newLog = this.deviceStatusLogRepository.create({
deviceId: addDeviceStatusDto.deviceUuid,
deviceTuyaId: addDeviceStatusDto.deviceTuyaUuid,
productId: addDeviceStatusDto.log.productId,
log: addDeviceStatusDto.log,
code: existingData.status[0].code,
value: existingData.status[0].value,
eventId: addDeviceStatusDto.log.dataId,
eventTime: new Date(
addDeviceStatusDto.log.properties[0].time,
).toISOString(),
const newLogs = addDeviceStatusDto.log.properties.map((property) => {
return this.deviceStatusLogRepository.create({
deviceId: addDeviceStatusDto.deviceUuid,
deviceTuyaId: addDeviceStatusDto.deviceTuyaUuid,
productId: addDeviceStatusDto.log.productId,
log: addDeviceStatusDto.log,
code: property.code,
value: property.value,
eventId: addDeviceStatusDto.log.dataId,
eventTime: new Date(property.time).toISOString(),
});
});
await this.deviceStatusLogRepository.save(newLog);
await this.deviceStatusLogRepository.save(newLogs);
// Save the updated data to Firebase
await set(dataRef, existingData);

View File

@ -0,0 +1,12 @@
export function removeCircularReferences() {
const seen = new WeakSet();
return (key: string, value: any) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return undefined; // Skip circular reference
}
seen.add(value);
}
return value;
};
}

View File

@ -6,6 +6,7 @@ import {
ConvertedAction,
TuyaResponseInterface,
} from '../interfaces';
import { GetDeviceDetailsFunctionsStatusInterface } from 'src/device/interfaces/get.device.interface';
@Injectable()
export class TuyaService {
@ -284,4 +285,24 @@ export class TuyaService {
);
}
}
async getDevicesInstructionStatusTuya(
deviceUuid: string,
): Promise<GetDeviceDetailsFunctionsStatusInterface> {
try {
const path = `/v1.0/iot-03/devices/status`;
const response = await this.tuya.request({
method: 'GET',
path,
query: {
device_ids: deviceUuid,
},
});
return response as GetDeviceDetailsFunctionsStatusInterface;
} catch (error) {
throw new HttpException(
'Error fetching device functions status from Tuya',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}

View File

@ -1,4 +1,4 @@
import { FindManyOptions, Repository } from 'typeorm';
import { FindManyOptions, Repository, SelectQueryBuilder } from 'typeorm';
import { InternalServerErrorException } from '@nestjs/common';
import { BaseResponseDto } from '../dto/base.response.dto';
import { PageResponseDto } from '../dto/pagination.response.dto';
@ -31,7 +31,7 @@ interface FindAllQueryWithDefaults extends CustomFindAllQuery {
function getDefaultQueryOptions(
query: Partial<TypeORMCustomModelFindAllQuery>,
): FindManyOptions & FindAllQueryWithDefaults {
const { page, size, includeDisable, modelName, ...rest } = query;
const { page, size, includeDisable, include, modelName, ...rest } = query;
// Set default if undefined or null
const returnPage = page ? Number(page) : 1;
@ -48,7 +48,9 @@ function getDefaultQueryOptions(
},
page: returnPage,
size: returnSize,
include: include || undefined,
includeDisable: returnIncludeDisable,
modelName: modelName || query.modelName, // Ensure modelName is passed through
};
}
@ -68,8 +70,8 @@ export function TypeORMCustomModel(repository: Repository<any>) {
return Object.assign(repository, {
findAll: async function (
query: Partial<TypeORMCustomModelFindAllQuery>,
customQueryBuilder?: SelectQueryBuilder<any>,
): Promise<TypeORMCustomModelFindAllResponse> {
// Extract values from the query
const {
page = 1,
size = 10,
@ -80,12 +82,7 @@ export function TypeORMCustomModel(repository: Repository<any>) {
select,
} = getDefaultQueryOptions(query);
// Ensure modelName is set before proceeding
if (!modelName) {
console.error(
'modelName is missing after getDefaultQueryOptions:',
query,
);
throw new InternalServerErrorException(
`[TypeORMCustomModel] Cannot findAll with unknown modelName`,
);
@ -94,20 +91,44 @@ export function TypeORMCustomModel(repository: Repository<any>) {
const skip = (page - 1) * size;
const order = buildTypeORMSortQuery(sort);
const relations = buildTypeORMIncludeQuery(modelName, include);
// Use the where clause directly, without wrapping it under 'where'
const whereClause = buildTypeORMWhereClause({ where });
console.log('Final where clause:', whereClause);
// Ensure the whereClause is passed directly to findAndCount
const [data, count] = await repository.findAndCount({
where: whereClause,
take: size,
skip: skip,
order: order,
select: select,
relations: relations,
});
let data: any[] = [];
let count = 0;
if (customQueryBuilder) {
const qb = customQueryBuilder.skip(skip).take(size);
if (order) {
Object.keys(order).forEach((key) => {
qb.addOrderBy(key, order[key]);
});
}
if (whereClause) {
qb.andWhere(whereClause); // Use .andWhere instead of .where to avoid overwriting conditions
}
if (select) {
const selectColumns = Array.isArray(select)
? select
: Object.keys(select).map(
(key) => `${customQueryBuilder.alias}.${key}`,
);
qb.select(selectColumns as string[]);
}
[data, count] = await qb.getManyAndCount();
} else {
[data, count] = await repository.findAndCount({
where: whereClause,
take: size,
skip: skip,
order: order,
select: select,
relations: relations,
});
}
const paginationResponseDto = getPaginationResponseDto(count, page, size);
const baseResponseDto: BaseResponseDto = {

View File

@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { InviteUserEntity, InviteUserSpaceEntity } from './entities';
@Module({
providers: [],
exports: [],
controllers: [],
imports: [
TypeOrmModule.forFeature([InviteUserEntity, InviteUserSpaceEntity]),
],
})
export class InviteUserRepositoryModule {}

View File

@ -0,0 +1,50 @@
import { RoleType } from '@app/common/constants/role.type.enum';
import { UserStatusEnum } from '@app/common/constants/user-status.enum';
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
export class InviteUserDto {
@IsString()
@IsNotEmpty()
public uuid: string;
@IsString()
@IsNotEmpty()
public email: string;
@IsString()
@IsNotEmpty()
public jobTitle: string;
@IsEnum(UserStatusEnum)
@IsNotEmpty()
public status: UserStatusEnum;
@IsString()
@IsNotEmpty()
public firstName: string;
@IsString()
@IsNotEmpty()
public lastName: string;
@IsEnum(RoleType)
@IsNotEmpty()
public invitedBy: RoleType;
}
export class InviteUserSpaceDto {
@IsString()
@IsNotEmpty()
public uuid: string;
@IsString()
@IsNotEmpty()
public inviteUserUuid: string;
@IsString()
@IsNotEmpty()
public spaceUuid: string;
@IsString()
@IsNotEmpty()
public invitationCode: string;
}

View File

@ -0,0 +1 @@
export * from './Invite-user.dto';

View File

@ -0,0 +1,126 @@
import {
Column,
Entity,
JoinColumn,
ManyToOne,
OneToMany,
OneToOne,
Unique,
} from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { RoleTypeEntity } from '../../role-type/entities';
import { UserStatusEnum } from '@app/common/constants/user-status.enum';
import { UserEntity } from '../../user/entities';
import { SpaceEntity } from '../../space/entities';
import { RoleType } from '@app/common/constants/role.type.enum';
import { InviteUserDto, InviteUserSpaceDto } from '../dtos';
import { ProjectEntity } from '../../project/entities';
@Entity({ name: 'invite-user' })
@Unique(['email', 'project'])
export class InviteUserEntity extends AbstractEntity<InviteUserDto> {
@Column({
type: 'uuid',
default: () => 'gen_random_uuid()',
nullable: false,
})
public uuid: string;
@Column({
nullable: false,
})
email: string;
@Column({
nullable: true,
})
jobTitle: string;
@Column({
nullable: false,
enum: Object.values(UserStatusEnum),
})
status: string;
@Column()
public firstName: string;
@Column({
nullable: false,
})
public lastName: string;
@Column({
nullable: true,
})
public phoneNumber: string;
@Column({
nullable: false,
default: true,
})
public isActive: boolean;
@Column({
nullable: false,
default: true,
})
public isEnabled: boolean;
@Column({
nullable: false,
unique: true,
})
public invitationCode: string;
@Column({
nullable: false,
enum: Object.values(RoleType),
})
public invitedBy: string;
@ManyToOne(() => RoleTypeEntity, (roleType) => roleType.invitedUsers, {
nullable: false,
onDelete: 'CASCADE',
})
public roleType: RoleTypeEntity;
@OneToOne(() => UserEntity, (user) => user.inviteUser, { nullable: true })
@JoinColumn({ name: 'user_uuid' })
user: UserEntity;
@OneToMany(
() => InviteUserSpaceEntity,
(inviteUserSpace) => inviteUserSpace.inviteUser,
)
spaces: InviteUserSpaceEntity[];
@ManyToOne(() => ProjectEntity, (project) => project.invitedUsers, {
nullable: true,
})
@JoinColumn({ name: 'project_uuid' })
public project: ProjectEntity;
constructor(partial: Partial<InviteUserEntity>) {
super();
Object.assign(this, partial);
}
}
@Entity({ name: 'invite-user-space' })
@Unique(['inviteUser', 'space'])
export class InviteUserSpaceEntity extends AbstractEntity<InviteUserSpaceDto> {
@Column({
type: 'uuid',
default: () => 'gen_random_uuid()',
nullable: false,
})
public uuid: string;
@ManyToOne(() => InviteUserEntity, (inviteUser) => inviteUser.spaces)
@JoinColumn({ name: 'invite_user_uuid' })
public inviteUser: InviteUserEntity;
@ManyToOne(() => SpaceEntity, (space) => space.invitedUsers)
@JoinColumn({ name: 'space_uuid' })
public space: SpaceEntity;
constructor(partial: Partial<InviteUserSpaceEntity>) {
super();
Object.assign(this, partial);
}
}

View File

@ -0,0 +1 @@
export * from './Invite-user.entity';

View File

@ -0,0 +1 @@
export * from './Invite-user.repository.module';

View File

@ -0,0 +1 @@
export * from './invite-user.repository';

View File

@ -0,0 +1,16 @@
import { DataSource, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { InviteUserEntity, InviteUserSpaceEntity } from '../entities';
@Injectable()
export class InviteUserRepository extends Repository<InviteUserEntity> {
constructor(private dataSource: DataSource) {
super(InviteUserEntity, dataSource.createEntityManager());
}
}
@Injectable()
export class InviteUserSpaceRepository extends Repository<InviteUserSpaceEntity> {
constructor(private dataSource: DataSource) {
super(InviteUserSpaceEntity, dataSource.createEntityManager());
}
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AutomationEntity } from './entities';
@Module({
providers: [],
exports: [],
controllers: [],
imports: [TypeOrmModule.forFeature([AutomationEntity])],
})
export class AutomationRepositoryModule {}

View File

@ -0,0 +1,14 @@
import { IsNotEmpty, IsString } from 'class-validator';
export class AutomationDto {
@IsString()
@IsNotEmpty()
public uuid: string;
@IsString()
@IsNotEmpty()
public automationTuyaUuid: string;
@IsString()
@IsNotEmpty()
public spaceUuid: string;
}

View File

@ -0,0 +1 @@
export * from './automation.dto';

View File

@ -0,0 +1,37 @@
import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm';
import { AutomationDto } from '../dtos';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { SpaceEntity } from '../../space/entities';
@Entity({ name: 'automation' })
export class AutomationEntity extends AbstractEntity<AutomationDto> {
@Column({
type: 'uuid',
default: () => 'gen_random_uuid()',
nullable: false,
})
public uuid: string;
@Column({
nullable: false,
})
automationTuyaUuid: string;
@ManyToOne(() => SpaceEntity, (space) => space.scenes, {
nullable: false,
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'space_uuid' })
space: SpaceEntity;
@Column({
nullable: false,
default: false,
})
public disabled: boolean;
constructor(partial: Partial<AutomationEntity>) {
super();
Object.assign(this, partial);
}
}

View File

@ -0,0 +1 @@
export * from './automation.entity';

View File

@ -0,0 +1,10 @@
import { DataSource, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { AutomationEntity } from '../entities';
@Injectable()
export class AutomationRepository extends Repository<AutomationEntity> {
constructor(private dataSource: DataSource) {
super(AutomationEntity, dataSource.createEntityManager());
}
}

View File

@ -0,0 +1 @@
export * from './automation.repository';

View File

@ -1,7 +1,8 @@
import { Column, Entity, OneToMany, Unique } from 'typeorm';
import { Column, Entity, ManyToOne, OneToMany, Unique } from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { CommunityDto } from '../dtos';
import { SpaceEntity } from '../../space/entities';
import { ProjectEntity } from '../../project/entities';
@Entity({ name: 'community' })
@Unique(['name'])
@ -31,4 +32,9 @@ export class CommunityEntity extends AbstractEntity<CommunityDto> {
nullable: true,
})
externalId: string;
@ManyToOne(() => ProjectEntity, (project) => project.communities, {
nullable: false,
})
project: ProjectEntity;
}

View File

@ -20,7 +20,7 @@ export class DeviceStatusLogEntity {
})
eventFrom: SourceType;
@Column({ type: 'text' })
@Column({ type: 'uuid' })
deviceId: string;
@Column({ type: 'text' })

View File

@ -6,10 +6,11 @@ import {
Unique,
Index,
JoinColumn,
OneToOne,
} from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { DeviceDto, DeviceUserPermissionDto } from '../dtos/device.dto';
import { SpaceEntity, SubspaceEntity } from '../../space/entities';
import { SpaceEntity, SubspaceEntity, TagEntity } from '../../space/entities';
import { ProductEntity } from '../../product/entities';
import { UserEntity } from '../../user/entities';
import { DeviceNotificationDto } from '../dtos';
@ -74,6 +75,11 @@ export class DeviceEntity extends AbstractEntity<DeviceDto> {
@OneToMany(() => SceneDeviceEntity, (sceneDevice) => sceneDevice.device, {})
sceneDevices: SceneDeviceEntity[];
@OneToOne(() => TagEntity, (tag) => tag.device, {
nullable: true,
})
tag: TagEntity;
constructor(partial: Partial<DeviceEntity>) {
super();
Object.assign(this, partial);
@ -102,6 +108,7 @@ export class DeviceNotificationEntity extends AbstractEntity<DeviceNotificationD
nullable: false,
})
user: UserEntity;
constructor(partial: Partial<DeviceNotificationEntity>) {
super();
Object.assign(this, partial);

View File

@ -2,8 +2,8 @@ import { Column, Entity, OneToMany } from 'typeorm';
import { ProductDto } from '../dtos';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { DeviceEntity } from '../../device/entities';
import { SpaceProductEntity } from '../../space/entities/space-product.entity';
import { TagModel } from '../../space-model';
import { TagEntity } from '../../space/entities/tag.entity';
@Entity({ name: 'product' })
export class ProductEntity extends AbstractEntity<ProductDto> {
@Column({
@ -27,8 +27,11 @@ export class ProductEntity extends AbstractEntity<ProductDto> {
})
public prodType: string;
@OneToMany(() => SpaceProductEntity, (spaceProduct) => spaceProduct.product)
spaceProducts: SpaceProductEntity[];
@OneToMany(() => TagModel, (tag) => tag.product)
tagModels: TagModel[];
@OneToMany(() => TagEntity, (tag) => tag.product)
tags: TagEntity[];
@OneToMany(
() => DeviceEntity,

View File

@ -0,0 +1 @@
export * from './project.dto';

View File

@ -0,0 +1,15 @@
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class ProjectDto {
@IsString()
@IsNotEmpty()
uuid: string;
@IsString()
@IsNotEmpty()
name: string;
@IsString()
@IsOptional()
public description?: string;
}

View File

@ -0,0 +1 @@
export * from './project.entity';

View File

@ -0,0 +1,43 @@
import { Entity, Column, Unique, OneToMany } from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { ProjectDto } from '../dtos';
import { CommunityEntity } from '../../community/entities';
import { SpaceModelEntity } from '../../space-model';
import { UserEntity } from '../../user/entities';
import { InviteUserEntity } from '../../Invite-user/entities';
@Entity({ name: 'project' })
@Unique(['name'])
export class ProjectEntity extends AbstractEntity<ProjectDto> {
@Column({
type: 'uuid',
default: () => 'gen_random_uuid()',
nullable: false,
})
public uuid: string;
@Column({
nullable: false,
})
public name: string;
@Column({ length: 255, nullable: true })
description: string;
@OneToMany(() => SpaceModelEntity, (spaceModel) => spaceModel.project)
public spaceModels: SpaceModelEntity[];
@OneToMany(() => CommunityEntity, (community) => community.project)
communities: CommunityEntity[];
@OneToMany(() => UserEntity, (user) => user.project)
public users: UserEntity[];
@OneToMany(() => InviteUserEntity, (inviteUser) => inviteUser.project)
public invitedUsers: InviteUserEntity[];
constructor(partial: Partial<ProjectEntity>) {
super();
Object.assign(this, partial);
}
}

View File

@ -0,0 +1 @@
export * from './project.repository.module';

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ProjectEntity } from './entities';
@Module({
providers: [],
exports: [],
controllers: [],
imports: [TypeOrmModule.forFeature([ProjectEntity])],
})
export class ProjectRepositoryModule {}

View File

@ -0,0 +1 @@
export * from './project.repository';

View File

@ -0,0 +1,10 @@
import { DataSource, Repository } from 'typeorm';
import { ProjectEntity } from '../entities';
import { Injectable } from '@nestjs/common';
@Injectable()
export class ProjectRepository extends Repository<ProjectEntity> {
constructor(private dataSource: DataSource) {
super(ProjectEntity, dataSource.createEntityManager());
}
}

View File

@ -2,7 +2,8 @@ import { Column, Entity, OneToMany, Unique } from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { RoleTypeDto } from '../dtos/role.type.dto';
import { RoleType } from '@app/common/constants/role.type.enum';
import { UserRoleEntity } from '../../user/entities';
import { UserEntity } from '../../user/entities';
import { InviteUserEntity } from '../../Invite-user/entities';
@Entity({ name: 'role-type' })
@Unique(['type'])
@ -12,10 +13,14 @@ export class RoleTypeEntity extends AbstractEntity<RoleTypeDto> {
enum: Object.values(RoleType),
})
type: string;
@OneToMany(() => UserRoleEntity, (role) => role.roleType, {
@OneToMany(() => UserEntity, (inviteUser) => inviteUser.roleType, {
nullable: true,
})
roles: UserRoleEntity[];
users: UserEntity[];
@OneToMany(() => InviteUserEntity, (inviteUser) => inviteUser.roleType, {
nullable: true,
})
invitedUsers: InviteUserEntity[];
constructor(partial: Partial<RoleTypeEntity>) {
super();
Object.assign(this, partial);

View File

@ -44,6 +44,12 @@ export class SceneDeviceEntity extends AbstractEntity<SceneDeviceDto> {
@JoinColumn({ name: 'scene_uuid' })
scene: SceneEntity;
@Column({
nullable: false,
default: false,
})
public disabled: boolean;
constructor(partial: Partial<SceneDeviceEntity>) {
super();
Object.assign(this, partial);

View File

@ -59,6 +59,12 @@ export class SceneEntity extends AbstractEntity<SceneDto> {
@JoinColumn({ name: 'space_uuid' })
space: SpaceEntity;
@Column({
nullable: false,
default: false,
})
public disabled: boolean;
@ManyToOne(() => SceneIconEntity, (icon) => icon.scenesIconEntity, {
nullable: false,
})

View File

@ -0,0 +1,2 @@
export * from './subspace-model';
export * from './space-model.dto';

View File

@ -0,0 +1,15 @@
import { IsString, IsNotEmpty } from 'class-validator';
export class SpaceModelDto {
@IsString()
@IsNotEmpty()
public uuid: string;
@IsString()
@IsNotEmpty()
public spaceModelName: string;
@IsString()
@IsNotEmpty()
projectUuid: string;
}

View File

@ -0,0 +1 @@
export * from './subspace-model.dto';

View File

@ -0,0 +1,15 @@
import { IsString, IsNotEmpty } from 'class-validator';
export class SubSpaceModelDto {
@IsString()
@IsNotEmpty()
public uuid: string;
@IsString()
@IsNotEmpty()
public subSpaceModelName: string;
@IsString()
@IsNotEmpty()
spaceModelUuid: string;
}

View File

@ -0,0 +1,21 @@
import { IsNotEmpty, IsString } from 'class-validator';
export class TagModelDto {
@IsString()
@IsNotEmpty()
public uuid: string;
@IsString()
@IsNotEmpty()
public name: string;
@IsString()
@IsNotEmpty()
public productUuid: string;
@IsString()
spaceModelUuid: string;
@IsString()
subspaceModelUuid: string;
}

View File

@ -0,0 +1,3 @@
export * from './space-model.entity';
export * from './subspace-model';
export * from './tag-model.entity';

View File

@ -0,0 +1,58 @@
import { Entity, Column, OneToMany, ManyToOne, JoinColumn } from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { SpaceModelDto } from '../dtos';
import { SubspaceModelEntity } from './subspace-model';
import { ProjectEntity } from '../../project/entities';
import { SpaceEntity } from '../../space/entities';
import { TagModel } from './tag-model.entity';
@Entity({ name: 'space-model' })
export class SpaceModelEntity extends AbstractEntity<SpaceModelDto> {
@Column({
type: 'uuid',
default: () => 'gen_random_uuid()',
nullable: false,
})
public uuid: string;
@Column({
nullable: false,
})
public modelName: string;
@Column({
nullable: false,
default: false,
})
public disabled: boolean;
@ManyToOne(() => ProjectEntity, (project) => project.spaceModels, {
nullable: false,
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'project_uuid' })
public project: ProjectEntity;
@OneToMany(
() => SubspaceModelEntity,
(subspaceModel) => subspaceModel.spaceModel,
{
cascade: true,
nullable: true,
},
)
public subspaceModels: SubspaceModelEntity[];
@OneToMany(() => SpaceEntity, (space) => space.spaceModel, {
cascade: true,
})
public spaces: SpaceEntity[];
@OneToMany(() => TagModel, (tag) => tag.spaceModel)
tags: TagModel[];
constructor(partial: Partial<SpaceModelEntity>) {
super();
Object.assign(this, partial);
}
}

View File

@ -0,0 +1,2 @@
export * from './subspace-model.entity';

View File

@ -0,0 +1,45 @@
import { AbstractEntity } from '@app/common/modules/abstract/entities/abstract.entity';
import { Column, Entity, ManyToOne, OneToMany, Unique } from 'typeorm';
import { SubSpaceModelDto } from '../../dtos';
import { SpaceModelEntity } from '../space-model.entity';
import { SubspaceEntity } from '@app/common/modules/space/entities';
import { TagModel } from '../tag-model.entity';
@Entity({ name: 'subspace-model' })
export class SubspaceModelEntity extends AbstractEntity<SubSpaceModelDto> {
@Column({
type: 'uuid',
default: () => 'gen_random_uuid()',
nullable: false,
})
public uuid: string;
@Column({
nullable: false,
})
public subspaceName: string;
@ManyToOne(
() => SpaceModelEntity,
(spaceModel) => spaceModel.subspaceModels,
{
nullable: false,
onDelete: 'CASCADE',
},
)
public spaceModel: SpaceModelEntity;
@OneToMany(() => SubspaceEntity, (subspace) => subspace.subSpaceModel, {
cascade: true,
})
public subspaceModel: SubspaceEntity[];
@Column({
nullable: false,
default: false,
})
public disabled: boolean;
@OneToMany(() => TagModel, (tag) => tag.subspaceModel)
tags: TagModel[];
}

View File

@ -0,0 +1,38 @@
import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { TagModelDto } from '../dtos/tag-model.dto';
import { SpaceModelEntity } from './space-model.entity';
import { SubspaceModelEntity } from './subspace-model';
import { ProductEntity } from '../../product/entities';
import { TagEntity } from '../../space/entities/tag.entity';
@Entity({ name: 'tag_model' })
export class TagModel extends AbstractEntity<TagModelDto> {
@Column({ type: 'varchar', length: 255 })
tag: string;
@ManyToOne(() => ProductEntity, (product) => product.tagModels, {
nullable: false,
})
@JoinColumn({ name: 'product_id' })
product: ProductEntity;
@ManyToOne(() => SpaceModelEntity, (space) => space.tags, { nullable: true })
@JoinColumn({ name: 'space_model_id' })
spaceModel: SpaceModelEntity;
@ManyToOne(() => SubspaceModelEntity, (subspace) => subspace.tags, {
nullable: true,
})
@JoinColumn({ name: 'subspace_model_id' })
subspaceModel: SubspaceModelEntity;
@Column({
nullable: false,
default: false,
})
public disabled: boolean;
@OneToMany(() => TagEntity, (tag) => tag.model)
tags: TagEntity[];
}

View File

@ -0,0 +1,3 @@
export * from './space-model.repository.module';
export * from './entities';
export * from './repositories';

View File

@ -0,0 +1 @@
export * from './space-model.repository';

View File

@ -0,0 +1,23 @@
import { DataSource, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { SpaceModelEntity, SubspaceModelEntity, TagModel } from '../entities';
@Injectable()
export class SpaceModelRepository extends Repository<SpaceModelEntity> {
constructor(private dataSource: DataSource) {
super(SpaceModelEntity, dataSource.createEntityManager());
}
}
@Injectable()
export class SubspaceModelRepository extends Repository<SubspaceModelEntity> {
constructor(private dataSource: DataSource) {
super(SubspaceModelEntity, dataSource.createEntityManager());
}
}
@Injectable()
export class TagModelRepository extends Repository<TagModel> {
constructor(private dataSource: DataSource) {
super(TagModel, dataSource.createEntityManager());
}
}

View File

@ -0,0 +1,13 @@
import { TypeOrmModule } from '@nestjs/typeorm';
import { SpaceModelEntity, SubspaceModelEntity, TagModel } from './entities';
import { Module } from '@nestjs/common';
@Module({
providers: [],
exports: [],
controllers: [],
imports: [
TypeOrmModule.forFeature([SpaceModelEntity, SubspaceModelEntity, TagModel]),
],
})
export class SpaceModelRepositoryModule {}

View File

@ -1,2 +1,3 @@
export * from './space.dto';
export * from './subspace.dto';
export * from './tag.dto';

View File

@ -0,0 +1,21 @@
import { IsNotEmpty, IsString } from 'class-validator';
export class TagDto {
@IsString()
@IsNotEmpty()
public uuid: string;
@IsString()
@IsNotEmpty()
public name: string;
@IsString()
@IsNotEmpty()
public productUuid: string;
@IsString()
spaceUuid: string;
@IsString()
subspaceUuid: string;
}

View File

@ -1,3 +1,4 @@
export * from './space.entity';
export * from './subspace.entity';
export * from './subspace';
export * from './space-link.entity';
export * from './tag.entity';

View File

@ -0,0 +1,35 @@
import { Column, Entity, ManyToOne, Unique } from 'typeorm';
import { UserSpaceDto } from '../../user/dtos';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { SpaceEntity } from './space.entity';
@Entity({ name: 'invite-space' })
@Unique(['invitationCode'])
export class InviteSpaceEntity extends AbstractEntity<UserSpaceDto> {
@Column({
type: 'uuid',
default: () => 'gen_random_uuid()', // Use gen_random_uuid() for default value
nullable: false,
})
public uuid: string;
@ManyToOne(() => SpaceEntity, (space) => space.userSpaces, {
nullable: false,
})
space: SpaceEntity;
@Column({
nullable: true,
})
public invitationCode: string;
@Column({
nullable: false,
default: true,
})
public isActive: boolean;
constructor(partial: Partial<InviteSpaceEntity>) {
super();
Object.assign(this, partial);
}
}

View File

@ -13,6 +13,12 @@ export class SpaceLinkEntity extends AbstractEntity {
@JoinColumn({ name: 'end_space_id' })
public endSpace: SpaceEntity;
@Column({
nullable: false,
default: false,
})
public disabled: boolean;
@Column({
nullable: false,
enum: Object.values(Direction),

View File

@ -1,32 +0,0 @@
import { Column, Entity, ManyToOne, JoinColumn } from 'typeorm';
import { SpaceEntity } from './space.entity';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { ProductEntity } from '../../product/entities';
@Entity({ name: 'space-product' })
export class SpaceProductEntity extends AbstractEntity<SpaceProductEntity> {
@ManyToOne(() => SpaceEntity, (space) => space.spaceProducts, {
nullable: false,
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'space_uuid' })
space: SpaceEntity;
@ManyToOne(() => ProductEntity, (product) => product.spaceProducts, {
nullable: false,
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'product_uuid' })
product: ProductEntity;
@Column({
nullable: false,
type: 'int',
})
productCount: number;
constructor(partial: Partial<SpaceProductEntity>) {
super();
Object.assign(this, partial);
}
}

View File

@ -1,23 +1,17 @@
import {
Column,
Entity,
JoinColumn,
ManyToOne,
OneToMany,
Unique,
} from 'typeorm';
import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm';
import { SpaceDto } from '../dtos';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { UserSpaceEntity } from '../../user/entities';
import { DeviceEntity } from '../../device/entities';
import { CommunityEntity } from '../../community/entities';
import { SubspaceEntity } from './subspace.entity';
import { SubspaceEntity } from './subspace';
import { SpaceLinkEntity } from './space-link.entity';
import { SpaceProductEntity } from './space-product.entity';
import { SceneEntity } from '../../scene/entities';
import { SpaceModelEntity } from '../../space-model';
import { InviteUserSpaceEntity } from '../../Invite-user/entities';
import { TagEntity } from './tag.entity';
@Entity({ name: 'space' })
@Unique(['invitationCode'])
export class SpaceEntity extends AbstractEntity<SpaceDto> {
@Column({
type: 'uuid',
@ -42,10 +36,6 @@ export class SpaceEntity extends AbstractEntity<SpaceDto> {
@JoinColumn({ name: 'community_id' })
community: CommunityEntity;
@Column({
nullable: true,
})
public invitationCode: string;
@ManyToOne(() => SpaceEntity, (space) => space.children, { nullable: true })
parent: SpaceEntity;
@ -58,6 +48,12 @@ export class SpaceEntity extends AbstractEntity<SpaceDto> {
@OneToMany(() => UserSpaceEntity, (userSpace) => userSpace.space)
userSpaces: UserSpaceEntity[];
@Column({
nullable: false,
default: false,
})
public disabled: boolean;
@OneToMany(() => SubspaceEntity, (subspace) => subspace.space, {
nullable: true,
})
@ -92,12 +88,23 @@ export class SpaceEntity extends AbstractEntity<SpaceDto> {
})
public icon: string;
@OneToMany(() => SpaceProductEntity, (spaceProduct) => spaceProduct.space)
spaceProducts: SpaceProductEntity[];
@OneToMany(() => SceneEntity, (scene) => scene.space)
scenes: SceneEntity[];
@ManyToOne(() => SpaceModelEntity, (spaceModel) => spaceModel.spaces, {
nullable: true,
})
spaceModel?: SpaceModelEntity;
@OneToMany(
() => InviteUserSpaceEntity,
(inviteUserSpace) => inviteUserSpace.space,
)
invitedUsers: InviteUserSpaceEntity[];
@OneToMany(() => TagEntity, (tag) => tag.space)
tags: TagEntity[];
constructor(partial: Partial<SpaceEntity>) {
super();
Object.assign(this, partial);

View File

@ -1,37 +0,0 @@
import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { DeviceEntity } from '../../device/entities';
import { SpaceEntity } from './space.entity';
import { SubspaceDto } from '../dtos';
@Entity({ name: 'subspace' })
export class SubspaceEntity extends AbstractEntity<SubspaceDto> {
@Column({
type: 'uuid',
default: () => 'gen_random_uuid()',
nullable: false,
})
public uuid: string;
@Column({
nullable: false,
})
public subspaceName: string;
@ManyToOne(() => SpaceEntity, (space) => space.subspaces, {
nullable: false,
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'space_id' })
space: SpaceEntity;
@OneToMany(() => DeviceEntity, (device) => device.subspace, {
nullable: true,
})
devices: DeviceEntity[];
constructor(partial: Partial<SubspaceEntity>) {
super();
Object.assign(this, partial);
}
}

View File

@ -0,0 +1 @@
export * from './subspace.entity';

View File

@ -0,0 +1,52 @@
import { AbstractEntity } from '@app/common/modules/abstract/entities/abstract.entity';
import { DeviceEntity } from '@app/common/modules/device/entities';
import { SubspaceModelEntity } from '@app/common/modules/space-model';
import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm';
import { SubspaceDto } from '../../dtos';
import { SpaceEntity } from '../space.entity';
import { TagEntity } from '../tag.entity';
@Entity({ name: 'subspace' })
export class SubspaceEntity extends AbstractEntity<SubspaceDto> {
@Column({
type: 'uuid',
default: () => 'gen_random_uuid()',
nullable: false,
})
public uuid: string;
@Column({
nullable: false,
})
public subspaceName: string;
@ManyToOne(() => SpaceEntity, (space) => space.subspaces, {
nullable: false,
})
@JoinColumn({ name: 'space_uuid' })
space: SpaceEntity;
@Column({
nullable: false,
default: false,
})
public disabled: boolean;
@OneToMany(() => DeviceEntity, (device) => device.subspace, {
nullable: true,
})
devices: DeviceEntity[];
@ManyToOne(() => SubspaceModelEntity, (subspace) => subspace.subspaceModel, {
nullable: true,
})
subSpaceModel?: SubspaceModelEntity;
@OneToMany(() => TagEntity, (tag) => tag.subspace)
tags: TagEntity[];
constructor(partial: Partial<SubspaceEntity>) {
super();
Object.assign(this, partial);
}
}

View File

@ -0,0 +1,45 @@
import { Entity, Column, ManyToOne, JoinColumn, OneToOne } from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { ProductEntity } from '../../product/entities';
import { TagDto } from '../dtos';
import { TagModel } from '../../space-model/entities/tag-model.entity';
import { SpaceEntity } from './space.entity';
import { SubspaceEntity } from './subspace';
import { DeviceEntity } from '../../device/entities';
@Entity({ name: 'tag' })
export class TagEntity extends AbstractEntity<TagDto> {
@Column({ type: 'varchar', length: 255 })
tag: string;
@ManyToOne(() => TagModel, (model) => model.tags, {
nullable: true,
})
model: TagModel;
@ManyToOne(() => ProductEntity, (product) => product.tags, {
nullable: false,
})
product: ProductEntity;
@ManyToOne(() => SpaceEntity, (space) => space.tags, { nullable: true })
space: SpaceEntity;
@ManyToOne(() => SubspaceEntity, (subspace) => subspace.tags, {
nullable: true,
})
@JoinColumn({ name: 'subspace_id' })
subspace: SubspaceEntity;
@Column({
nullable: false,
default: false,
})
public disabled: boolean;
@OneToOne(() => DeviceEntity, (device) => device.tag, {
nullable: true,
})
@JoinColumn({ name: 'device_id' })
device: DeviceEntity;
}

View File

@ -0,0 +1,4 @@
export * from './dtos';
export * from './entities';
export * from './repositories';
export * from './space.repository.module';

View File

@ -1,7 +1,7 @@
import { DataSource, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { SpaceProductEntity } from '../entities/space-product.entity';
import { SpaceEntity, SpaceLinkEntity, SubspaceEntity } from '../entities';
import { SpaceEntity, SpaceLinkEntity, TagEntity } from '../entities';
import { InviteSpaceEntity } from '../entities/invite-space.entity';
@Injectable()
export class SpaceRepository extends Repository<SpaceEntity> {
@ -9,12 +9,6 @@ export class SpaceRepository extends Repository<SpaceEntity> {
super(SpaceEntity, dataSource.createEntityManager());
}
}
@Injectable()
export class SubspaceRepository extends Repository<SubspaceEntity> {
constructor(private dataSource: DataSource) {
super(SubspaceEntity, dataSource.createEntityManager());
}
}
@Injectable()
export class SpaceLinkRepository extends Repository<SpaceLinkEntity> {
@ -22,9 +16,17 @@ export class SpaceLinkRepository extends Repository<SpaceLinkEntity> {
super(SpaceLinkEntity, dataSource.createEntityManager());
}
}
@Injectable()
export class SpaceProductRepository extends Repository<SpaceProductEntity> {
export class TagRepository extends Repository<TagEntity> {
constructor(private dataSource: DataSource) {
super(SpaceProductEntity, dataSource.createEntityManager());
super(TagEntity, dataSource.createEntityManager());
}
}
@Injectable()
export class InviteSpaceRepository extends Repository<InviteSpaceEntity> {
constructor(private dataSource: DataSource) {
super(InviteSpaceEntity, dataSource.createEntityManager());
}
}

View File

@ -0,0 +1,10 @@
import { DataSource, Repository } from 'typeorm';
import { SubspaceEntity } from '../entities';
import { Injectable } from '@nestjs/common';
@Injectable()
export class SubspaceRepository extends Repository<SubspaceEntity> {
constructor(private dataSource: DataSource) {
super(SubspaceEntity, dataSource.createEntityManager());
}
}

View File

@ -1,11 +1,19 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { SpaceEntity, SubspaceEntity } from './entities';
import { SpaceEntity, SubspaceEntity, TagEntity } from './entities';
import { InviteSpaceEntity } from './entities/invite-space.entity';
@Module({
providers: [],
exports: [],
controllers: [],
imports: [TypeOrmModule.forFeature([SpaceEntity, SubspaceEntity])],
imports: [
TypeOrmModule.forFeature([
SpaceEntity,
SubspaceEntity,
TagEntity,
InviteSpaceEntity,
]),
],
})
export class SpaceRepositoryModule {}

View File

@ -58,20 +58,6 @@ export class UserOtpDto {
public expiryTime: string;
}
export class UserRoleDto {
@IsString()
@IsNotEmpty()
public uuid: string;
@IsString()
@IsNotEmpty()
public userUuid: string;
@IsString()
@IsNotEmpty()
public roleTypeUuid: string;
}
export class UserSpaceDto {
@IsString()
@IsNotEmpty()

View File

@ -2,15 +2,16 @@ import {
Column,
DeleteDateColumn,
Entity,
JoinColumn,
ManyToOne,
OneToMany,
OneToOne,
Unique,
} from 'typeorm';
import {
UserDto,
UserNotificationDto,
UserOtpDto,
UserRoleDto,
UserSpaceDto,
} from '../dtos';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
@ -26,6 +27,8 @@ import { OtpType } from '../../../../src/constants/otp-type.enum';
import { RoleTypeEntity } from '../../role-type/entities';
import { SpaceEntity } from '../../space/entities';
import { VisitorPasswordEntity } from '../../visitor-password/entities';
import { InviteUserEntity } from '../../Invite-user/entities';
import { ProjectEntity } from '../../project/entities';
@Entity({ name: 'user' })
export class UserEntity extends AbstractEntity<UserDto> {
@ -79,6 +82,18 @@ export class UserEntity extends AbstractEntity<UserDto> {
})
public isActive: boolean;
@Column({ default: false })
hasAcceptedWebAgreement: boolean;
@Column({ default: false })
hasAcceptedAppAgreement: boolean;
@Column({ type: 'timestamp', nullable: true })
webAgreementAcceptedAt: Date;
@Column({ type: 'timestamp', nullable: true })
appAgreementAcceptedAt: Date;
@OneToMany(() => UserSpaceEntity, (userSpace) => userSpace.user)
userSpaces: UserSpaceEntity[];
@ -100,10 +115,7 @@ export class UserEntity extends AbstractEntity<UserDto> {
(deviceUserNotification) => deviceUserNotification.user,
)
deviceUserNotification: DeviceNotificationEntity[];
@OneToMany(() => UserRoleEntity, (role) => role.user, {
nullable: true,
})
roles: UserRoleEntity[];
@ManyToOne(() => RegionEntity, (region) => region.users, { nullable: true })
region: RegionEntity;
@ManyToOne(() => TimeZoneEntity, (timezone) => timezone.users, {
@ -116,6 +128,21 @@ export class UserEntity extends AbstractEntity<UserDto> {
)
public visitorPasswords: VisitorPasswordEntity[];
@ManyToOne(() => RoleTypeEntity, (roleType) => roleType.users, {
nullable: false,
})
public roleType: RoleTypeEntity;
@OneToOne(() => InviteUserEntity, (inviteUser) => inviteUser.user, {
nullable: true,
})
@JoinColumn({ name: 'invite_user_uuid' })
inviteUser: InviteUserEntity;
@ManyToOne(() => ProjectEntity, (project) => project.users, {
nullable: true,
})
@JoinColumn({ name: 'project_uuid' })
public project: ProjectEntity;
constructor(partial: Partial<UserEntity>) {
super();
Object.assign(this, partial);
@ -125,7 +152,7 @@ export class UserEntity extends AbstractEntity<UserDto> {
@Entity({ name: 'user-notification' })
@Unique(['user', 'subscriptionUuid'])
export class UserNotificationEntity extends AbstractEntity<UserNotificationDto> {
@ManyToOne(() => UserEntity, (user) => user.roles, {
@ManyToOne(() => UserEntity, (user) => user.roleType, {
nullable: false,
})
user: UserEntity;
@ -178,25 +205,6 @@ export class UserOtpEntity extends AbstractEntity<UserOtpDto> {
}
}
@Entity({ name: 'user-role' })
@Unique(['user', 'roleType'])
export class UserRoleEntity extends AbstractEntity<UserRoleDto> {
@ManyToOne(() => UserEntity, (user) => user.roles, {
nullable: false,
})
user: UserEntity;
@ManyToOne(() => RoleTypeEntity, (roleType) => roleType.roles, {
nullable: false,
})
roleType: RoleTypeEntity;
constructor(partial: Partial<UserRoleEntity>) {
super();
Object.assign(this, partial);
}
}
@Entity({ name: 'user-space' })
@Unique(['user', 'space'])
export class UserSpaceEntity extends AbstractEntity<UserSpaceDto> {

View File

@ -4,7 +4,6 @@ import {
UserEntity,
UserNotificationEntity,
UserOtpEntity,
UserRoleEntity,
UserSpaceEntity,
} from '../entities/';
@ -29,13 +28,6 @@ export class UserOtpRepository extends Repository<UserOtpEntity> {
}
}
@Injectable()
export class UserRoleRepository extends Repository<UserRoleEntity> {
constructor(private dataSource: DataSource) {
super(UserRoleEntity, dataSource.createEntityManager());
}
}
@Injectable()
export class UserSpaceRepository extends Repository<UserSpaceEntity> {
constructor(private dataSource: DataSource) {

View File

@ -4,7 +4,6 @@ import {
UserEntity,
UserNotificationEntity,
UserOtpEntity,
UserRoleEntity,
UserSpaceEntity,
} from './entities';
@ -17,7 +16,6 @@ import {
UserEntity,
UserNotificationEntity,
UserOtpEntity,
UserRoleEntity,
UserSpaceEntity,
]),
],

View File

@ -10,7 +10,6 @@ import { RoleTypeSeeder } from './services/role.type.seeder';
import { SpaceRepositoryModule } from '../modules/space/space.repository.module';
import { SuperAdminSeeder } from './services/supper.admin.seeder';
import { UserRepository } from '../modules/user/repositories';
import { UserRoleRepository } from '../modules/user/repositories';
import { UserRepositoryModule } from '../modules/user/user.repository.module';
import { RegionSeeder } from './services/regions.seeder';
import { RegionRepository } from '../modules/region/repositories';
@ -28,7 +27,6 @@ import { SceneIconRepository } from '../modules/scene/repositories';
RoleTypeRepository,
SuperAdminSeeder,
UserRepository,
UserRoleRepository,
RegionSeeder,
RegionRepository,
TimeZoneSeeder,

View File

@ -19,7 +19,12 @@ export class RoleTypeSeeder {
if (!roleTypeNames.includes(RoleType.ADMIN)) {
missingRoleTypes.push(RoleType.ADMIN);
}
if (!roleTypeNames.includes(RoleType.SPACE_OWNER)) {
missingRoleTypes.push(RoleType.SPACE_OWNER);
}
if (!roleTypeNames.includes(RoleType.SPACE_MEMBER)) {
missingRoleTypes.push(RoleType.SPACE_MEMBER);
}
if (missingRoleTypes.length > 0) {
await this.addRoleTypeData(missingRoleTypes);
}

View File

@ -1,7 +1,6 @@
import { Injectable } from '@nestjs/common';
import { UserRepository } from '@app/common/modules/user/repositories';
import { RoleType } from '@app/common/constants/role.type.enum';
import { UserRoleRepository } from '@app/common/modules/user/repositories';
import { RoleTypeRepository } from '@app/common/modules/role-type/repositories';
import { ConfigService } from '@nestjs/config';
import { HelperHashService } from '../../helper/services';
@ -11,19 +10,23 @@ export class SuperAdminSeeder {
constructor(
private readonly configService: ConfigService,
private readonly userRepository: UserRepository,
private readonly userRoleRepository: UserRoleRepository,
private readonly roleTypeRepository: RoleTypeRepository,
private readonly helperHashService: HelperHashService,
) {}
async createSuperAdminIfNotFound(): Promise<void> {
try {
const superAdminData = await this.userRoleRepository.find({
where: { roleType: { type: RoleType.SUPER_ADMIN } },
const superAdmin = await this.userRepository.findOne({
where: {
roleType: { type: RoleType.SUPER_ADMIN },
email: this.configService.get<string>(
'super-admin.SUPER_ADMIN_EMAIL',
),
},
relations: ['roleType'],
});
if (superAdminData.length <= 0) {
if (!superAdmin) {
// Create the super admin user if not found
console.log('Creating super admin user...');
@ -48,20 +51,16 @@ export class SuperAdminSeeder {
salt,
);
try {
const user = await this.userRepository.save({
const defaultUserRoleUuid = await this.getRoleUuidByRoleType(
RoleType.SUPER_ADMIN,
);
await this.userRepository.save({
email: this.configService.get<string>('super-admin.SUPER_ADMIN_EMAIL'),
password: hashedPassword,
firstName: 'Super',
lastName: 'Admin',
isUserVerified: true,
isActive: true,
});
const defaultUserRoleUuid = await this.getRoleUuidByRoleType(
RoleType.SUPER_ADMIN,
);
await this.userRoleRepository.save({
user: { uuid: user.uuid },
roleType: { uuid: defaultUserRoleUuid },
});
} catch (err) {

View File

@ -16,6 +16,20 @@ const mappingInclude: { [key: string]: any } = {
subspace: {
subspace: true,
},
project: {
project: true,
},
'space-model': {
subspaceModels: 'subspace-model',
tags: 'tag_model',
},
'subspace-model': {
tags: 'tag_model',
},
tag_model: {
tag_model: true,
product: true,
},
};
export function buildTypeORMIncludeQuery(
@ -27,17 +41,33 @@ export function buildTypeORMIncludeQuery(
const fieldsToInclude: string[] = includeParam.split(',');
fieldsToInclude.forEach((field: string) => {
if (mappingInclude[field]) {
relations.push(field); // Push mapped field
} else {
console.warn(
`Field ${field} not found in mappingInclude for ${modelName}`,
);
const nestedFields = field.split('.');
let currentModelName = modelName;
let isValid = true;
nestedFields.forEach((nestedField, index) => {
const currentMapping = mappingInclude[currentModelName];
if (currentMapping?.[nestedField]) {
currentModelName =
typeof currentMapping[nestedField] === 'string'
? currentMapping[nestedField]
: currentModelName;
} else {
console.warn(
`Field "${nestedFields.slice(0, index + 1).join('.')}" not found in mappingInclude for model "${currentModelName}"`,
);
isValid = false;
return;
}
});
if (isValid) {
relations.push(field);
}
});
return relations;
return relations.length ? relations : undefined;
}
return undefined; // If no includes, return undefined
return undefined;
}

View File

@ -1,6 +1,11 @@
import { Injectable } from '@nestjs/common';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as nodemailer from 'nodemailer';
import axios from 'axios';
import {
SEND_EMAIL_API_URL_DEV,
SEND_EMAIL_API_URL_PROD,
} from '../constants/mail-trap';
@Injectable()
export class EmailService {
@ -35,4 +40,180 @@ export class EmailService {
await transporter.sendMail(mailOptions);
}
async sendEmailWithInvitationTemplate(
email: string,
emailInvitationData: any,
): Promise<void> {
const isProduction = process.env.NODE_ENV === 'production';
const API_TOKEN = this.configService.get<string>(
'email-config.MAILTRAP_API_TOKEN',
);
const API_URL = isProduction
? SEND_EMAIL_API_URL_PROD
: SEND_EMAIL_API_URL_DEV;
const TEMPLATE_UUID = this.configService.get<string>(
'email-config.MAILTRAP_INVITATION_TEMPLATE_UUID',
);
const emailData = {
from: {
email: this.smtpConfig.sender,
},
to: [
{
email: email,
},
],
template_uuid: TEMPLATE_UUID,
template_variables: emailInvitationData,
};
try {
await axios.post(API_URL, emailData, {
headers: {
Authorization: `Bearer ${API_TOKEN}`,
'Content-Type': 'application/json',
},
});
} catch (error) {
throw new HttpException(
error.response?.data?.message ||
'Error sending email using Mailtrap template',
error.response?.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async sendEmailWithTemplate(
email: string,
name: string,
isEnable: boolean,
isDelete: boolean,
): Promise<void> {
const isProduction = process.env.NODE_ENV === 'production';
const API_TOKEN = this.configService.get<string>(
'email-config.MAILTRAP_API_TOKEN',
);
const API_URL = isProduction
? SEND_EMAIL_API_URL_PROD
: SEND_EMAIL_API_URL_DEV;
// Determine the template UUID based on the arguments
const templateUuid = isDelete
? this.configService.get<string>(
'email-config.MAILTRAP_DELETE_USER_TEMPLATE_UUID',
)
: this.configService.get<string>(
isEnable
? 'email-config.MAILTRAP_ENABLE_TEMPLATE_UUID'
: 'email-config.MAILTRAP_DISABLE_TEMPLATE_UUID',
);
const emailData = {
from: {
email: this.smtpConfig.sender,
},
to: [
{
email,
},
],
template_uuid: templateUuid,
template_variables: {
name,
},
};
try {
await axios.post(API_URL, emailData, {
headers: {
Authorization: `Bearer ${API_TOKEN}`,
'Content-Type': 'application/json',
},
});
} catch (error) {
throw new HttpException(
error.response?.data?.message ||
'Error sending email using Mailtrap template',
error.response?.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async sendEditUserEmailWithTemplate(
email: string,
emailEditData: any,
): Promise<void> {
const isProduction = process.env.NODE_ENV === 'production';
const API_TOKEN = this.configService.get<string>(
'email-config.MAILTRAP_API_TOKEN',
);
const API_URL = isProduction
? SEND_EMAIL_API_URL_PROD
: SEND_EMAIL_API_URL_DEV;
const TEMPLATE_UUID = this.configService.get<string>(
'email-config.MAILTRAP_EDIT_USER_TEMPLATE_UUID',
);
const emailData = {
from: {
email: this.smtpConfig.sender,
},
to: [
{
email: email,
},
],
template_uuid: TEMPLATE_UUID,
template_variables: emailEditData,
};
try {
await axios.post(API_URL, emailData, {
headers: {
Authorization: `Bearer ${API_TOKEN}`,
'Content-Type': 'application/json',
},
});
} catch (error) {
throw new HttpException(
error.response?.data?.message ||
'Error sending email using Mailtrap template',
error.response?.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
generateUserChangesEmailBody(
addedSpaceNames: string[],
removedSpaceNames: string[],
oldRole: string,
newRole: string,
oldName: string,
newName: string,
) {
const addedSpaceNamesChanged =
addedSpaceNames.length > 0
? `Access to the following spaces were added: ${addedSpaceNames.join(', ')}`
: '';
const removedSpaceNamesChanged =
removedSpaceNames.length > 0
? `Access to the following spaces were deleted: ${removedSpaceNames.join(', ')}`
: '';
const roleChanged =
oldRole !== newRole
? `Your user role has been changed from [${oldRole}] to [${newRole}]`
: '';
const nameChanged =
oldName !== newName
? `The name associated with your account has changed from [${oldName}] to [${newName}]`
: '';
return {
addedSpaceNamesChanged,
removedSpaceNamesChanged,
roleChanged,
nameChanged,
};
}
}

1675
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -23,6 +23,7 @@
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.0",
"@nestjs/core": "^10.0.0",
"@nestjs/cqrs": "^10.2.8",
"@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0",
@ -66,6 +67,7 @@
"concurrently": "^8.2.2",
"eslint": "^8.42.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.5.0",
"prettier": "^3.0.0",

View File

@ -7,7 +7,6 @@ import { GroupModule } from './group/group.module';
import { DeviceModule } from './device/device.module';
import { UserDevicePermissionModule } from './user-device-permission/user-device-permission.module';
import { CommunityModule } from './community/community.module';
import { RoleModule } from './role/role.module';
import { SeederModule } from '@app/common/seed/seeder.module';
import { UserNotificationModule } from './user-notification/user-notification.module';
import { DeviceMessagesSubscriptionModule } from './device-messages/device-messages.module';
@ -22,6 +21,13 @@ import { VisitorPasswordModule } from './vistor-password/visitor-password.module
import { ScheduleModule } from './schedule/schedule.module';
import { SpaceModule } from './space/space.module';
import { ProductModule } from './product';
import { ProjectModule } from './project';
import { SpaceModelModule } from './space-model';
import { InviteUserModule } from './invite-user/invite-user.module';
import { PermissionModule } from './permission/permission.module';
import { RoleModule } from './role/role.module';
import { TermsConditionsModule } from './terms-conditions/terms-conditions.module';
import { PrivacyPolicyModule } from './privacy-policy/privacy-policy.module';
@Module({
imports: [
ConfigModule.forRoot({
@ -29,11 +35,11 @@ import { ProductModule } from './product';
}),
AuthenticationModule,
UserModule,
RoleModule,
InviteUserModule,
CommunityModule,
SpaceModule,
SpaceModelModule,
GroupModule,
DeviceModule,
DeviceMessagesSubscriptionModule,
@ -48,6 +54,11 @@ import { ProductModule } from './product';
VisitorPasswordModule,
ScheduleModule,
ProductModule,
ProjectModule,
PermissionModule,
RoleModule,
TermsConditionsModule,
PrivacyPolicyModule,
],
providers: [
{

View File

@ -6,11 +6,9 @@ import { UserAuthController } from './controllers';
import { UserAuthService } from './services';
import { UserRepository } from '@app/common/modules/user/repositories';
import { UserSessionRepository } from '@app/common/modules/session/repositories/session.repository';
import {
UserRoleRepository,
UserOtpRepository,
} from '@app/common/modules/user/repositories';
import { UserOtpRepository } from '@app/common/modules/user/repositories';
import { RoleTypeRepository } from '@app/common/modules/role-type/repositories';
import { RoleService } from 'src/role/services';
@Module({
imports: [ConfigModule, UserRepositoryModule, CommonModule],
@ -20,8 +18,8 @@ import { RoleTypeRepository } from '@app/common/modules/role-type/repositories';
UserRepository,
UserSessionRepository,
UserOtpRepository,
UserRoleRepository,
RoleTypeRepository,
RoleService,
],
exports: [UserAuthService],
})

View File

@ -1,5 +1,11 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator';
import {
IsBoolean,
IsEmail,
IsNotEmpty,
IsOptional,
IsString,
} from 'class-validator';
import { IsPasswordStrong } from 'src/validators/password.validator';
export class UserSignUpDto {
@ -39,7 +45,19 @@ export class UserSignUpDto {
@IsNotEmpty()
public lastName: string;
@ApiProperty({
description: 'regionUuid',
required: false,
})
@IsString()
@IsOptional()
public regionUuid?: string;
@ApiProperty({
description: 'hasAcceptedAppAgreement',
required: true,
})
@IsBoolean()
@IsNotEmpty()
public hasAcceptedAppAgreement: boolean;
}

Some files were not shown because too many files have changed in this diff Show More