Compare commits

...

27 Commits

Author SHA1 Message Date
c0b95a8b4b Update configuration and documentation for syncrow.ae domain 2025-07-17 20:56:35 -06:00
efa53da8c5 Trigger deployment after env change 2025-07-16 23:41:01 -06:00
57cf110229 Increase desired count for ECS service from 1 to 2 2025-07-14 01:41:00 -06:00
2e959a6ef3 Trigger GitHub Action 2025-07-13 23:28:02 -06:00
a6151220e2 Add .env injection from ENV_FILE secret 2025-07-13 23:17:42 -06:00
f4ba9c9bda Trigger GitHub Action 2025-07-13 22:43:00 -06:00
e91966c8f8 Add production deployment GitHub Action 2025-07-13 22:37:27 -06:00
ab59a310d9 Refactor stack.ts: reorganize imports, enhance security group definitions, and improve Fargate service configuration 2025-07-09 04:46:53 -06:00
30166810ca Fix import order and standardize database name in app.ts 2025-07-09 04:46:42 -06:00
805c5dd180 Update infra:build script to use bash for improved compatibility 2025-07-09 04:46:28 -06:00
e4ba7d46bb Refactor build.sh to improve readability and maintainability by defining variables for configuration and adding descriptive echo statements. 2025-07-09 04:46:18 -06:00
ef21b589c0 rds 2025-07-08 13:46:04 +03:00
44f83ea54e Merge branch 'cdk-aq1' of https://github.com/SyncrowIOT/backend into cdk-aq1 2025-07-08 04:26:51 -06:00
e4694db79c add build.sh command 2025-07-08 04:26:19 -06:00
13064296a7 import db 2025-07-08 13:25:23 +03:00
a269f833bc Updates ECR repository handling to import existing repo 2025-07-08 12:44:10 +03:00
fbf62fcd66 Enhances CDK deployment process and documentation
Improves the deployment script to use the UAE  region and adds context for the CDK stack.
2025-07-07 09:37:10 +03:00
374fb69804 fix the super user seeded to accept terms and add certificate arn 2025-06-30 03:58:47 -04:00
d4d1ec817d a functioning backend stack bypassing firebase and using an existing domain 2025-06-29 20:45:38 -04:00
90ab291d83 add curtain module device (#440) 2025-06-29 10:10:19 +03:00
5381a949bc task: delete used & its relations (#437) 2025-06-25 15:32:46 +03:00
6973e8b195 task: sort communities by creation date (#416) 2025-06-19 11:13:24 +03:00
92d102d08f Merge pull request #413 from SyncrowIOT/fix-staging-insirt-logs-data
Fix-staging-insirt-logs-data
2025-06-18 07:35:30 -06:00
7dc28d0cb3 fix: enable AQI sensor historical data update in device status processing 2025-06-18 07:32:39 -06:00
d9ad431a23 fix: correct procedure names in energy consumption updates 2025-06-18 05:33:49 -06:00
4bf43dab2b feat: enhance device status DTO and service with optional properties and environment checks 2025-06-18 05:33:43 -06:00
7520b8d9c7 fix: power clamp historical API (#408) 2025-06-17 15:17:49 +03:00
30 changed files with 1830 additions and 260 deletions

40
.github/workflows/production.yml vendored Normal file
View File

@ -0,0 +1,40 @@
name: 🚀 Production Deployment
on:
push:
branches:
- master
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: ⬇️ Checkout Code
uses: actions/checkout@v4
- name: 🐢 Set up Node.js 20.x
uses: actions/setup-node@v4
with:
node-version: '20'
- name: 🐳 Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: 🔐 Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: me-central-1
- name: 🗂️ Write .env file from ENV_FILE Secret
run: echo "${{ secrets.ENV_FILE }}" > .env
- name: 📦 Install Dependencies
run: npm install
- name: 🛠️ Run Production Build & Deploy Script
run: |
chmod +x ./build.sh
./build.sh

7
.gitignore vendored
View File

@ -58,4 +58,9 @@ pids
# Diagnostic reports (https://nodejs.org/api/report.html) # Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
config.dev config.dev
cdk.out
backend-cdk-new.out
web-cdk.out
backend-cdk.out
backend-cdk-final.out

View File

@ -1,16 +1,28 @@
FROM node:20-alpine FROM --platform=linux/amd64 node:20-alpine
# curl for health checks
RUN apk add --no-cache curl
WORKDIR /app WORKDIR /app
COPY package*.json ./ COPY package*.json ./
RUN npm install RUN npm install --production --ignore-scripts
RUN npm install -g @nestjs/cli
COPY . . COPY . .
RUN npm run build RUN npm run build
EXPOSE 4000 RUN addgroup -g 1001 -S nodejs
RUN adduser -S nestjs -u 1001
CMD ["npm", "run", "start"] RUN chown -R nestjs:nodejs /app
USER nestjs
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
CMD ["npm", "run", "start:prod"]

129
GITHUB_SETUP.md Normal file
View File

@ -0,0 +1,129 @@
# GitHub Actions Setup Guide
## Required GitHub Secrets
Add these secrets to your GitHub repository (Settings > Secrets and variables > Actions):
### AWS Credentials
```
AWS_ACCESS_KEY_ID=your-aws-access-key
AWS_SECRET_ACCESS_KEY=your-aws-secret-key
```
### JWT Configuration (CRITICAL - Generate secure random strings)
```
JWT_SECRET=your-super-secure-jwt-secret-key-here
JWT_SECRET_REFRESH=your-super-secure-refresh-secret-key-here
SECRET_KEY=your-general-encryption-secret-key-here
```
### Admin Configuration
```
SUPER_ADMIN_EMAIL=admin@syncrow.ae
SUPER_ADMIN_PASSWORD=YourSecureAdminPassword123!
```
### Tuya IoT Configuration
```
TUYA_ACCESS_ID=your-tuya-access-id
TUYA_ACCESS_KEY=your-tuya-access-key
TRUN_ON_TUYA_SOCKET=true-or-false
```
### Firebase Configuration
```
FIREBASE_API_KEY=your-firebase-api-key
FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
FIREBASE_PROJECT_ID=your-project-id
FIREBASE_STORAGE_BUCKET=your-project.appspot.com
FIREBASE_MESSAGING_SENDER_ID=your-sender-id
FIREBASE_APP_ID=your-app-id
FIREBASE_MEASUREMENT_ID=your-measurement-id
FIREBASE_DATABASE_URL=https://your-project.firebaseio.com
```
### Google OAuth
```
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
```
### OneSignal Push Notifications
```
ONESIGNAL_APP_ID=your-onesignal-app-id
ONESIGNAL_API_KEY=your-onesignal-api-key
```
### Email Configuration (SMTP)
```
SMTP_HOST=your-smtp-host
SMTP_USER=your-smtp-username
SMTP_PASSWORD=your-smtp-password
```
### Mailtrap Configuration
```
MAILTRAP_API_TOKEN=your-mailtrap-api-token
MAILTRAP_ENABLE_TEMPLATE_UUID=template-uuid
MAILTRAP_DISABLE_TEMPLATE_UUID=template-uuid
MAILTRAP_INVITATION_TEMPLATE_UUID=template-uuid
MAILTRAP_DELETE_USER_TEMPLATE_UUID=template-uuid
MAILTRAP_EDIT_USER_TEMPLATE_UUID=template-uuid
```
### Optional Services (leave empty if not used)
```
AZURE_REDIS_CONNECTIONSTRING=your-redis-connection-string
DOPPLER_PROJECT=your-doppler-project
DOPPLER_CONFIG=your-doppler-config
DOPPLER_ENVIRONMENT=your-doppler-environment
ACCESS_KEY=your-access-key
DOCKER_REGISTRY_SERVER_URL=your-registry-url
DOCKER_REGISTRY_SERVER_USERNAME=your-registry-username
DOCKER_REGISTRY_SERVER_PASSWORD=your-registry-password
```
## Setup Steps
1. **Add AWS Credentials**
- Create IAM user with ECR, ECS, CloudFormation permissions
- Add AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY to GitHub Secrets
2. **Generate JWT Secrets**
- Use a secure random string generator
- Make JWT_SECRET and JWT_SECRET_REFRESH different values
- Keep these values secure and never share them
3. **Configure Services**
- Add secrets for each service you're using
- Leave unused services empty (they'll default to empty strings)
4. **Test Deployment**
- Push to master/main branch
- Check GitHub Actions tab for deployment status
- Verify API is accessible at https://api.syncos.syncrow.ae
## Security Notes
- Never commit secrets to the repository
- Use GitHub Secrets for all sensitive values
- Rotate secrets regularly
- Monitor GitHub Actions logs for any exposed values
- Database password is automatically managed by AWS Secrets Manager
## Troubleshooting
- Check GitHub Actions logs for deployment errors
- Verify all required secrets are set
- Ensure AWS credentials have sufficient permissions
- Check ECS service logs in CloudWatch for runtime errors

View File

@ -1,17 +1,19 @@
# Backend # Backend
## Overview ## Overview
This is the backend for an IoT application built using NestJS. It interfaces with the Tuya IoT cloud platform to manage homes, rooms, devices, ...etc. This is the backend for an IoT application built using NestJS. It interfaces with the Tuya IoT cloud platform to manage homes, rooms, devices, ...etc.
This is the backend APIs project, developed with NestJS for Syncrow IOT Project. This is the backend APIs project, developed with NestJS for Syncrow IOT Project.
## Database Model ## Database Model
The database uses PostgreSQL and TypeORM. Below is an entity relationship diagram: The database uses PostgreSQL and TypeORM. Below is an entity relationship diagram:
The main entities are: The main entities are:
User - Stores user account information User - Stores user account information
Home - Represents a home/space Home - Represents a home/space
Room - Represents a room/sub-space Room - Represents a room/sub-space
Device - Represents a connected device Device - Represents a connected device
Product - Stores metadata about device products Product - Stores metadata about device products
Other Entities - sessions, OTPs, etc. Other Entities - sessions, OTPs, etc.
@ -19,10 +21,11 @@ Other Entities - sessions, OTPs, etc.
The entities have a one-to-many relationship - a user has multiple homes, a home has multiple rooms, and a room has multiple devices. The entities have a one-to-many relationship - a user has multiple homes, a home has multiple rooms, and a room has multiple devices.
## Architecture ## Architecture
The application is deployed on Azure App Service using Docker containers. There are separate deployment slots for development, staging, and production environments. The application is deployed on Azure App Service using Docker containers. There are separate deployment slots for development, staging, and production environments.
## Installation ## Installation
First, ensure that you have Node.js `v20.11` or newer (LTS ONLY) installed on your system. First, ensure that you have Node.js `v20.11` or newer (LTS ONLY) installed on your system.
To install the project dependencies, run the following command in the project root directory: To install the project dependencies, run the following command in the project root directory:
@ -61,8 +64,8 @@ $ npm run test:cov
![Syncrow ERD Digram](https://github.com/SyncrowIOT/backend/assets/83524184/94273a2b-625c-4a34-9cd4-fb14415ce884) ![Syncrow ERD Digram](https://github.com/SyncrowIOT/backend/assets/83524184/94273a2b-625c-4a34-9cd4-fb14415ce884)
## Architecture ## Architecture
+----------------------------------+ +----------------------------------+
| | | |
| Applications | | Applications |
@ -107,3 +110,29 @@ $ npm run test:cov
| | Standby Node | | | | | Standby Node | | |
| +------------------+----------------+ | | +------------------+----------------+ |
+-----------------------------------------------------------------+ +-----------------------------------------------------------------+
## CDK Deployment
• Bootstrap CDK (first time only): npx cdk bootstrap aws://482311766496/me-central-1
• List available stacks: npx cdk list
• Deploy infrastructure: npx cdk deploy --require-approval never
• View changes before deploy: npx cdk diff
• Generate CloudFormation template: npx cdk synth
• Destroy infrastructure: npx cdk destroy
• Environment variables are configured in infrastructure/stack.ts
• After code changes: build Docker image, push to ECR, force ECS deployment
• Database seeding happens automatically on first deployment with DB_SYNC=true
• Admin credentials: admin@syncrow.ae / YourSecureAdminPassword123!
• Production API: https://api.syncos.syncrow.ae
• Health check: https://api.syncos.syncrow.ae/health
## GitHub Actions Deployment
• Automatic deployment on push to master/main branch
• Configure GitHub Secrets (see GITHUB_SETUP.md for complete list)
• Required secrets: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, JWT_SECRET, JWT_SECRET_REFRESH
• Workflow builds Docker image, pushes to ECR, and deploys CDK stack
• Environment variables are passed securely via GitHub Secrets
• Manual deployment: Go to Actions tab and run "Deploy Backend to AWS" workflow
• Check deployment status in GitHub Actions tab
• Logs available in CloudWatch under /ecs/syncrow-backend log group

46
build.sh Normal file
View File

@ -0,0 +1,46 @@
#!/bin/bash
set -e
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGION=${AWS_DEFAULT_REGION:-me-central-1}
REPO_NAME=syncrow-backend
IMAGE_TAG=latest
CLUSTER_NAME=syncrow-backend-cluster
STACK_NAME=SyncrowBackendStack
CERTIFICATE_ARN="arn:aws:acm:$REGION:$ACCOUNT_ID:certificate/bea1e2ae-84a1-414e-8dbf-4599397e7ed0"
echo "🔐 Logging into ECR..."
aws ecr get-login-password --region $REGION | docker login --username AWS --password-stdin "$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com"
echo "🐳 Building Docker image..."
docker build --platform=linux/amd64 -t $REPO_NAME .
echo "🏷️ Tagging image..."
docker tag $REPO_NAME:$IMAGE_TAG "$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/$REPO_NAME:$IMAGE_TAG"
echo "📤 Pushing image to ECR..."
docker push "$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/$REPO_NAME:$IMAGE_TAG"
echo "🔍 Checking if ECS service exists..."
SERVICE_ARN=$(aws ecs list-services \
--cluster $CLUSTER_NAME \
--query 'serviceArns[0]' \
--output text \
--region $REGION 2>/dev/null || echo "")
echo "📦 Deploying CDK Stack..."
npx cdk deploy $STACK_NAME \
--context certificateArn=$CERTIFICATE_ARN \
--require-approval never
if [[ "$SERVICE_ARN" != "" && "$SERVICE_ARN" != "None" ]]; then
SERVICE_NAME=$(basename "$SERVICE_ARN")
echo "🚀 Redeploying ECS Service: $SERVICE_NAME"
aws ecs update-service \
--cluster $CLUSTER_NAME \
--service $SERVICE_NAME \
--force-new-deployment \
--region $REGION
fi
echo "✅ All done."

29
cdk.context.json Normal file
View File

@ -0,0 +1,29 @@
{
"availability-zones:account=426265406140:region=us-east-2": [
"us-east-2a",
"us-east-2b",
"us-east-2c"
],
"availability-zones:account=482311766496:region=us-east-2": [
"us-east-2a",
"us-east-2b",
"us-east-2c"
],
"hosted-zone:account=482311766496:domainName=syncrow.me:region=us-east-2": {
"Id": "/hostedzone/Z02085662NLJECF4DGJV3",
"Name": "syncrow.me."
},
"availability-zones:account=482311766496:region=me-central-1": [
"me-central-1a",
"me-central-1b",
"me-central-1c"
],
"hosted-zone:account=482311766496:domainName=syncrow.me:region=me-central-1": {
"Id": "/hostedzone/Z02085662NLJECF4DGJV3",
"Name": "syncrow.me."
},
"hosted-zone:account=482311766496:domainName=syncrow.ae:region=me-central-1": {
"Id": "/hostedzone/Z01153152LRHQTA1370P4",
"Name": "syncrow.ae."
}
}

58
cdk.json Normal file
View File

@ -0,0 +1,58 @@
{
"app": "npx ts-node --prefer-ts-exts infrastructure/app.ts",
"watch": {
"include": [
"**"
],
"exclude": [
"README.md",
"cdk*.json",
"**/*.d.ts",
"**/*.js",
"tsconfig.json",
"package*.json",
"yarn.lock",
"node_modules",
"test"
]
},
"context": {
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
"@aws-cdk/core:checkSecretUsage": true,
"@aws-cdk/core:target-partitions": [
"aws",
"aws-cn"
],
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
"@aws-cdk/aws-iam:minimizePolicies": true,
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
"@aws-cdk/core:enablePartitionLiterals": true,
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
"@aws-cdk/aws-iam:standardizedServicePrincipals": true,
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
"@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
"@aws-cdk/aws-route53-patters:useCertificate": true,
"@aws-cdk/customresources:installLatestAwsSdkDefault": false,
"@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
"@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
"@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
"@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
"@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
"@aws-cdk/aws-redshift:columnId": true,
"@aws-cdk/aws-stepfunctions-tasks:enableLogging": true,
"@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
"@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
"@aws-cdk/aws-kms:aliasNameRef": true,
"@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
"@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true,
"@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
"@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForSourceAction": true
}
}

22
deploy.sh Executable file
View File

@ -0,0 +1,22 @@
#!/bin/bash
set -e
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGION=${AWS_DEFAULT_REGION:-me-central-1}
npx cdk deploy SyncrowBackendStack --context certificateArn=arn:aws:acm:me-central-1:482311766496:certificate/bea1e2ae-84a1-414e-8dbf-4599397e7ed0 --require-approval never
aws ecr get-login-password --region $REGION | docker login --username AWS --password-stdin $ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com
docker build --platform=linux/amd64 -t syncrow-backend .
docker tag syncrow-backend:latest $ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/syncrow-backend:latest
docker push $ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/syncrow-backend:latest
SERVICE_ARN=$(aws ecs list-services --cluster syncrow-backend-cluster --query 'serviceArns[0]' --output text --region $REGION 2>/dev/null || echo "")
if [ "$SERVICE_ARN" != "" ] && [ "$SERVICE_ARN" != "None" ]; then
SERVICE_NAME=$(echo $SERVICE_ARN | cut -d'/' -f3)
aws ecs update-service --cluster syncrow-backend-cluster --service $SERVICE_NAME --force-new-deployment --region $REGION
else
npx cdk deploy SyncrowBackendStack --context certificateArn=arn:aws:acm:me-central-1:482311766496:certificate/bea1e2ae-84a1-414e-8dbf-4599397e7ed0 --require-approval never
fi

16
infrastructure/app.ts Normal file
View File

@ -0,0 +1,16 @@
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import 'source-map-support/register';
import { BackendStack } from './stack';
const app = new cdk.App();
new BackendStack(app, 'SyncrowBackendStack', {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: 'me-central-1',
},
databaseName: 'postgres',
certificateArn:
'arn:aws:acm:me-central-1:482311766496:certificate/423b343e-402b-4978-89bd-cda25f7a8873',
});

393
infrastructure/stack.ts Normal file
View File

@ -0,0 +1,393 @@
import * as cdk from 'aws-cdk-lib';
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecr from 'aws-cdk-lib/aws-ecr';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ecsPatterns from 'aws-cdk-lib/aws-ecs-patterns';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import * as logs from 'aws-cdk-lib/aws-logs';
import * as rds from 'aws-cdk-lib/aws-rds';
import * as route53 from 'aws-cdk-lib/aws-route53';
import { Construct } from 'constructs';
import * as dotenv from 'dotenv';
export interface BackendStackProps extends cdk.StackProps {
vpcId?: string;
databaseName?: string;
certificateArn?: string;
}
export class BackendStack extends cdk.Stack {
public readonly apiUrl: string;
public readonly databaseEndpoint: string;
public readonly vpc: ec2.IVpc;
constructor(scope: Construct, id: string, props?: BackendStackProps) {
super(scope, id, props);
// Load environment variables from .env file
dotenv.config({ path: '.env' });
// VPC - either use existing or create new
this.vpc = props?.vpcId
? ec2.Vpc.fromLookup(this, 'ExistingVpc', { vpcId: props.vpcId })
: new ec2.Vpc(this, 'SyncrowVpc', {
maxAzs: 2,
natGateways: 1,
subnetConfiguration: [
{
cidrMask: 24,
name: 'public',
subnetType: ec2.SubnetType.PUBLIC,
},
{
cidrMask: 24,
name: 'private',
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
],
});
// Security Groups
const dbSecurityGroup = new ec2.SecurityGroup(
this,
'DatabaseSecurityGroup',
{
vpc: this.vpc,
description: 'Security group for RDS PostgreSQL',
allowAllOutbound: false,
},
);
const ecsSecurityGroup = new ec2.SecurityGroup(this, 'EcsSecurityGroup', {
vpc: this.vpc,
description: 'Security group for ECS Fargate service',
allowAllOutbound: true,
});
const albSecurityGroup = new ec2.SecurityGroup(this, 'AlbSecurityGroup', {
vpc: this.vpc,
description: 'Security group for Application Load Balancer',
allowAllOutbound: true,
});
// Allow ALB to connect to ECS
ecsSecurityGroup.addIngressRule(
albSecurityGroup,
ec2.Port.tcp(3000),
'Allow ALB to connect to ECS service',
);
// Allow ECS to connect to RDS
dbSecurityGroup.addIngressRule(
ecsSecurityGroup,
ec2.Port.tcp(5432),
'Allow ECS to connect to PostgreSQL',
);
// Temporary access for admin IP
dbSecurityGroup.addIngressRule(
ec2.Peer.ipv4('216.126.231.231/32'),
ec2.Port.tcp(5432),
'Temporary access from admin IP',
);
// Allow HTTP/HTTPS traffic to ALB
albSecurityGroup.addIngressRule(
ec2.Peer.anyIpv4(),
ec2.Port.tcp(80),
'Allow HTTP traffic',
);
albSecurityGroup.addIngressRule(
ec2.Peer.anyIpv4(),
ec2.Port.tcp(443),
'Allow HTTPS traffic',
);
const dbCluster = rds.DatabaseCluster.fromDatabaseClusterAttributes(
this,
'SyncrowDatabase',
{
clusterIdentifier: 'syncrow-backend',
instanceIdentifiers: ['syncrowdatabase-instance-1'],
engine: rds.DatabaseClusterEngine.auroraPostgres({
version: rds.AuroraPostgresEngineVersion.VER_16_6,
}),
port: 5432,
securityGroups: [
ec2.SecurityGroup.fromSecurityGroupId(
this,
'ImportedDbSecurityGroup',
'sg-07e163f588b2bac25',
),
],
clusterEndpointAddress:
'syncrow-backend.cluster-criskv1sdkq4.me-central-1.rds.amazonaws.com',
},
);
// Import the existing database secret separately
const dbSecret = rds.DatabaseSecret.fromSecretCompleteArn(
this,
'ImportedDbSecret',
'arn:aws:secretsmanager:me-central-1:482311766496:secret:rds!cluster-43ec14cd-9301-43e2-aa79-d330a429a126-v0JDQN',
);
// ECR Repository for Docker images - import existing repository
const ecrRepository = ecr.Repository.fromRepositoryName(
this,
'SyncrowBackendRepo',
'syncrow-backend',
);
// Output the correct ECR URI for this region
new cdk.CfnOutput(this, 'EcrRepositoryUriRegional', {
value: ecrRepository.repositoryUri,
description: `ECR Repository URI in region ${this.region}`,
exportName: `${this.stackName}-EcrRepositoryUriRegional`,
});
// ECS Cluster
const cluster = new ecs.Cluster(this, 'SyncrowCluster', {
vpc: this.vpc,
clusterName: 'syncrow-backend-cluster',
});
// CloudWatch Log Group
const logGroup = new logs.LogGroup(this, 'SyncrowBackendLogs', {
logGroupName: '/ecs/syncrow-backend',
retention: logs.RetentionDays.ONE_WEEK,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
// Use existing wildcard certificate or create new one
const apiCertificate = props?.certificateArn
? acm.Certificate.fromCertificateArn(
this,
'ApiCertificate',
props.certificateArn,
)
: new acm.Certificate(this, 'ApiCertificate', {
domainName: 'api.syncos.syncrow.ae',
validation: acm.CertificateValidation.fromDns(),
});
// ECS Fargate Service with Application Load Balancer
const fargateService =
new ecsPatterns.ApplicationLoadBalancedFargateService(
this,
'SyncrowBackendService',
{
cluster,
memoryLimitMiB: 1024,
cpu: 512,
desiredCount: 2,
domainName: 'api.syncos.syncrow.ae',
domainZone: route53.HostedZone.fromLookup(this, 'SyncrowZone', {
domainName: 'syncrow.ae',
}),
certificate: apiCertificate,
protocol: elbv2.ApplicationProtocol.HTTPS,
redirectHTTP: true,
taskImageOptions: {
image: ecs.ContainerImage.fromEcrRepository(
ecrRepository,
'latest',
),
containerPort: 3000,
enableLogging: true,
environment: {
// App settings
NODE_ENV: process.env.NODE_ENV || 'production',
PORT: process.env.PORT || '3000',
BASE_URL: process.env.BASE_URL || '',
// Database connection (CDK provides these automatically)
AZURE_POSTGRESQL_HOST: dbCluster.clusterEndpoint.hostname,
AZURE_POSTGRESQL_PORT: '5432',
AZURE_POSTGRESQL_DATABASE: props?.databaseName || 'postgres',
AZURE_POSTGRESQL_USER: 'postgres',
AZURE_POSTGRESQL_SSL: process.env.AZURE_POSTGRESQL_SSL || 'false',
AZURE_POSTGRESQL_SYNC:
process.env.AZURE_POSTGRESQL_SYNC || 'false',
// JWT Configuration - CRITICAL: These must be set
JWT_SECRET:
process.env.JWT_SECRET ||
'syncrow-jwt-secret-key-2025-production-environment-very-secure-random-string',
JWT_SECRET_REFRESH:
process.env.JWT_SECRET_REFRESH ||
'syncrow-refresh-secret-key-2025-production-environment-different-secure-string',
JWT_EXPIRE_TIME: process.env.JWT_EXPIRE_TIME || '1h',
JWT_EXPIRE_TIME_REFRESH:
process.env.JWT_EXPIRE_TIME_REFRESH || '7d',
// Firebase Configuration
FIREBASE_API_KEY: process.env.FIREBASE_API_KEY || '',
FIREBASE_AUTH_DOMAIN: process.env.FIREBASE_AUTH_DOMAIN || '',
FIREBASE_PROJECT_ID: process.env.FIREBASE_PROJECT_ID || '',
FIREBASE_STORAGE_BUCKET:
process.env.FIREBASE_STORAGE_BUCKET || '',
FIREBASE_MESSAGING_SENDER_ID:
process.env.FIREBASE_MESSAGING_SENDER_ID || '',
FIREBASE_APP_ID: process.env.FIREBASE_APP_ID || '',
FIREBASE_MEASUREMENT_ID:
process.env.FIREBASE_MEASUREMENT_ID || '',
FIREBASE_DATABASE_URL: process.env.FIREBASE_DATABASE_URL || '',
// Tuya IoT Configuration
TUYA_EU_URL:
process.env.TUYA_EU_URL || 'https://openapi.tuyaeu.com',
TUYA_ACCESS_ID: process.env.TUYA_ACCESS_ID || '',
TUYA_ACCESS_KEY: process.env.TUYA_ACCESS_KEY || '',
TRUN_ON_TUYA_SOCKET: process.env.TRUN_ON_TUYA_SOCKET || '',
// Email Configuration
SMTP_HOST: process.env.SMTP_HOST || '',
SMTP_PORT: process.env.SMTP_PORT || '587',
SMTP_SECURE: process.env.SMTP_SECURE || 'true',
SMTP_USER: process.env.SMTP_USER || '',
SMTP_PASSWORD: process.env.SMTP_PASSWORD || '',
// Mailtrap Configuration
MAILTRAP_API_TOKEN: process.env.MAILTRAP_API_TOKEN || '',
MAILTRAP_INVITATION_TEMPLATE_UUID:
process.env.MAILTRAP_INVITATION_TEMPLATE_UUID || '',
MAILTRAP_EDIT_USER_TEMPLATE_UUID:
process.env.MAILTRAP_EDIT_USER_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 || '',
// OneSignal Push Notifications
ONESIGNAL_APP_ID: process.env.ONESIGNAL_APP_ID || '',
ONESIGNAL_API_KEY: process.env.ONESIGNAL_API_KEY || '',
// Admin Configuration
SUPER_ADMIN_EMAIL:
process.env.SUPER_ADMIN_EMAIL || 'admin@yourdomain.com',
SUPER_ADMIN_PASSWORD:
process.env.SUPER_ADMIN_PASSWORD ||
'YourSecureAdminPassword123!',
// Google OAuth
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID || '',
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET || '',
// Other Configuration
OTP_LIMITER: process.env.OTP_LIMITER || '5',
SECRET_KEY:
process.env.SECRET_KEY ||
'another-random-secret-key-for-general-encryption',
ACCESS_KEY: process.env.ACCESS_KEY || '',
DB_SYNC: process.env.DB_SYNC || 'txsrue',
// Redis (used?)
AZURE_REDIS_CONNECTIONSTRING:
process.env.AZURE_REDIS_CONNECTIONSTRING || '',
// Docker Registry (for deployment)
DOCKER_REGISTRY_SERVER_URL:
process.env.DOCKER_REGISTRY_SERVER_URL || '',
DOCKER_REGISTRY_SERVER_USERNAME:
process.env.DOCKER_REGISTRY_SERVER_USERNAME || '',
DOCKER_REGISTRY_SERVER_PASSWORD:
process.env.DOCKER_REGISTRY_SERVER_PASSWORD || '',
// Doppler (if used for secrets management)
DOPPLER_PROJECT: process.env.DOPPLER_PROJECT || '',
DOPPLER_CONFIG: process.env.DOPPLER_CONFIG || '',
DOPPLER_ENVIRONMENT: process.env.DOPPLER_ENVIRONMENT || '',
// Azure specific
WEBSITES_ENABLE_APP_SERVICE_STORAGE:
process.env.WEBSITES_ENABLE_APP_SERVICE_STORAGE || 'false',
},
secrets: {
AZURE_POSTGRESQL_PASSWORD: ecs.Secret.fromSecretsManager(
dbSecret,
'password',
),
},
logDriver: ecs.LogDrivers.awsLogs({
streamPrefix: 'syncrow-backend',
logGroup,
}),
},
publicLoadBalancer: true,
securityGroups: [ecsSecurityGroup],
},
);
// Add security group to load balancer after creation
fargateService.loadBalancer.addSecurityGroup(albSecurityGroup);
// Configure health check
fargateService.targetGroup.configureHealthCheck({
path: '/health',
healthyHttpCodes: '200',
interval: cdk.Duration.seconds(30),
timeout: cdk.Duration.seconds(5),
healthyThresholdCount: 2,
unhealthyThresholdCount: 3,
});
// Auto Scaling
const scalableTarget = fargateService.service.autoScaleTaskCount({
minCapacity: 1,
maxCapacity: 10,
});
scalableTarget.scaleOnCpuUtilization('CpuScaling', {
targetUtilizationPercent: 70,
scaleInCooldown: cdk.Duration.minutes(5),
scaleOutCooldown: cdk.Duration.minutes(2),
});
scalableTarget.scaleOnMemoryUtilization('MemoryScaling', {
targetUtilizationPercent: 80,
scaleInCooldown: cdk.Duration.minutes(5),
scaleOutCooldown: cdk.Duration.minutes(2),
});
// Grant ECS task access to RDS credentials
dbSecret.grantRead(fargateService.taskDefinition.taskRole);
this.apiUrl = 'https://api.syncos.syncrow.ae';
this.databaseEndpoint = dbCluster.clusterEndpoint.hostname;
// Outputs
new cdk.CfnOutput(this, 'ApiUrl', {
value: this.apiUrl,
description: 'Application Load Balancer URL',
exportName: `${this.stackName}-ApiUrl`,
});
new cdk.CfnOutput(this, 'DatabaseEndpoint', {
value: this.databaseEndpoint,
description: 'RDS Cluster Endpoint',
exportName: `${this.stackName}-DatabaseEndpoint`,
});
new cdk.CfnOutput(this, 'EcrRepositoryUri', {
value: ecrRepository.repositoryUri,
description: 'ECR Repository URI',
exportName: `${this.stackName}-EcrRepositoryUri`,
});
new cdk.CfnOutput(this, 'ClusterName', {
value: cluster.clusterName,
description: 'ECS Cluster Name',
exportName: `${this.stackName}-ClusterName`,
});
new cdk.CfnOutput(this, 'ServiceName', {
value: fargateService.service.serviceName,
description: 'ECS Service Name',
exportName: `${this.stackName}-ServiceName`,
});
}
}

View File

@ -397,6 +397,11 @@ export class ControllerRoute {
public static readonly DELETE_USER_SUMMARY = 'Delete user by UUID'; public static readonly DELETE_USER_SUMMARY = 'Delete user by UUID';
public static readonly DELETE_USER_DESCRIPTION = public static readonly DELETE_USER_DESCRIPTION =
'This endpoint deletes a user identified by their UUID. Accessible only by users with the Super Admin role.'; 'This endpoint deletes a user identified by their UUID. Accessible only by users with the Super Admin role.';
public static readonly DELETE_USER_PROFILE_SUMMARY =
'Delete user profile by UUID';
public static readonly DELETE_USER_PROFILE_DESCRIPTION =
'This endpoint deletes a user profile identified by their UUID. Accessible only by users with the Super Admin role.';
public static readonly UPDATE_USER_WEB_AGREEMENT_SUMMARY = public static readonly UPDATE_USER_WEB_AGREEMENT_SUMMARY =
'Update user web agreement by user UUID'; 'Update user web agreement by user UUID';
public static readonly UPDATE_USER_WEB_AGREEMENT_DESCRIPTION = public static readonly UPDATE_USER_WEB_AGREEMENT_DESCRIPTION =
@ -501,12 +506,15 @@ export class ControllerRoute {
}; };
static PowerClamp = class { static PowerClamp = class {
public static readonly ROUTE = 'power-clamp'; public static readonly ROUTE = 'power-clamp';
static ACTIONS = class { static ACTIONS = class {
public static readonly GET_ENERGY_SUMMARY = public static readonly GET_ENERGY_SUMMARY =
'Get power clamp historical data'; 'Get power clamp historical data';
public static readonly GET_ENERGY_DESCRIPTION = public static readonly GET_ENERGY_DESCRIPTION =
'This endpoint retrieves the historical data of a power clamp device based on the provided parameters.'; 'This endpoint retrieves the historical data of a power clamp device based on the provided parameters.';
public static readonly GET_ENERGY_BY_COMMUNITY_OR_SPACE_SUMMARY =
'Get power clamp historical data by community or space';
public static readonly GET_ENERGY_BY_COMMUNITY_OR_SPACE_DESCRIPTION =
'This endpoint retrieves the historical data of power clamp devices based on the provided community or space UUID.';
}; };
}; };

View File

@ -15,6 +15,7 @@ export enum ProductType {
WL = 'WL', WL = 'WL',
GD = 'GD', GD = 'GD',
CUR = 'CUR', CUR = 'CUR',
CUR_2 = 'CUR_2',
PC = 'PC', PC = 'PC',
FOUR_S = '4S', FOUR_S = '4S',
SIX_S = '6S', SIX_S = '6S',

View File

@ -13,6 +13,7 @@ class StatusDto {
@IsNotEmpty() @IsNotEmpty()
value: any; value: any;
t?: string | number | Date;
} }
export class AddDeviceStatusDto { export class AddDeviceStatusDto {

View File

@ -28,6 +28,8 @@ import { AqiDataService } from '@app/common/helper/services/aqi.data.service';
export class DeviceStatusFirebaseService { export class DeviceStatusFirebaseService {
private tuya: TuyaContext; private tuya: TuyaContext;
private firebaseDb: Database; private firebaseDb: Database;
private readonly isDevEnv: boolean;
constructor( constructor(
private readonly configService: ConfigService, private readonly configService: ConfigService,
private readonly deviceRepository: DeviceRepository, private readonly deviceRepository: DeviceRepository,
@ -46,7 +48,14 @@ export class DeviceStatusFirebaseService {
}); });
// Initialize firebaseDb using firebaseDataBase function // Initialize firebaseDb using firebaseDataBase function
this.firebaseDb = firebaseDataBase(this.configService); try {
this.firebaseDb = firebaseDataBase(this.configService);
} catch (error) {
console.warn('Firebase initialization failed, continuing without Firebase:', error.message);
this.firebaseDb = null;
}
this.isDevEnv =
this.configService.get<string>('NODE_ENV') === 'development';
} }
async addDeviceStatusByDeviceUuid( async addDeviceStatusByDeviceUuid(
deviceTuyaUuid: string, deviceTuyaUuid: string,
@ -61,7 +70,7 @@ export class DeviceStatusFirebaseService {
const deviceStatusSaved = await this.createDeviceStatusFirebase({ const deviceStatusSaved = await this.createDeviceStatusFirebase({
deviceUuid: device.uuid, deviceUuid: device.uuid,
deviceTuyaUuid: deviceTuyaUuid, deviceTuyaUuid: deviceTuyaUuid,
status: deviceStatus.status, status: deviceStatus?.status,
productUuid: deviceStatus.productUuid, productUuid: deviceStatus.productUuid,
productType: deviceStatus.productType, productType: deviceStatus.productType,
}); });
@ -122,7 +131,7 @@ export class DeviceStatusFirebaseService {
return { return {
productUuid: deviceDetails.productDevice.uuid, productUuid: deviceDetails.productDevice.uuid,
productType: deviceDetails.productDevice.prodType, productType: deviceDetails.productDevice.prodType,
status: deviceStatus.result[0].status, status: deviceStatus.result[0]?.status,
}; };
} catch (error) { } catch (error) {
throw new HttpException( throw new HttpException(
@ -166,6 +175,14 @@ export class DeviceStatusFirebaseService {
async createDeviceStatusFirebase( async createDeviceStatusFirebase(
addDeviceStatusDto: AddDeviceStatusDto, addDeviceStatusDto: AddDeviceStatusDto,
): Promise<any> { ): Promise<any> {
// Check if Firebase is available
if (!this.firebaseDb) {
console.warn('Firebase not available, skipping Firebase operations');
// Still process the database logs but skip Firebase operations
await this.processDeviceStatusLogs(addDeviceStatusDto);
return { message: 'Device status processed without Firebase' };
}
const dataRef = ref( const dataRef = ref(
this.firebaseDb, this.firebaseDb,
`device-status/${addDeviceStatusDto.deviceUuid}`, `device-status/${addDeviceStatusDto.deviceUuid}`,
@ -187,18 +204,18 @@ export class DeviceStatusFirebaseService {
if (!existingData.productType) { if (!existingData.productType) {
existingData.productType = addDeviceStatusDto.productType; existingData.productType = addDeviceStatusDto.productType;
} }
if (!existingData.status) { if (!existingData?.status) {
existingData.status = []; existingData.status = [];
} }
// Create a map to track existing status codes // Create a map to track existing status codes
const statusMap = new Map( const statusMap = new Map(
existingData.status.map((item) => [item.code, item.value]), existingData?.status.map((item) => [item.code, item.value]),
); );
// Update or add status codes // Update or add status codes
for (const statusItem of addDeviceStatusDto.status) { for (const statusItem of addDeviceStatusDto?.status) {
statusMap.set(statusItem.code, statusItem.value); statusMap.set(statusItem.code, statusItem.value);
} }
@ -211,66 +228,251 @@ export class DeviceStatusFirebaseService {
return existingData; return existingData;
}); });
// Save logs to your repository if (this.isDevEnv) {
const newLogs = addDeviceStatusDto.log.properties.map((property) => { // Save logs to your repository
return this.deviceStatusLogRepository.create({ const newLogs = addDeviceStatusDto.log.properties.map((property) => {
deviceId: addDeviceStatusDto.deviceUuid, return this.deviceStatusLogRepository.create({
deviceTuyaId: addDeviceStatusDto.deviceTuyaUuid, deviceId: addDeviceStatusDto.deviceUuid,
productId: addDeviceStatusDto.log.productId, deviceTuyaId: addDeviceStatusDto.deviceTuyaUuid,
log: addDeviceStatusDto.log, productId: addDeviceStatusDto.log.productId,
code: property.code, log: addDeviceStatusDto.log,
value: property.value, code: property.code,
eventId: addDeviceStatusDto.log.dataId, value: property.value,
eventTime: new Date(property.time).toISOString(), eventId: addDeviceStatusDto.log.dataId,
eventTime: new Date(property.time).toISOString(),
});
}); });
}); await this.deviceStatusLogRepository.save(newLogs);
await this.deviceStatusLogRepository.save(newLogs);
if (addDeviceStatusDto.productType === ProductType.PC) { if (addDeviceStatusDto.productType === ProductType.PC) {
const energyCodes = new Set([ const energyCodes = new Set([
PowerClampEnergyEnum.ENERGY_CONSUMED, PowerClampEnergyEnum.ENERGY_CONSUMED,
PowerClampEnergyEnum.ENERGY_CONSUMED_A, PowerClampEnergyEnum.ENERGY_CONSUMED_A,
PowerClampEnergyEnum.ENERGY_CONSUMED_B, PowerClampEnergyEnum.ENERGY_CONSUMED_B,
PowerClampEnergyEnum.ENERGY_CONSUMED_C, PowerClampEnergyEnum.ENERGY_CONSUMED_C,
]); ]);
const energyStatus = addDeviceStatusDto?.log?.properties?.find((status) => const energyStatus = addDeviceStatusDto?.log?.properties?.find(
energyCodes.has(status.code), (status) => energyCodes.has(status.code),
); );
if (energyStatus) { if (energyStatus) {
await this.powerClampService.updateEnergyConsumedHistoricalData( await this.powerClampService.updateEnergyConsumedHistoricalData(
addDeviceStatusDto.deviceUuid,
);
}
}
if (
addDeviceStatusDto.productType === ProductType.CPS ||
addDeviceStatusDto.productType === ProductType.WPS
) {
const occupancyCodes = new Set([PresenceSensorEnum.PRESENCE_STATE]);
const occupancyStatus = addDeviceStatusDto?.log?.properties?.find(
(status) => occupancyCodes.has(status.code),
);
if (occupancyStatus) {
await this.occupancyService.updateOccupancySensorHistoricalData(
addDeviceStatusDto.deviceUuid,
);
await this.occupancyService.updateOccupancySensorHistoricalDurationData(
addDeviceStatusDto.deviceUuid,
);
}
}
if (addDeviceStatusDto.productType === ProductType.AQI) {
await this.aqiDataService.updateAQISensorHistoricalData(
addDeviceStatusDto.deviceUuid, addDeviceStatusDto.deviceUuid,
); );
} }
} } else {
// Save logs to your repository
const newLogs = addDeviceStatusDto?.status.map((property) => {
return this.deviceStatusLogRepository.create({
deviceId: addDeviceStatusDto.deviceUuid,
deviceTuyaId: addDeviceStatusDto.deviceTuyaUuid,
productId: addDeviceStatusDto.log.productKey,
log: addDeviceStatusDto.log,
code: property.code,
value: property.value,
eventId: addDeviceStatusDto.log.dataId,
eventTime: new Date(property.t).toISOString(),
});
});
await this.deviceStatusLogRepository.save(newLogs);
if ( if (addDeviceStatusDto.productType === ProductType.PC) {
addDeviceStatusDto.productType === ProductType.CPS || const energyCodes = new Set([
addDeviceStatusDto.productType === ProductType.WPS PowerClampEnergyEnum.ENERGY_CONSUMED,
) { PowerClampEnergyEnum.ENERGY_CONSUMED_A,
const occupancyCodes = new Set([PresenceSensorEnum.PRESENCE_STATE]); PowerClampEnergyEnum.ENERGY_CONSUMED_B,
PowerClampEnergyEnum.ENERGY_CONSUMED_C,
]);
const occupancyStatus = addDeviceStatusDto?.log?.properties?.find( const energyStatus = addDeviceStatusDto?.status?.find((status) => {
(status) => occupancyCodes.has(status.code), return energyCodes.has(status.code as PowerClampEnergyEnum);
); });
if (occupancyStatus) { if (energyStatus) {
await this.occupancyService.updateOccupancySensorHistoricalData( await this.powerClampService.updateEnergyConsumedHistoricalData(
addDeviceStatusDto.deviceUuid, addDeviceStatusDto.deviceUuid,
); );
await this.occupancyService.updateOccupancySensorHistoricalDurationData( }
}
if (
addDeviceStatusDto.productType === ProductType.CPS ||
addDeviceStatusDto.productType === ProductType.WPS
) {
const occupancyCodes = new Set([PresenceSensorEnum.PRESENCE_STATE]);
const occupancyStatus = addDeviceStatusDto?.status?.find((status) => {
return occupancyCodes.has(status.code as PresenceSensorEnum);
});
if (occupancyStatus) {
await this.occupancyService.updateOccupancySensorHistoricalData(
addDeviceStatusDto.deviceUuid,
);
await this.occupancyService.updateOccupancySensorHistoricalDurationData(
addDeviceStatusDto.deviceUuid,
);
}
}
if (addDeviceStatusDto.productType === ProductType.AQI) {
await this.aqiDataService.updateAQISensorHistoricalData(
addDeviceStatusDto.deviceUuid, addDeviceStatusDto.deviceUuid,
); );
} }
} }
if (addDeviceStatusDto.productType === ProductType.AQI) {
await this.aqiDataService.updateAQISensorHistoricalData(
addDeviceStatusDto.deviceUuid,
);
}
// Return the updated data // Return the updated data
const snapshot: DataSnapshot = await get(dataRef); const snapshot: DataSnapshot = await get(dataRef);
return snapshot.val(); return snapshot.val();
} }
private async processDeviceStatusLogs(addDeviceStatusDto: AddDeviceStatusDto): Promise<void> {
if (this.isDevEnv) {
// Save logs to your repository
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(newLogs);
if (addDeviceStatusDto.productType === ProductType.PC) {
const energyCodes = new Set([
PowerClampEnergyEnum.ENERGY_CONSUMED,
PowerClampEnergyEnum.ENERGY_CONSUMED_A,
PowerClampEnergyEnum.ENERGY_CONSUMED_B,
PowerClampEnergyEnum.ENERGY_CONSUMED_C,
]);
const energyStatus = addDeviceStatusDto?.log?.properties?.find(
(status) => energyCodes.has(status.code),
);
if (energyStatus) {
await this.powerClampService.updateEnergyConsumedHistoricalData(
addDeviceStatusDto.deviceUuid,
);
}
}
if (
addDeviceStatusDto.productType === ProductType.CPS ||
addDeviceStatusDto.productType === ProductType.WPS
) {
const occupancyCodes = new Set([PresenceSensorEnum.PRESENCE_STATE]);
const occupancyStatus = addDeviceStatusDto?.log?.properties?.find(
(status) => occupancyCodes.has(status.code),
);
if (occupancyStatus) {
await this.occupancyService.updateOccupancySensorHistoricalData(
addDeviceStatusDto.deviceUuid,
);
await this.occupancyService.updateOccupancySensorHistoricalDurationData(
addDeviceStatusDto.deviceUuid,
);
}
}
if (addDeviceStatusDto.productType === ProductType.AQI) {
await this.aqiDataService.updateAQISensorHistoricalData(
addDeviceStatusDto.deviceUuid,
);
}
} else {
// Save logs to your repository
const newLogs = addDeviceStatusDto?.status.map((property) => {
return this.deviceStatusLogRepository.create({
deviceId: addDeviceStatusDto.deviceUuid,
deviceTuyaId: addDeviceStatusDto.deviceTuyaUuid,
productId: addDeviceStatusDto.log.productKey,
log: addDeviceStatusDto.log,
code: property.code,
value: property.value,
eventId: addDeviceStatusDto.log.dataId,
eventTime: new Date(property.t).toISOString(),
});
});
await this.deviceStatusLogRepository.save(newLogs);
if (addDeviceStatusDto.productType === ProductType.PC) {
const energyCodes = new Set([
PowerClampEnergyEnum.ENERGY_CONSUMED,
PowerClampEnergyEnum.ENERGY_CONSUMED_A,
PowerClampEnergyEnum.ENERGY_CONSUMED_B,
PowerClampEnergyEnum.ENERGY_CONSUMED_C,
]);
const energyStatus = addDeviceStatusDto?.status?.find((status) => {
return energyCodes.has(status.code as PowerClampEnergyEnum);
});
if (energyStatus) {
await this.powerClampService.updateEnergyConsumedHistoricalData(
addDeviceStatusDto.deviceUuid,
);
}
}
if (
addDeviceStatusDto.productType === ProductType.CPS ||
addDeviceStatusDto.productType === ProductType.WPS
) {
const occupancyCodes = new Set([PresenceSensorEnum.PRESENCE_STATE]);
const occupancyStatus = addDeviceStatusDto?.status?.find((status) => {
return occupancyCodes.has(status.code as PresenceSensorEnum);
});
if (occupancyStatus) {
await this.occupancyService.updateOccupancySensorHistoricalData(
addDeviceStatusDto.deviceUuid,
);
await this.occupancyService.updateOccupancySensorHistoricalDurationData(
addDeviceStatusDto.deviceUuid,
);
}
}
if (addDeviceStatusDto.productType === ProductType.AQI) {
await this.aqiDataService.updateAQISensorHistoricalData(
addDeviceStatusDto.deviceUuid,
);
}
}
}
} }

View File

@ -3,21 +3,32 @@ import { getDatabase } from 'firebase/database';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
export const initializeFirebaseApp = (configService: ConfigService) => { export const initializeFirebaseApp = (configService: ConfigService) => {
const firebaseConfig = { try {
apiKey: configService.get<string>('FIREBASE_API_KEY'), const firebaseConfig = {
authDomain: configService.get<string>('FIREBASE_AUTH_DOMAIN'), apiKey: configService.get<string>('FIREBASE_API_KEY'),
projectId: configService.get<string>('FIREBASE_PROJECT_ID'), authDomain: configService.get<string>('FIREBASE_AUTH_DOMAIN'),
storageBucket: configService.get<string>('FIREBASE_STORAGE_BUCKET'), projectId: configService.get<string>('FIREBASE_PROJECT_ID'),
messagingSenderId: configService.get<string>( storageBucket: configService.get<string>('FIREBASE_STORAGE_BUCKET'),
'FIREBASE_MESSAGING_SENDER_ID', messagingSenderId: configService.get<string>(
), 'FIREBASE_MESSAGING_SENDER_ID',
appId: configService.get<string>('FIREBASE_APP_ID'), ),
measurementId: configService.get<string>('FIREBASE_MEASUREMENT_ID'), appId: configService.get<string>('FIREBASE_APP_ID'),
databaseURL: configService.get<string>('FIREBASE_DATABASE_URL'), measurementId: configService.get<string>('FIREBASE_MEASUREMENT_ID'),
}; databaseURL: configService.get<string>('FIREBASE_DATABASE_URL'),
};
const app = initializeApp(firebaseConfig); // Check if required Firebase config is available
return getDatabase(app); if (!firebaseConfig.projectId || firebaseConfig.projectId === 'placeholder-project') {
console.warn('Firebase configuration not available, Firebase features will be disabled');
return null;
}
const app = initializeApp(firebaseConfig);
return getDatabase(app);
} catch (error) {
console.warn('Firebase initialization failed, Firebase features will be disabled:', error.message);
return null;
}
}; };
export const firebaseDataBase = (configService: ConfigService) => export const firebaseDataBase = (configService: ConfigService) =>

View File

@ -22,21 +22,20 @@ export class PowerClampService {
}) })
.replace('/', '-'); // MM-YYYY .replace('/', '-'); // MM-YYYY
await this.executeProcedure('fact_hourly_energy_consumed_procedure', [ await this.executeProcedure(
deviceUuid, 'fact_hourly_device_energy_consumed_procedure',
dateStr, [deviceUuid, dateStr, hour],
hour, );
]);
await this.executeProcedure('fact_daily_energy_consumed_procedure', [ await this.executeProcedure(
deviceUuid, 'fact_daily_device_energy_consumed_procedure',
dateStr, [deviceUuid, dateStr],
]); );
await this.executeProcedure('fact_monthly_energy_consumed_procedure', [ await this.executeProcedure(
deviceUuid, 'fact_monthly_device_energy_consumed_procedure',
monthYear, [deviceUuid, monthYear],
]); );
} catch (err) { } catch (err) {
console.error('Failed to insert or update energy data:', err); console.error('Failed to insert or update energy data:', err);
throw err; throw err;
@ -47,15 +46,15 @@ export class PowerClampService {
procedureFileName: string, procedureFileName: string,
params: (string | number | null)[], params: (string | number | null)[],
): Promise<void> { ): Promise<void> {
const query = this.loadQuery(procedureFileName); const query = this.loadQuery(
'fact_device_energy_consumed',
procedureFileName,
);
await this.dataSource.query(query, params); await this.dataSource.query(query, params);
console.log(`Procedure ${procedureFileName} executed successfully.`);
} }
private loadQuery(fileName: string): string { private loadQuery(folderName: string, fileName: string): string {
return this.sqlLoader.loadQuery( return this.sqlLoader.loadQuery(folderName, fileName, SQL_PROCEDURES_PATH);
'fact_energy_consumed',
fileName,
SQL_PROCEDURES_PATH,
);
} }
} }

View File

@ -8,14 +8,14 @@ import {
Unique, Unique,
} from 'typeorm'; } 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 { RoleType } from '@app/common/constants/role.type.enum'; import { RoleType } from '@app/common/constants/role.type.enum';
import { InviteUserDto, InviteUserSpaceDto } from '../dtos'; import { UserStatusEnum } from '@app/common/constants/user-status.enum';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { ProjectEntity } from '../../project/entities'; import { ProjectEntity } from '../../project/entities';
import { RoleTypeEntity } from '../../role-type/entities';
import { SpaceEntity } from '../../space/entities/space.entity'; import { SpaceEntity } from '../../space/entities/space.entity';
import { UserEntity } from '../../user/entities';
import { InviteUserDto, InviteUserSpaceDto } from '../dtos';
@Entity({ name: 'invite-user' }) @Entity({ name: 'invite-user' })
@Unique(['email', 'project']) @Unique(['email', 'project'])
@ -82,7 +82,10 @@ export class InviteUserEntity extends AbstractEntity<InviteUserDto> {
onDelete: 'CASCADE', onDelete: 'CASCADE',
}) })
public roleType: RoleTypeEntity; public roleType: RoleTypeEntity;
@OneToOne(() => UserEntity, (user) => user.inviteUser, { nullable: true }) @OneToOne(() => UserEntity, (user) => user.inviteUser, {
nullable: true,
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'user_uuid' }) @JoinColumn({ name: 'user_uuid' })
user: UserEntity; user: UserEntity;
@OneToMany( @OneToMany(
@ -112,7 +115,9 @@ export class InviteUserSpaceEntity extends AbstractEntity<InviteUserSpaceDto> {
}) })
public uuid: string; public uuid: string;
@ManyToOne(() => InviteUserEntity, (inviteUser) => inviteUser.spaces) @ManyToOne(() => InviteUserEntity, (inviteUser) => inviteUser.spaces, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'invite_user_uuid' }) @JoinColumn({ name: 'invite_user_uuid' })
public inviteUser: InviteUserEntity; public inviteUser: InviteUserEntity;

View File

@ -1,24 +1,24 @@
import { import {
Column, Column,
Entity, Entity,
Index,
JoinColumn,
ManyToOne, ManyToOne,
OneToMany, OneToMany,
Unique, Unique,
Index,
JoinColumn,
} from 'typeorm'; } from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { DeviceDto, DeviceUserPermissionDto } from '../dtos/device.dto';
import { ProductEntity } from '../../product/entities';
import { UserEntity } from '../../user/entities';
import { DeviceNotificationDto } from '../dtos';
import { PermissionTypeEntity } from '../../permission/entities'; import { PermissionTypeEntity } from '../../permission/entities';
import { PowerClampHourlyEntity } from '../../power-clamp/entities/power-clamp.entity';
import { PresenceSensorDailyDeviceEntity } from '../../presence-sensor/entities';
import { ProductEntity } from '../../product/entities';
import { SceneDeviceEntity } from '../../scene-device/entities'; import { SceneDeviceEntity } from '../../scene-device/entities';
import { SpaceEntity } from '../../space/entities/space.entity'; import { SpaceEntity } from '../../space/entities/space.entity';
import { SubspaceEntity } from '../../space/entities/subspace/subspace.entity'; import { SubspaceEntity } from '../../space/entities/subspace/subspace.entity';
import { NewTagEntity } from '../../tag'; import { NewTagEntity } from '../../tag';
import { PowerClampHourlyEntity } from '../../power-clamp/entities/power-clamp.entity'; import { UserEntity } from '../../user/entities';
import { PresenceSensorDailyDeviceEntity } from '../../presence-sensor/entities'; import { DeviceNotificationDto } from '../dtos';
import { DeviceDto, DeviceUserPermissionDto } from '../dtos/device.dto';
@Entity({ name: 'device' }) @Entity({ name: 'device' })
@Unique(['deviceTuyaUuid']) @Unique(['deviceTuyaUuid'])
@ -111,6 +111,7 @@ export class DeviceNotificationEntity extends AbstractEntity<DeviceNotificationD
@ManyToOne(() => UserEntity, (user) => user.userPermission, { @ManyToOne(() => UserEntity, (user) => user.userPermission, {
nullable: false, nullable: false,
onDelete: 'CASCADE',
}) })
user: UserEntity; user: UserEntity;
@ -149,6 +150,7 @@ export class DeviceUserPermissionEntity extends AbstractEntity<DeviceUserPermiss
@ManyToOne(() => UserEntity, (user) => user.userPermission, { @ManyToOne(() => UserEntity, (user) => user.userPermission, {
nullable: false, nullable: false,
onDelete: 'CASCADE',
}) })
user: UserEntity; user: UserEntity;
constructor(partial: Partial<DeviceUserPermissionEntity>) { constructor(partial: Partial<DeviceUserPermissionEntity>) {

View File

@ -1,3 +1,4 @@
import { defaultProfilePicture } from '@app/common/constants/default.profile.picture';
import { import {
Column, Column,
DeleteDateColumn, DeleteDateColumn,
@ -8,27 +9,26 @@ import {
OneToOne, OneToOne,
Unique, Unique,
} from 'typeorm'; } from 'typeorm';
import { OtpType } from '../../../../src/constants/otp-type.enum';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { ClientEntity } from '../../client/entities';
import {
DeviceNotificationEntity,
DeviceUserPermissionEntity,
} from '../../device/entities';
import { InviteUserEntity } from '../../Invite-user/entities';
import { ProjectEntity } from '../../project/entities';
import { RegionEntity } from '../../region/entities';
import { RoleTypeEntity } from '../../role-type/entities';
import { SpaceEntity } from '../../space/entities/space.entity';
import { TimeZoneEntity } from '../../timezone/entities';
import { VisitorPasswordEntity } from '../../visitor-password/entities';
import { import {
UserDto, UserDto,
UserNotificationDto, UserNotificationDto,
UserOtpDto, UserOtpDto,
UserSpaceDto, UserSpaceDto,
} from '../dtos'; } from '../dtos';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import {
DeviceNotificationEntity,
DeviceUserPermissionEntity,
} from '../../device/entities';
import { defaultProfilePicture } from '@app/common/constants/default.profile.picture';
import { RegionEntity } from '../../region/entities';
import { TimeZoneEntity } from '../../timezone/entities';
import { OtpType } from '../../../../src/constants/otp-type.enum';
import { RoleTypeEntity } from '../../role-type/entities';
import { VisitorPasswordEntity } from '../../visitor-password/entities';
import { InviteUserEntity } from '../../Invite-user/entities';
import { ProjectEntity } from '../../project/entities';
import { SpaceEntity } from '../../space/entities/space.entity';
import { ClientEntity } from '../../client/entities';
@Entity({ name: 'user' }) @Entity({ name: 'user' })
export class UserEntity extends AbstractEntity<UserDto> { export class UserEntity extends AbstractEntity<UserDto> {
@ -94,7 +94,9 @@ export class UserEntity extends AbstractEntity<UserDto> {
@Column({ type: 'timestamp', nullable: true }) @Column({ type: 'timestamp', nullable: true })
appAgreementAcceptedAt: Date; appAgreementAcceptedAt: Date;
@OneToMany(() => UserSpaceEntity, (userSpace) => userSpace.user) @OneToMany(() => UserSpaceEntity, (userSpace) => userSpace.user, {
onDelete: 'CASCADE',
})
userSpaces: UserSpaceEntity[]; userSpaces: UserSpaceEntity[];
@OneToMany( @OneToMany(
@ -158,6 +160,7 @@ export class UserEntity extends AbstractEntity<UserDto> {
export class UserNotificationEntity extends AbstractEntity<UserNotificationDto> { export class UserNotificationEntity extends AbstractEntity<UserNotificationDto> {
@ManyToOne(() => UserEntity, (user) => user.roleType, { @ManyToOne(() => UserEntity, (user) => user.roleType, {
nullable: false, nullable: false,
onDelete: 'CASCADE',
}) })
user: UserEntity; user: UserEntity;
@Column({ @Column({
@ -219,7 +222,10 @@ export class UserSpaceEntity extends AbstractEntity<UserSpaceDto> {
}) })
public uuid: string; public uuid: string;
@ManyToOne(() => UserEntity, (user) => user.userSpaces, { nullable: false }) @ManyToOne(() => UserEntity, (user) => user.userSpaces, {
nullable: false,
onDelete: 'CASCADE',
})
user: UserEntity; user: UserEntity;
@ManyToOne(() => SpaceEntity, (space) => space.userSpaces, { @ManyToOne(() => SpaceEntity, (space) => space.userSpaces, {

View File

@ -1,7 +1,7 @@
import { Column, Entity, ManyToOne, JoinColumn, Index } from 'typeorm'; import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
import { VisitorPasswordDto } from '../dtos';
import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { UserEntity } from '../../user/entities/user.entity'; import { UserEntity } from '../../user/entities/user.entity';
import { VisitorPasswordDto } from '../dtos';
@Entity({ name: 'visitor-password' }) @Entity({ name: 'visitor-password' })
@Index('IDX_PASSWORD_TUYA_UUID', ['passwordTuyaUuid']) @Index('IDX_PASSWORD_TUYA_UUID', ['passwordTuyaUuid'])
@ -14,6 +14,7 @@ export class VisitorPasswordEntity extends AbstractEntity<VisitorPasswordDto> {
@ManyToOne(() => UserEntity, (user) => user.visitorPasswords, { @ManyToOne(() => UserEntity, (user) => user.visitorPasswords, {
nullable: false, nullable: false,
onDelete: 'CASCADE',
}) })
@JoinColumn({ name: 'authorizer_uuid' }) @JoinColumn({ name: 'authorizer_uuid' })
public user: UserEntity; public user: UserEntity;

View File

@ -61,6 +61,10 @@ export class SuperAdminSeeder {
lastName: 'Admin', lastName: 'Admin',
isUserVerified: true, isUserVerified: true,
isActive: true, isActive: true,
hasAcceptedAppAgreement: true,
hasAcceptedWebAgreement: true,
appAgreementAcceptedAt: new Date(),
webAgreementAcceptedAt: new Date(),
roleType: { uuid: defaultUserRoleUuid }, roleType: { uuid: defaultUserRoleUuid },
}); });
} catch (err) { } catch (err) {

630
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,19 +6,25 @@
"private": true, "private": true,
"license": "UNLICENSED", "license": "UNLICENSED",
"scripts": { "scripts": {
"build": "npm run test && npx nest build", "build": "npx nest build",
"build:lambda": "npx nest build && cp package*.json dist/",
"format": "prettier --write \"apps/**/*.ts\" \"libs/**/*.ts\"", "format": "prettier --write \"apps/**/*.ts\" \"libs/**/*.ts\"",
"start": "npm run test && node dist/main", "start": "node dist/main",
"start:dev": "npm run test && npx nest start --watch", "start:dev": "npx nest start --watch",
"dev": "npx nest start --watch", "dev": "npx nest start --watch",
"start:debug": "npm run test && npx nest start --debug --watch", "start:debug": "npx nest start --debug --watch",
"start:prod": "npm run test && node dist/main", "start:prod": "node dist/main",
"start:lambda": "node dist/lambda",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest --config jest.config.js", "test": "jest --config jest.config.js",
"test:watch": "jest --watch --config jest.config.js", "test:watch": "jest --watch --config jest.config.js",
"test:cov": "jest --coverage --config jest.config.js", "test:cov": "jest --coverage --config jest.config.js",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./apps/backend/test/jest-e2e.json" "test:e2e": "jest --config ./apps/backend/test/jest-e2e.json",
"deploy": "./deploy.sh",
"infra:build": "bash build.sh",
"infra:deploy": "cdk deploy SyncrowBackendStack",
"infra:destroy": "cdk destroy SyncrowBackendStack"
}, },
"dependencies": { "dependencies": {
"@fast-csv/format": "^5.0.2", "@fast-csv/format": "^5.0.2",
@ -36,13 +42,16 @@
"@nestjs/typeorm": "^10.0.2", "@nestjs/typeorm": "^10.0.2",
"@nestjs/websockets": "^10.3.8", "@nestjs/websockets": "^10.3.8",
"@tuya/tuya-connector-nodejs": "^2.1.2", "@tuya/tuya-connector-nodejs": "^2.1.2",
"@types/aws-lambda": "^8.10.150",
"argon2": "^0.40.1", "argon2": "^0.40.1",
"aws-serverless-express": "^3.4.0",
"axios": "^1.7.7", "axios": "^1.7.7",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.1", "class-validator": "^0.14.1",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"csv-parser": "^3.2.0", "csv-parser": "^3.2.0",
"dotenv": "^17.0.0",
"express-rate-limit": "^7.1.5", "express-rate-limit": "^7.1.5",
"firebase": "^10.12.5", "firebase": "^10.12.5",
"google-auth-library": "^9.14.1", "google-auth-library": "^9.14.1",
@ -52,11 +61,13 @@
"nest-winston": "^1.10.2", "nest-winston": "^1.10.2",
"nodemailer": "^6.9.10", "nodemailer": "^6.9.10",
"onesignal-node": "^3.4.0", "onesignal-node": "^3.4.0",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1", "passport-jwt": "^4.0.1",
"pg": "^8.11.3", "pg": "^8.11.3",
"reflect-metadata": "^0.2.0", "reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"typeorm": "^0.3.20", "typeorm": "^0.3.20",
"webpack": "^5.99.9",
"winston": "^3.17.0", "winston": "^3.17.0",
"ws": "^8.17.0" "ws": "^8.17.0"
}, },
@ -73,7 +84,9 @@
"@types/supertest": "^6.0.0", "@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0", "@typescript-eslint/parser": "^6.0.0",
"aws-cdk-lib": "^2.202.0",
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
"constructs": "^10.4.2",
"eslint": "^8.42.0", "eslint": "^8.42.0",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^9.0.0",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.31.0",

View File

@ -111,6 +111,7 @@ export class CommunityService {
.leftJoin('c.spaces', 's', 's.disabled = false') .leftJoin('c.spaces', 's', 's.disabled = false')
.where('c.project = :projectUuid', { projectUuid }) .where('c.project = :projectUuid', { projectUuid })
.andWhere(`c.name != '${ORPHAN_COMMUNITY_NAME}-${project.name}'`) .andWhere(`c.name != '${ORPHAN_COMMUNITY_NAME}-${project.name}'`)
.orderBy('c.createdAt', 'DESC')
.distinct(true); .distinct(true);
if (pageable.search) { if (pageable.search) {
qb.andWhere( qb.andWhere(

View File

@ -57,7 +57,8 @@ async function bootstrap() {
logger.error('Seeding failed!', error.stack || error); logger.error('Seeding failed!', error.stack || error);
} }
logger.log('Starting auth at port ...', process.env.PORT || 4000); const port = process.env.PORT || 3000;
await app.listen(process.env.PORT || 4000); logger.log(`Starting application on port ${port}...`);
await app.listen(port, '0.0.0.0');
} }
bootstrap(); bootstrap();

View File

@ -1,18 +1,24 @@
import { ControllerRoute } from '@app/common/constants/controller-route';
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common'; import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
import { import {
ApiTags,
ApiBearerAuth, ApiBearerAuth,
ApiOperation, ApiOperation,
ApiParam, ApiParam,
ApiQuery,
ApiTags,
} from '@nestjs/swagger'; } from '@nestjs/swagger';
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; import {
import { ControllerRoute } from '@app/common/constants/controller-route'; GetPowerClampBySpaceDto,
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; GetPowerClampDto,
} from '../dto/get-power-clamp.dto';
import {
PowerClampParamsDto,
ResourceParamsDto,
} from '../dto/power-clamp-params.dto';
import { PowerClampService } from '../services/power-clamp.service'; import { PowerClampService } from '../services/power-clamp.service';
import { GetPowerClampDto } from '../dto/get-power-clamp.dto';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { PowerClampParamsDto } from '../dto/power-clamp-params.dto';
@ApiTags('Power Clamp Module') @ApiTags('Power Clamp Module')
@Controller({ @Controller({
version: EnableDisableStatusEnum.ENABLED, version: EnableDisableStatusEnum.ENABLED,
@ -20,7 +26,6 @@ import { PowerClampParamsDto } from '../dto/power-clamp-params.dto';
}) })
export class PowerClampController { export class PowerClampController {
constructor(private readonly powerClampService: PowerClampService) {} constructor(private readonly powerClampService: PowerClampService) {}
@ApiBearerAuth() @ApiBearerAuth()
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Get(':powerClampUuid/historical') @Get(':powerClampUuid/historical')
@ -39,4 +44,34 @@ export class PowerClampController {
): Promise<BaseResponseDto> { ): Promise<BaseResponseDto> {
return await this.powerClampService.getPowerClampData(params, query); return await this.powerClampService.getPowerClampData(params, query);
} }
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('historical')
@ApiOperation({
summary:
ControllerRoute.PowerClamp.ACTIONS
.GET_ENERGY_BY_COMMUNITY_OR_SPACE_SUMMARY,
description:
ControllerRoute.PowerClamp.ACTIONS
.GET_ENERGY_BY_COMMUNITY_OR_SPACE_DESCRIPTION,
})
@ApiQuery({
name: 'spaceUuid',
description: 'UUID of the Space',
required: false,
})
@ApiQuery({
name: 'communityUuid',
description: 'UUID of the Community',
required: false,
})
async getPowerClampDataBySpaceOrCommunity(
@Query() params: ResourceParamsDto,
@Query() query: GetPowerClampBySpaceDto,
): Promise<BaseResponseDto> {
return await this.powerClampService.getPowerClampDataBySpaceOrCommunity(
params,
query,
);
}
} }

View File

@ -1,6 +1,6 @@
import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { TuyaContext } from '@tuya/tuya-connector-nodejs';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { TuyaContext } from '@tuya/tuya-connector-nodejs';
import { import {
AddScheduleDto, AddScheduleDto,
EnableScheduleDto, EnableScheduleDto,
@ -11,14 +11,14 @@ import {
getDeviceScheduleInterface, getDeviceScheduleInterface,
} from '../interfaces/get.schedule.interface'; } from '../interfaces/get.schedule.interface';
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
import { DeviceRepository } from '@app/common/modules/device/repositories';
import { ProductType } from '@app/common/constants/product-type.enum'; import { ProductType } from '@app/common/constants/product-type.enum';
import { convertKeysToCamelCase } from '@app/common/helper/camelCaseConverter';
import { convertTimestampToDubaiTime } from '@app/common/helper/convertTimestampToDubaiTime'; import { convertTimestampToDubaiTime } from '@app/common/helper/convertTimestampToDubaiTime';
import { import {
getEnabledDays, getEnabledDays,
getScheduleStatus, getScheduleStatus,
} from '@app/common/helper/getScheduleStatus'; } from '@app/common/helper/getScheduleStatus';
import { DeviceRepository } from '@app/common/modules/device/repositories';
@Injectable() @Injectable()
export class ScheduleService { export class ScheduleService {
@ -57,7 +57,8 @@ export class ScheduleService {
deviceDetails.productDevice.prodType !== ProductType.ONE_1TG && deviceDetails.productDevice.prodType !== ProductType.ONE_1TG &&
deviceDetails.productDevice.prodType !== ProductType.TWO_2TG && deviceDetails.productDevice.prodType !== ProductType.TWO_2TG &&
deviceDetails.productDevice.prodType !== ProductType.THREE_3TG && deviceDetails.productDevice.prodType !== ProductType.THREE_3TG &&
deviceDetails.productDevice.prodType !== ProductType.GD deviceDetails.productDevice.prodType !== ProductType.GD &&
deviceDetails.productDevice.prodType !== ProductType.CUR_2
) { ) {
throw new HttpException( throw new HttpException(
'This device is not supported for schedule', 'This device is not supported for schedule',
@ -115,7 +116,8 @@ export class ScheduleService {
deviceDetails.productDevice.prodType !== ProductType.ONE_1TG && deviceDetails.productDevice.prodType !== ProductType.ONE_1TG &&
deviceDetails.productDevice.prodType !== ProductType.TWO_2TG && deviceDetails.productDevice.prodType !== ProductType.TWO_2TG &&
deviceDetails.productDevice.prodType !== ProductType.THREE_3TG && deviceDetails.productDevice.prodType !== ProductType.THREE_3TG &&
deviceDetails.productDevice.prodType !== ProductType.GD deviceDetails.productDevice.prodType !== ProductType.GD &&
deviceDetails.productDevice.prodType !== ProductType.CUR_2
) { ) {
throw new HttpException( throw new HttpException(
'This device is not supported for schedule', 'This device is not supported for schedule',
@ -169,7 +171,8 @@ export class ScheduleService {
deviceDetails.productDevice.prodType !== ProductType.ONE_1TG && deviceDetails.productDevice.prodType !== ProductType.ONE_1TG &&
deviceDetails.productDevice.prodType !== ProductType.TWO_2TG && deviceDetails.productDevice.prodType !== ProductType.TWO_2TG &&
deviceDetails.productDevice.prodType !== ProductType.THREE_3TG && deviceDetails.productDevice.prodType !== ProductType.THREE_3TG &&
deviceDetails.productDevice.prodType !== ProductType.GD deviceDetails.productDevice.prodType !== ProductType.GD &&
deviceDetails.productDevice.prodType !== ProductType.CUR_2
) { ) {
throw new HttpException( throw new HttpException(
'This device is not supported for schedule', 'This device is not supported for schedule',
@ -237,7 +240,8 @@ export class ScheduleService {
deviceDetails.productDevice.prodType !== ProductType.ONE_1TG && deviceDetails.productDevice.prodType !== ProductType.ONE_1TG &&
deviceDetails.productDevice.prodType !== ProductType.TWO_2TG && deviceDetails.productDevice.prodType !== ProductType.TWO_2TG &&
deviceDetails.productDevice.prodType !== ProductType.THREE_3TG && deviceDetails.productDevice.prodType !== ProductType.THREE_3TG &&
deviceDetails.productDevice.prodType !== ProductType.GD deviceDetails.productDevice.prodType !== ProductType.GD &&
deviceDetails.productDevice.prodType !== ProductType.CUR_2
) { ) {
throw new HttpException( throw new HttpException(
'This device is not supported for schedule', 'This device is not supported for schedule',
@ -323,7 +327,8 @@ export class ScheduleService {
deviceDetails.productDevice.prodType !== ProductType.ONE_1TG && deviceDetails.productDevice.prodType !== ProductType.ONE_1TG &&
deviceDetails.productDevice.prodType !== ProductType.TWO_2TG && deviceDetails.productDevice.prodType !== ProductType.TWO_2TG &&
deviceDetails.productDevice.prodType !== ProductType.THREE_3TG && deviceDetails.productDevice.prodType !== ProductType.THREE_3TG &&
deviceDetails.productDevice.prodType !== ProductType.GD deviceDetails.productDevice.prodType !== ProductType.GD &&
deviceDetails.productDevice.prodType !== ProductType.CUR_2
) { ) {
throw new HttpException( throw new HttpException(
'This device is not supported for schedule', 'This device is not supported for schedule',

View File

@ -1,3 +1,7 @@
import { ControllerRoute } from '@app/common/constants/controller-route';
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
import { RoleType } from '@app/common/constants/role.type.enum';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { import {
Body, Body,
Controller, Controller,
@ -7,10 +11,12 @@ import {
Param, Param,
Patch, Patch,
Put, Put,
Req,
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { UserService } from '../services/user.service'; import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; import { CheckProfilePictureGuard } from 'src/guards/profile.picture.guard';
import { SuperAdminRoleGuard } from 'src/guards/super.admin.role.guard';
import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard'; import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard';
import { import {
UpdateNameDto, UpdateNameDto,
@ -18,11 +24,7 @@ import {
UpdateRegionDataDto, UpdateRegionDataDto,
UpdateTimezoneDataDto, UpdateTimezoneDataDto,
} from '../dtos'; } from '../dtos';
import { CheckProfilePictureGuard } from 'src/guards/profile.picture.guard'; import { UserService } from '../services/user.service';
import { SuperAdminRoleGuard } from 'src/guards/super.admin.role.guard';
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
import { ControllerRoute } from '@app/common/constants/controller-route';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
@ApiTags('User Module') @ApiTags('User Module')
@Controller({ @Controller({
@ -154,6 +156,32 @@ export class UserController {
}; };
} }
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Delete('')
@ApiOperation({
summary: ControllerRoute.USER.ACTIONS.DELETE_USER_PROFILE_SUMMARY,
description: ControllerRoute.USER.ACTIONS.DELETE_USER_PROFILE_DESCRIPTION,
})
async deleteUserProfile(@Req() req: Request) {
const userUuid = req['user']?.userUuid;
const userRole = req['user']?.role;
if (!userUuid || (userRole && userRole == RoleType.SUPER_ADMIN)) {
throw {
statusCode: HttpStatus.UNAUTHORIZED,
message: 'Unauthorized',
};
}
await this.userService.deleteUserProfile(userUuid);
return {
statusCode: HttpStatus.OK,
data: {
userId: userUuid,
},
message: 'User deleted successfully',
};
}
@ApiBearerAuth() @ApiBearerAuth()
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Patch('agreements/web/:userUuid') @Patch('agreements/web/:userUuid')

View File

@ -1,21 +1,21 @@
import { import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
UpdateNameDto, import { removeBase64Prefix } from '@app/common/helper/removeBase64Prefix';
UpdateProfilePictureDataDto, import { RegionRepository } from '@app/common/modules/region/repositories';
UpdateRegionDataDto, import { TimeZoneRepository } from '@app/common/modules/timezone/repositories';
UpdateTimezoneDataDto, import { UserEntity } from '@app/common/modules/user/entities';
} from './../dtos/update.user.dto'; import { UserRepository } from '@app/common/modules/user/repositories';
import { import {
BadRequestException, BadRequestException,
HttpException, HttpException,
HttpStatus, HttpStatus,
Injectable, Injectable,
} from '@nestjs/common'; } from '@nestjs/common';
import { UserRepository } from '@app/common/modules/user/repositories'; import {
import { RegionRepository } from '@app/common/modules/region/repositories'; UpdateNameDto,
import { TimeZoneRepository } from '@app/common/modules/timezone/repositories'; UpdateProfilePictureDataDto,
import { removeBase64Prefix } from '@app/common/helper/removeBase64Prefix'; UpdateRegionDataDto,
import { UserEntity } from '@app/common/modules/user/entities'; UpdateTimezoneDataDto,
import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; } from './../dtos/update.user.dto';
@Injectable() @Injectable()
export class UserService { export class UserService {
@ -269,4 +269,12 @@ export class UserService {
} }
return await this.userRepository.update({ uuid }, { isActive: false }); return await this.userRepository.update({ uuid }, { isActive: false });
} }
async deleteUserProfile(uuid: string) {
const user = await this.findOneById(uuid);
if (!user) {
throw new BadRequestException('User not found');
}
return this.userRepository.delete({ uuid });
}
} }