Compare commits

..

1 Commits

Author SHA1 Message Date
7ea53feddc add deviceName to handle password API 2025-06-30 08:54:25 +03:00
101 changed files with 16142 additions and 4980 deletions

View File

@ -1,17 +0,0 @@
<!--
Thanks for contributing!
Provide a description of your changes below and a general summary in the title.
-->
## Jira Ticket
[SP-0000](https://syncrow.atlassian.net/browse/SP-0000)
## Description
<!--- Describe your changes in detail -->
## How to Test
<!--- Describe the created APIs / Logic -->

View File

@ -1,7 +1,4 @@
# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
# More GitHub Actions for Azure: https://github.com/Azure/actions
name: Build and deploy container app to Azure Web App - syncrow(staging)
name: Backend deployment to Azure App Service
on:
push:
@ -9,43 +6,50 @@ on:
- main
workflow_dispatch:
env:
AZURE_WEB_APP_NAME: 'syncrow'
AZURE_WEB_APP_SLOT_NAME: 'staging'
ACR_REGISTRY: 'syncrow.azurecr.io'
IMAGE_NAME: 'backend'
IMAGE_TAG: 'latest'
jobs:
build:
runs-on: 'ubuntu-latest'
steps:
- uses: actions/checkout@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to registry
uses: docker/login-action@v2
with:
registry: https://syncrow.azurecr.io/
username: ${{ secrets.AzureAppService_ContainerUsername_47395803300340b49931ea82f6d80be3 }}
password: ${{ secrets.AzureAppService_ContainerPassword_e7b0ff54f54d44cba04a970a22384848 }}
- name: Build and push container image to registry
uses: docker/build-push-action@v3
with:
push: true
tags: syncrow.azurecr.io/${{ secrets.AzureAppService_ContainerUsername_47395803300340b49931ea82f6d80be3 }}/syncrow/backend:${{ github.sha }}
file: ./Dockerfile
deploy:
build_and_deploy:
runs-on: ubuntu-latest
needs: build
environment:
name: 'staging'
url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}
steps:
- name: Deploy to Azure Web App
id: deploy-to-webapp
uses: azure/webapps-deploy@v2
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v3
with:
app-name: 'syncrow'
slot-name: 'staging'
publish-profile: ${{ secrets.AzureAppService_PublishProfile_44f7766441ec4796b74789e9761ef589 }}
images: 'syncrow.azurecr.io/${{ secrets.AzureAppService_ContainerUsername_47395803300340b49931ea82f6d80be3 }}/syncrow/backend:${{ github.sha }}'
node-version: '20'
- name: Install dependencies and build project
run: |
npm install
npm run build
- name: Log in to Azure
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Log in to Azure Container Registry
run: az acr login --name ${{ env.ACR_REGISTRY }}
- name: List build output
run: ls -R dist/
- name: Build and push Docker image
run: |
docker build . -t ${{ env.ACR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}
docker push ${{ env.ACR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}
- name: Set Web App with Docker container
run: |
az webapp config container set \
--name ${{ env.AZURE_WEB_APP_NAME }} \
--resource-group backend \
--docker-custom-image-name ${{ env.ACR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} \
--docker-registry-server-url https://${{ env.ACR_REGISTRY }}

View File

@ -1,73 +0,0 @@
# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
# More GitHub Actions for Azure: https://github.com/Azure/actions
name: Build and deploy Node.js app to Azure Web App - syncrow
on:
push:
branches:
- main
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read #This is required for actions/checkout
steps:
- uses: actions/checkout@v4
- name: Set up Node.js version
uses: actions/setup-node@v3
with:
node-version: '20.x'
- name: npm install, build, and test
run: |
npm install
npm run build --if-present
npm run test --if-present
- name: Zip artifact for deployment
run: zip release.zip ./* -r
- name: Upload artifact for deployment job
uses: actions/upload-artifact@v4
with:
name: node-app
path: release.zip
deploy:
runs-on: ubuntu-latest
needs: build
environment:
name: 'stg'
url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}
permissions:
id-token: write #This is required for requesting the JWT
contents: read #This is required for actions/checkout
steps:
- name: Download artifact from build job
uses: actions/download-artifact@v4
with:
name: node-app
- name: Unzip artifact for deployment
run: unzip release.zip
- name: Login to Azure
uses: azure/login@v2
with:
client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_515C8E782CFF431AB20448C85CA0FE58 }}
tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_2AEFE5534424490387C08FAE41573CC2 }}
subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_00623C33023749FEA5F6BC36884F9C8A }}
- name: 'Deploy to Azure Web App'
id: deploy-to-webapp
uses: azure/webapps-deploy@v3
with:
app-name: 'syncrow'
slot-name: 'stg'
package: .

View File

@ -1,64 +0,0 @@
name: 🤖 AI PR Description Commenter (100% Safe with jq)
on:
pull_request:
types: [opened, edited]
jobs:
generate-description:
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v4
- name: Install GitHub CLI and jq
run: |
sudo apt-get update
sudo apt-get install gh jq -y
- name: Fetch PR Commits
id: fetch_commits
run: |
COMMITS=$(gh pr view ${{ github.event.pull_request.number }} --json commits --jq '.commits[].message' | sed 's/^/- /')
echo "commits<<EOF" >> $GITHUB_ENV
echo "$COMMITS" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
env:
GH_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
- name: Generate PR Description with OpenAI (Safe JSON with jq)
run: |
REQUEST_BODY=$(jq -n \
--arg model "gpt-4o" \
--arg content "Given the following commit messages:\n\n${commits}\n\nGenerate a clear and professional pull request description." \
'{
model: $model,
messages: [{ role: "user", content: $content }]
}'
)
RESPONSE=$(curl -s https://api.openai.com/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d "$REQUEST_BODY")
DESCRIPTION=$(echo "$RESPONSE" | jq -r '.choices[0].message.content')
echo "---------- OpenAI Raw Response ----------"
echo "$RESPONSE"
echo "---------- Extracted Description ----------"
echo "$DESCRIPTION"
echo "description<<EOF" >> $GITHUB_ENV
echo "$DESCRIPTION" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
commits: ${{ env.commits }}
- name: Post AI Generated Description as Comment
run: |
gh pr comment ${{ github.event.pull_request.number }} --body "${{ env.description }}"
env:
GH_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}

View File

@ -1,40 +0,0 @@
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

9
.gitignore vendored
View File

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

View File

@ -1,28 +1,16 @@
FROM --platform=linux/amd64 node:20-alpine
# curl for health checks
RUN apk add --no-cache curl
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production --ignore-scripts
RUN npm install
RUN npm install -g @nestjs/cli
COPY . .
RUN npm run build
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nestjs -u 1001
EXPOSE 4000
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"]
CMD ["npm", "run", "start"]

View File

@ -1,129 +0,0 @@
# 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,19 +1,17 @@
# Backend
## 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 APIs project, developed with NestJS for Syncrow IOT Project.
This is the backend APIs project, developed with NestJS for Syncrow IOT Project.
## Database Model
The database uses PostgreSQL and TypeORM. Below is an entity relationship diagram:
The main entities are:
User - Stores user account information
Home - Represents a home/space
Room - Represents a room/sub-space
Home - Represents a home/space
Room - Represents a room/sub-space
Device - Represents a connected device
Product - Stores metadata about device products
Other Entities - sessions, OTPs, etc.
@ -21,11 +19,10 @@ 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.
## Architecture
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.
To install the project dependencies, run the following command in the project root directory:
@ -64,8 +61,8 @@ $ npm run test:cov
![Syncrow ERD Digram](https://github.com/SyncrowIOT/backend/assets/83524184/94273a2b-625c-4a34-9cd4-fb14415ce884)
## Architecture
## Architecture
+----------------------------------+
| |
| Applications |
@ -110,29 +107,3 @@ $ npm run test:cov
| | 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

View File

@ -1,46 +0,0 @@
#!/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."

View File

@ -1,29 +0,0 @@
{
"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."
}
}

View File

@ -1,58 +0,0 @@
{
"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
}
}

View File

@ -1,22 +0,0 @@
#!/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

View File

@ -1,16 +0,0 @@
#!/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',
});

View File

@ -1,393 +0,0 @@
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

@ -1,6 +1,5 @@
import { PlatformType } from '@app/common/constants/platform-type.enum';
import { RoleType } from '@app/common/constants/role.type.enum';
import { UserEntity } from '@app/common/modules/user/entities';
import {
BadRequestException,
Injectable,
@ -33,7 +32,7 @@ export class AuthService {
pass: string,
regionUuid?: string,
platform?: PlatformType,
): Promise<Omit<UserEntity, 'password'>> {
): Promise<any> {
const user = await this.userRepository.findOne({
where: {
email,
@ -71,9 +70,8 @@ export class AuthService {
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
// const { password, ...result } = user;
delete user.password;
return user;
const { password, ...result } = user;
return result;
}
async createSession(data): Promise<UserSessionEntity> {
@ -116,7 +114,6 @@ export class AuthService {
hasAcceptedWebAgreement: user.hasAcceptedWebAgreement,
hasAcceptedAppAgreement: user.hasAcceptedAppAgreement,
project: user?.project,
bookingPoints: user?.bookingPoints,
};
if (payload.googleCode) {
const profile = await this.getProfile(payload.googleCode);

View File

@ -1,11 +1,12 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ErrorMessageService } from 'src/error-message/error-message.service';
import { AuthModule } from './auth/auth.module';
import { CommonService } from './common.service';
import config from './config';
import { DatabaseModule } from './database/database.module';
import { HelperModule } from './helper/helper.module';
import { AuthModule } from './auth/auth.module';
import { ConfigModule } from '@nestjs/config';
import config from './config';
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';
@ -14,7 +15,6 @@ import {
SubspaceModelRepository,
} from './modules/space-model';
import { SubspaceRepository } from './modules/space/repositories/subspace.repository';
import { EmailService } from './util/email/email.service';
@Module({
providers: [
CommonService,

View File

@ -10,8 +10,6 @@ export default registerAs(
SMTP_USER: process.env.SMTP_USER,
SMTP_SENDER: process.env.SMTP_SENDER,
SMTP_PASSWORD: process.env.SMTP_PASSWORD,
BATCH_EMAIL_API_URL: process.env.BATCH_EMAIL_API_URL,
SEND_EMAIL_API_URL: process.env.SEND_EMAIL_API_URL,
MAILTRAP_API_TOKEN: process.env.MAILTRAP_API_TOKEN,
MAILTRAP_INVITATION_TEMPLATE_UUID:
process.env.MAILTRAP_INVITATION_TEMPLATE_UUID,
@ -23,9 +21,5 @@ export default registerAs(
process.env.MAILTRAP_EDIT_USER_TEMPLATE_UUID,
MAILTRAP_SEND_OTP_TEMPLATE_UUID:
process.env.MAILTRAP_SEND_OTP_TEMPLATE_UUID,
MAILTRAP_SEND_BOOKING_AVAILABILITY_UPDATE_TEMPLATE_UUID:
process.env.MAILTRAP_SEND_BOOKING_AVAILABILITY_UPDATE_TEMPLATE_UUID,
MAILTRAP_SEND_BOOKING_TIMING_UPDATE_TEMPLATE_UUID:
process.env.MAILTRAP_SEND_BOOKING_TIMING_UPDATE_TEMPLATE_UUID,
}),
);

View File

@ -69,47 +69,7 @@ export class ControllerRoute {
'Retrieve the list of all regions registered in Syncrow.';
};
};
static BOOKABLE_SPACES = class {
public static readonly ROUTE = 'bookable-spaces';
static ACTIONS = class {
public static readonly ADD_BOOKABLE_SPACES_SUMMARY =
'Add new bookable spaces';
public static readonly ADD_BOOKABLE_SPACES_DESCRIPTION =
'This endpoint allows you to add new bookable spaces by providing the required details.';
public static readonly GET_ALL_BOOKABLE_SPACES_SUMMARY =
'Get all bookable spaces';
public static readonly GET_ALL_BOOKABLE_SPACES_DESCRIPTION =
'This endpoint retrieves all bookable spaces.';
public static readonly UPDATE_BOOKABLE_SPACES_SUMMARY =
'Update existing bookable spaces';
public static readonly UPDATE_BOOKABLE_SPACES_DESCRIPTION =
'This endpoint allows you to update existing bookable spaces by providing the required details.';
};
};
static BOOKING = class {
public static readonly ROUTE = 'bookings';
static ACTIONS = class {
public static readonly ADD_BOOKING_SUMMARY = 'Add new booking';
public static readonly ADD_BOOKING_DESCRIPTION =
'This endpoint allows you to add new booking by providing the required details.';
public static readonly GET_ALL_BOOKINGS_SUMMARY = 'Get all bookings';
public static readonly GET_ALL_BOOKINGS_DESCRIPTION =
'This endpoint retrieves all bookings.';
public static readonly GET_MY_BOOKINGS_SUMMARY = 'Get my bookings';
public static readonly GET_MY_BOOKINGS_DESCRIPTION =
'This endpoint retrieves all bookings for the authenticated user.';
};
};
static COMMUNITY = class {
public static readonly ROUTE = '/projects/:projectUuid/communities';
static ACTIONS = class {
@ -239,11 +199,6 @@ export class ControllerRoute {
public static readonly UPDATE_SPACE_DESCRIPTION =
'Updates a space by its UUID and community ID. You can update the name, parent space, and other properties. If a parent space is provided and not already a parent, its `isParent` flag will be set to true.';
public static readonly UPDATE_CHILDREN_SPACES_ORDER_OF_A_SPACE_SUMMARY =
'Update the order of child spaces under a specific parent space';
public static readonly UPDATE_CHILDREN_SPACES_ORDER_OF_A_SPACE_DESCRIPTION =
'Updates the order of child spaces under a specific parent space. You can provide a new order for the child spaces.';
public static readonly GET_HEIRARCHY_SUMMARY = 'Get space hierarchy';
public static readonly GET_HEIRARCHY_DESCRIPTION =
'This endpoint retrieves the hierarchical structure of spaces under a given space ID. It returns all the child spaces nested within the specified space, organized by their parent-child relationships. ';
@ -442,11 +397,6 @@ 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 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 =
'Update user web agreement by user UUID';
public static readonly UPDATE_USER_WEB_AGREEMENT_DESCRIPTION =
@ -551,6 +501,7 @@ export class ControllerRoute {
};
static PowerClamp = class {
public static readonly ROUTE = 'power-clamp';
static ACTIONS = class {
public static readonly GET_ENERGY_SUMMARY =
'Get power clamp historical data';
@ -677,11 +628,6 @@ export class ControllerRoute {
'Delete scenes by device uuid and switch name';
public static readonly DELETE_SCENES_BY_SWITCH_NAME_DESCRIPTION =
'This endpoint deletes all scenes associated with a specific switch device.';
public static readonly POPULATE_TUYA_CONST_UUID_SUMMARY =
'Populate Tuya const UUID';
public static readonly POPULATE_TUYA_CONST_UUID_DESCRIPTION =
'This endpoint populates the Tuya const UUID for all devices.';
};
};
static DEVICE_COMMISSION = class {

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

@ -14,8 +14,6 @@ import { createLogger } from 'winston';
import { winstonLoggerOptions } from '../logger/services/winston.logger';
import { AqiSpaceDailyPollutantStatsEntity } from '../modules/aqi/entities';
import { AutomationEntity } from '../modules/automation/entities';
import { BookableSpaceEntity } from '../modules/booking/entities/bookable-space.entity';
import { BookingEntity } from '../modules/booking/entities/booking.entity';
import { ClientEntity } from '../modules/client/entities';
import { CommunityEntity } from '../modules/community/entities';
import { DeviceStatusLogEntity } from '../modules/device-status-log/entities';
@ -119,8 +117,6 @@ import { VisitorPasswordEntity } from '../modules/visitor-password/entities';
PresenceSensorDailySpaceEntity,
AqiSpaceDailyPollutantStatsEntity,
SpaceDailyOccupancyDurationEntity,
BookableSpaceEntity,
BookingEntity,
],
namingStrategy: new SnakeNamingStrategy(),
synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))),

View File

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

View File

@ -1,13 +1,15 @@
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories';
import { DeviceRepository } from '@app/common/modules/device/repositories';
import {
HttpException,
HttpStatus,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { AddDeviceStatusDto } from '../dtos/add.devices-status.dto';
import { DeviceRepository } from '@app/common/modules/device/repositories';
import { GetDeviceDetailsFunctionsStatusInterface } from 'src/device/interfaces/get.device.interface';
import { TuyaContext } from '@tuya/tuya-connector-nodejs';
import { ConfigService } from '@nestjs/config';
import { firebaseDataBase } from '../../firebase.config';
import {
Database,
DataSnapshot,
@ -15,15 +17,11 @@ import {
ref,
runTransaction,
} from 'firebase/database';
import { GetDeviceDetailsFunctionsStatusInterface } from 'src/device/interfaces/get.device.interface';
import { firebaseDataBase } from '../../firebase.config';
import { AddDeviceStatusDto } from '../dtos/add.devices-status.dto';
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories';
@Injectable()
export class DeviceStatusFirebaseService {
private tuya: TuyaContext;
private firebaseDb: Database;
private readonly isDevEnv: boolean;
constructor(
private readonly configService: ConfigService,
private readonly deviceRepository: DeviceRepository,
@ -39,14 +37,7 @@ export class DeviceStatusFirebaseService {
});
// Initialize firebaseDb using firebaseDataBase function
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';
this.firebaseDb = firebaseDataBase(this.configService);
}
async addDeviceStatusByDeviceUuid(
deviceTuyaUuid: string,
@ -61,7 +52,7 @@ export class DeviceStatusFirebaseService {
const deviceStatusSaved = await this.createDeviceStatusFirebase({
deviceUuid: device.uuid,
deviceTuyaUuid: deviceTuyaUuid,
status: deviceStatus?.status,
status: deviceStatus.status,
productUuid: deviceStatus.productUuid,
productType: deviceStatus.productType,
});
@ -84,77 +75,64 @@ export class DeviceStatusFirebaseService {
device: any;
}[],
): Promise<void> {
console.log(`🔁 Preparing logs from batch of ${batch.length} items...`);
const allLogs = [];
console.log(`🔁 Preparing logs from batch of ${batch.length} items...`);
for (const item of batch) {
const device = item.device;
if (!device?.uuid) {
console.log(`⛔ Skipped unknown device: ${item.deviceTuyaUuid}`);
continue;
}
// Determine properties based on environment
const properties =
this.isDevEnv && Array.isArray(item.log?.properties)
? item.log.properties
: Array.isArray(item.status)
? item.status
: null;
if (!properties) {
console.log(
`⛔ Skipped invalid status/properties for device: ${item.deviceTuyaUuid}`,
);
continue;
}
const logs = properties.map((property) =>
const logs = item.log.properties.map((property) =>
this.deviceStatusLogRepository.create({
deviceId: device.uuid,
deviceTuyaId: item.deviceTuyaUuid,
productId: device.productDevice?.uuid,
productId: item.log.productId,
log: item.log,
code: property.code,
value: property.value,
eventId: item.log?.dataId,
eventTime: new Date(
this.isDevEnv ? property.time : property.t,
).toISOString(),
eventId: item.log.dataId,
eventTime: new Date(property.time).toISOString(),
}),
);
allLogs.push(...logs);
}
console.log(`📝 Total logs to insert: ${allLogs.length}`);
const chunkSize = 300;
let insertedCount = 0;
const insertLogsPromise = (async () => {
const chunkSize = 300;
let insertedCount = 0;
for (let i = 0; i < allLogs.length; i += chunkSize) {
const chunk = allLogs.slice(i, i + chunkSize);
try {
const result = await this.deviceStatusLogRepository
.createQueryBuilder()
.insert()
.into('device-status-log')
.values(chunk)
.orIgnore()
.execute();
for (let i = 0; i < allLogs.length; i += chunkSize) {
const chunk = allLogs.slice(i, i + chunkSize);
try {
const result = await this.deviceStatusLogRepository
.createQueryBuilder()
.insert()
.into('device-status-log') // or use DeviceStatusLogEntity
.values(chunk)
.orIgnore() // skip duplicates
.execute();
insertedCount += result.identifiers.length;
console.log(
`✅ Inserted ${result.identifiers.length} / ${chunk.length} logs (chunk)`,
);
} catch (error) {
console.error('❌ Insert error (skipped chunk):', error.message);
insertedCount += result.identifiers.length;
console.log(
`✅ Inserted ${result.identifiers.length} / ${chunk.length} logs (chunk)`,
);
} catch (error) {
console.error('❌ Insert error (skipped chunk):', error.message);
}
}
}
console.log(`✅ Total logs inserted: ${insertedCount} / ${allLogs.length}`);
console.log(
`✅ Total logs inserted: ${insertedCount} / ${allLogs.length}`,
);
})();
await insertLogsPromise;
}
async addDeviceStatusToFirebase(
@ -213,7 +191,7 @@ export class DeviceStatusFirebaseService {
return {
productUuid: deviceDetails.productDevice.uuid,
productType: deviceDetails.productDevice.prodType,
status: deviceStatus.result[0]?.status,
status: deviceStatus.result[0].status,
};
} catch (error) {
throw new HttpException(
@ -257,14 +235,6 @@ export class DeviceStatusFirebaseService {
async createDeviceStatusFirebase(
addDeviceStatusDto: AddDeviceStatusDto,
): 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(
this.firebaseDb,
`device-status/${addDeviceStatusDto.deviceUuid}`,
@ -286,18 +256,18 @@ export class DeviceStatusFirebaseService {
if (!existingData.productType) {
existingData.productType = addDeviceStatusDto.productType;
}
if (!existingData?.status) {
if (!existingData.status) {
existingData.status = [];
}
// Create a map to track existing status codes
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
for (const statusItem of addDeviceStatusDto?.status) {
for (const statusItem of addDeviceStatusDto.status) {
statusMap.set(statusItem.code, statusItem.value);
}
@ -314,127 +284,4 @@ export class DeviceStatusFirebaseService {
const snapshot: DataSnapshot = await get(dataRef);
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,32 +3,21 @@ import { getDatabase } from 'firebase/database';
import { ConfigService } from '@nestjs/config';
export const initializeFirebaseApp = (configService: ConfigService) => {
try {
const firebaseConfig = {
apiKey: configService.get<string>('FIREBASE_API_KEY'),
authDomain: configService.get<string>('FIREBASE_AUTH_DOMAIN'),
projectId: configService.get<string>('FIREBASE_PROJECT_ID'),
storageBucket: configService.get<string>('FIREBASE_STORAGE_BUCKET'),
messagingSenderId: configService.get<string>(
'FIREBASE_MESSAGING_SENDER_ID',
),
appId: configService.get<string>('FIREBASE_APP_ID'),
measurementId: configService.get<string>('FIREBASE_MEASUREMENT_ID'),
databaseURL: configService.get<string>('FIREBASE_DATABASE_URL'),
};
const firebaseConfig = {
apiKey: configService.get<string>('FIREBASE_API_KEY'),
authDomain: configService.get<string>('FIREBASE_AUTH_DOMAIN'),
projectId: configService.get<string>('FIREBASE_PROJECT_ID'),
storageBucket: configService.get<string>('FIREBASE_STORAGE_BUCKET'),
messagingSenderId: configService.get<string>(
'FIREBASE_MESSAGING_SENDER_ID',
),
appId: configService.get<string>('FIREBASE_APP_ID'),
measurementId: configService.get<string>('FIREBASE_MEASUREMENT_ID'),
databaseURL: configService.get<string>('FIREBASE_DATABASE_URL'),
};
// Check if required Firebase config is available
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;
}
const app = initializeApp(firebaseConfig);
return getDatabase(app);
};
export const firebaseDataBase = (configService: ConfigService) =>

View File

@ -1,9 +1,9 @@
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
import { Injectable, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as NodeCache from 'node-cache';
import TuyaWebsocket from '../../config/tuya-web-socket-config';
import { ConfigService } from '@nestjs/config';
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
import { SosHandlerService } from './sos.handler.service';
import * as NodeCache from 'node-cache';
@Injectable()
export class TuyaWebSocketService implements OnModuleInit {
@ -74,12 +74,7 @@ export class TuyaWebSocketService implements OnModuleInit {
this.client.message(async (ws: WebSocket, message: any) => {
try {
const { devId, status, logData } = this.extractMessageData(message);
// console.log(
// `📬 Received message for device: ${devId}, status:`,
// status,
// logData,
// );
if (!Array.isArray(status)) {
if (!Array.isArray(logData?.properties)) {
this.client.ackMessage(message.messageId);
return;
}
@ -167,8 +162,6 @@ export class TuyaWebSocketService implements OnModuleInit {
status: any;
logData: any;
} {
// console.log('Received message:', message);
const payloadData = message.payload.data;
if (this.isDevEnv) {

View File

@ -1,5 +0,0 @@
// Convert time string (HH:mm) to minutes
export function timeToMinutes(time: string): number {
const [hours, minutes] = time.split(':').map(Number);
return hours * 60 + minutes;
}

View File

@ -49,12 +49,12 @@ export class TuyaService {
path,
});
// if (!response.success) {
// throw new HttpException(
// `Error fetching device details: ${response.msg}`,
// HttpStatus.BAD_REQUEST,
// );
// }
if (!response.success) {
throw new HttpException(
`Error fetching device details: ${response.msg}`,
HttpStatus.BAD_REQUEST,
);
}
return response.result;
}

View File

@ -1,26 +1,43 @@
import { utilities as nestWinstonModuleUtilities } from 'nest-winston';
import * as winston from 'winston';
const environment = process.env.NODE_ENV || 'local';
export const winstonLoggerOptions: winston.LoggerOptions = {
level:
process.env.AZURE_POSTGRESQL_DATABASE === 'development' ? 'debug' : 'error',
environment === 'local'
? 'debug'
: environment === 'development'
? 'warn'
: 'error',
transports: [
new winston.transports.Console({
level:
environment === 'local'
? 'debug'
: environment === 'development'
? 'warn'
: 'error',
format: winston.format.combine(
winston.format.timestamp(),
nestWinstonModuleUtilities.format.nestLike('MyApp', {
prettyPrint: true,
prettyPrint: environment === 'local',
}),
),
}),
new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
format: winston.format.json(),
}),
new winston.transports.File({
filename: 'logs/combined.log',
format: winston.format.json(),
}),
// Only create file logs if NOT local
...(environment !== 'local'
? [
new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
format: winston.format.json(),
}),
new winston.transports.File({
filename: 'logs/combined.log',
level: 'info',
format: winston.format.json(),
}),
]
: []),
],
};

View File

@ -1,6 +1,6 @@
import { RoleType } from '@app/common/constants/role.type.enum';
import { UserStatusEnum } from '@app/common/constants/user-status.enum';
import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
export class InviteUserDto {
@IsString()
@ -12,12 +12,8 @@ export class InviteUserDto {
public email: string;
@IsString()
@IsOptional()
public jobTitle?: string;
@IsString()
@IsOptional()
public companyName?: string;
@IsNotEmpty()
public jobTitle: string;
@IsEnum(UserStatusEnum)
@IsNotEmpty()

View File

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

View File

@ -1,12 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BookableSpaceEntity } from './entities/bookable-space.entity';
import { BookingEntity } from './entities/booking.entity';
@Module({
providers: [],
exports: [],
controllers: [],
imports: [TypeOrmModule.forFeature([BookableSpaceEntity, BookingEntity])],
})
export class BookingRepositoryModule {}

View File

@ -1,51 +0,0 @@
import { DaysEnum } from '@app/common/constants/days.enum';
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
OneToOne,
UpdateDateColumn,
} from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { SpaceEntity } from '../../space/entities/space.entity';
@Entity('bookable-space')
export class BookableSpaceEntity extends AbstractEntity {
@Column({
type: 'uuid',
default: () => 'gen_random_uuid()',
nullable: false,
})
public uuid: string;
@OneToOne(() => SpaceEntity, (space) => space.bookableConfig)
@JoinColumn({ name: 'space_uuid' })
space: SpaceEntity;
@Column({
type: 'enum',
enum: DaysEnum,
array: true,
nullable: false,
})
daysAvailable: DaysEnum[];
@Column({ type: 'time' })
startTime: string;
@Column({ type: 'time' })
endTime: string;
@Column({ type: Boolean, default: true })
active: boolean;
@Column({ type: 'int', default: null })
points?: number;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -1,44 +0,0 @@
import {
Column,
CreateDateColumn,
Entity,
ManyToOne,
UpdateDateColumn,
} from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { SpaceEntity } from '../../space/entities/space.entity';
import { UserEntity } from '../../user/entities';
@Entity('booking')
export class BookingEntity extends AbstractEntity {
@Column({
type: 'uuid',
default: () => 'gen_random_uuid()',
nullable: false,
})
public uuid: string;
@ManyToOne(() => SpaceEntity, (space) => space.bookableConfig)
space: SpaceEntity;
@ManyToOne(() => UserEntity, (user) => user.bookings)
user: UserEntity;
@Column({ type: Date, nullable: false })
date: Date;
@Column({ type: 'time' })
startTime: string;
@Column({ type: 'time' })
endTime: string;
@Column({ type: 'int', default: null })
cost?: number;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
export class SpaceLinkEntity extends AbstractEntity {}

View File

@ -1,14 +1,6 @@
import {
Column,
Entity,
JoinColumn,
ManyToOne,
OneToMany,
OneToOne,
} from 'typeorm';
import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { AqiSpaceDailyPollutantStatsEntity } from '../../aqi/entities';
import { BookableSpaceEntity } from '../../booking/entities/bookable-space.entity';
import { CommunityEntity } from '../../community/entities';
import { DeviceEntity } from '../../device/entities';
import { InviteUserSpaceEntity } from '../../Invite-user/entities';
@ -20,7 +12,6 @@ import { UserSpaceEntity } from '../../user/entities';
import { SpaceDto } from '../dtos';
import { SpaceProductAllocationEntity } from './space-product-allocation.entity';
import { SubspaceEntity } from './subspace/subspace.entity';
import { BookingEntity } from '../../booking/entities/booking.entity';
@Entity({ name: 'space' })
export class SpaceEntity extends AbstractEntity<SpaceDto> {
@ -65,12 +56,6 @@ export class SpaceEntity extends AbstractEntity<SpaceDto> {
})
public disabled: boolean;
@Column({
nullable: true,
type: Number,
})
public order?: number;
@OneToMany(() => SubspaceEntity, (subspace) => subspace.space, {
nullable: true,
})
@ -130,12 +115,6 @@ export class SpaceEntity extends AbstractEntity<SpaceDto> {
)
occupancyDaily: SpaceDailyOccupancyDurationEntity[];
@OneToOne(() => BookableSpaceEntity, (bookable) => bookable.space)
bookableConfig: BookableSpaceEntity;
@OneToMany(() => BookingEntity, (booking) => booking.space)
bookings: BookingEntity[];
constructor(partial: Partial<SpaceEntity>) {
super();
Object.assign(this, partial);

View File

@ -11,6 +11,9 @@ export class SpaceRepository extends Repository<SpaceEntity> {
}
}
@Injectable()
export class SpaceLinkRepository {}
@Injectable()
export class InviteSpaceRepository extends Repository<InviteSpaceEntity> {
constructor(private dataSource: DataSource) {

View File

@ -1,4 +1,3 @@
import { defaultProfilePicture } from '@app/common/constants/default.profile.picture';
import {
Column,
DeleteDateColumn,
@ -9,27 +8,27 @@ import {
OneToOne,
Unique,
} 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 {
UserDto,
UserNotificationDto,
UserOtpDto,
UserSpaceDto,
} from '../dtos';
import { BookingEntity } from '../../booking/entities/booking.entity';
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' })
export class UserEntity extends AbstractEntity<UserDto> {
@ -83,12 +82,6 @@ export class UserEntity extends AbstractEntity<UserDto> {
})
public isActive: boolean;
@Column({
nullable: true,
type: Number,
})
public bookingPoints?: number;
@Column({ default: false })
hasAcceptedWebAgreement: boolean;
@ -101,9 +94,7 @@ export class UserEntity extends AbstractEntity<UserDto> {
@Column({ type: 'timestamp', nullable: true })
appAgreementAcceptedAt: Date;
@OneToMany(() => UserSpaceEntity, (userSpace) => userSpace.user, {
onDelete: 'CASCADE',
})
@OneToMany(() => UserSpaceEntity, (userSpace) => userSpace.user)
userSpaces: UserSpaceEntity[];
@OneToMany(
@ -122,9 +113,6 @@ export class UserEntity extends AbstractEntity<UserDto> {
)
deviceUserNotification: DeviceNotificationEntity[];
@OneToMany(() => BookingEntity, (booking) => booking.user)
bookings: BookingEntity[];
@ManyToOne(() => RegionEntity, (region) => region.users, { nullable: true })
region: RegionEntity;
@ManyToOne(() => TimeZoneEntity, (timezone) => timezone.users, {
@ -170,7 +158,6 @@ export class UserEntity extends AbstractEntity<UserDto> {
export class UserNotificationEntity extends AbstractEntity<UserNotificationDto> {
@ManyToOne(() => UserEntity, (user) => user.roleType, {
nullable: false,
onDelete: 'CASCADE',
})
user: UserEntity;
@Column({
@ -232,10 +219,7 @@ export class UserSpaceEntity extends AbstractEntity<UserSpaceDto> {
})
public uuid: string;
@ManyToOne(() => UserEntity, (user) => user.userSpaces, {
nullable: false,
onDelete: 'CASCADE',
})
@ManyToOne(() => UserEntity, (user) => user.userSpaces, { nullable: false })
user: UserEntity;
@ManyToOne(() => SpaceEntity, (space) => space.userSpaces, {

View File

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

View File

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

View File

@ -2,16 +2,14 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import axios from 'axios';
import * as nodemailer from 'nodemailer';
import Mail from 'nodemailer/lib/mailer';
import { BatchEmailData } from './batch-email.interface';
import { SingleEmailData } from './single-email.interface';
import {
SEND_EMAIL_API_URL_DEV,
SEND_EMAIL_API_URL_PROD,
} from '../constants/mail-trap';
@Injectable()
export class EmailService {
private smtpConfig: any;
private API_TOKEN: string;
private SEND_EMAIL_API_URL: string;
private BATCH_EMAIL_API_URL: string;
constructor(private readonly configService: ConfigService) {
this.smtpConfig = {
@ -24,15 +22,6 @@ export class EmailService {
pass: this.configService.get<string>('email-config.SMTP_PASSWORD'),
},
};
this.API_TOKEN = this.configService.get<string>(
'email-config.MAILTRAP_API_TOKEN',
);
this.SEND_EMAIL_API_URL = this.configService.get<string>(
'email-config.SEND_EMAIL_API_URL',
);
this.BATCH_EMAIL_API_URL = this.configService.get<string>(
'email-config.BATCH_EMAIL_API_URL',
);
}
async sendEmail(
@ -42,7 +31,7 @@ export class EmailService {
): Promise<void> {
const transporter = nodemailer.createTransport(this.smtpConfig);
const mailOptions: Mail.Options = {
const mailOptions = {
from: this.smtpConfig.sender,
to: email,
subject,
@ -55,6 +44,13 @@ export class EmailService {
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',
);
@ -72,12 +68,21 @@ export class EmailService {
template_variables: emailInvitationData,
};
return this.sendEmailWithTemplateV2({
...emailData,
isBatch: false,
});
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,
name,
@ -89,6 +94,14 @@ export class EmailService {
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>(
@ -115,16 +128,32 @@ export class EmailService {
},
};
return this.sendEmailWithTemplateV2({
...emailData,
isBatch: false,
});
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',
);
@ -142,15 +171,32 @@ export class EmailService {
template_variables: emailEditData,
};
return this.sendEmailWithTemplateV2({
...emailData,
isBatch: false,
});
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 sendOtpEmailWithTemplate(
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_SEND_OTP_TEMPLATE_UUID',
);
@ -168,84 +214,20 @@ export class EmailService {
template_variables: emailEditData,
};
return this.sendEmailWithTemplateV2({
...emailData,
isBatch: false,
});
}
async sendUpdateBookingTimingEmailWithTemplate(
emails: {
email: string;
name: string;
bookings: {
date: string;
start_time: string;
end_time: string;
}[];
}[],
emailVariables: {
space_name: string;
days: string;
start_time: string;
end_time: string;
},
): Promise<void> {
const TEMPLATE_UUID = this.configService.get<string>(
'email-config.MAILTRAP_SEND_BOOKING_TIMING_UPDATE_TEMPLATE_UUID',
);
const emailData = {
base: {
from: {
email: this.smtpConfig.sender,
try {
await axios.post(API_URL, emailData, {
headers: {
Authorization: `Bearer ${API_TOKEN}`,
'Content-Type': 'application/json',
},
template_uuid: TEMPLATE_UUID,
},
requests: emails.map(({ email, name, bookings }) => ({
to: [{ email }],
template_variables: { ...emailVariables, name, bookings },
})),
};
return this.sendEmailWithTemplateV2({
...emailData,
isBatch: true,
});
}
async sendUpdateBookingAvailabilityEmailWithTemplate(
emails: { email: string; name: string }[],
emailVariables: {
space_name: string;
availability: string;
isAvailable: boolean;
},
): Promise<void> {
const TEMPLATE_UUID = this.configService.get<string>(
'email-config.MAILTRAP_SEND_BOOKING_AVAILABILITY_UPDATE_TEMPLATE_UUID',
);
const emailData = {
base: {
from: {
email: this.smtpConfig.sender,
},
template_uuid: TEMPLATE_UUID,
},
requests: emails.map(({ email, name }) => ({
to: [{ email }],
template_variables: {
...emailVariables,
name,
},
})),
};
return this.sendEmailWithTemplateV2({
...emailData,
isBatch: true,
});
});
} 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[],
@ -282,30 +264,4 @@ export class EmailService {
nameChanged,
};
}
private async sendEmailWithTemplateV2({
isBatch,
...emailData
}: BatchEmailData | SingleEmailData): Promise<void> {
try {
await axios.post(
isBatch ? this.BATCH_EMAIL_API_URL : this.SEND_EMAIL_API_URL,
{
...emailData,
},
{
headers: {
Authorization: `Bearer ${this.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,
);
}
}
}

View File

@ -1,8 +0,0 @@
export interface BatchEmailData {
base: { from: { email: string }; template_uuid: string };
requests: Array<{
to: { email: string }[];
template_variables: Record<string, any>;
}>;
isBatch: true;
}

View File

@ -1,7 +0,0 @@
export interface SingleEmailData {
from: { email: string };
to: { email: string }[];
template_uuid: string;
template_variables?: Record<string, any>;
isBatch: false;
}

View File

@ -1,7 +0,0 @@
import { format, parse } from 'date-fns';
export function to12HourFormat(timeString: string): string {
timeString = timeString.padEnd(8, ':00');
const parsedTime = parse(timeString, 'HH:mm:ss', new Date());
return format(parsedTime, 'hh:mm a');
}

14167
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -6,25 +6,19 @@
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "npx nest build",
"build:lambda": "npx nest build && cp package*.json dist/",
"build": "npm run test && npx nest build",
"format": "prettier --write \"apps/**/*.ts\" \"libs/**/*.ts\"",
"start": "node dist/main",
"start:dev": "npx nest start --watch",
"start": "npm run test && node dist/main",
"start:dev": "npm run test && npx nest start --watch",
"dev": "npx nest start --watch",
"start:debug": "npx nest start --debug --watch",
"start:prod": "node dist/main",
"start:lambda": "node dist/lambda",
"start:debug": "npm run test && npx nest start --debug --watch",
"start:prod": "npm run test && node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest --config jest.config.js",
"test:watch": "jest --watch --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: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"
"test:e2e": "jest --config ./apps/backend/test/jest-e2e.json"
},
"dependencies": {
"@fast-csv/format": "^5.0.2",
@ -43,17 +37,13 @@
"@nestjs/typeorm": "^10.0.2",
"@nestjs/websockets": "^10.3.8",
"@tuya/tuya-connector-nodejs": "^2.1.2",
"@types/aws-lambda": "^8.10.150",
"argon2": "^0.40.1",
"aws-serverless-express": "^3.4.0",
"axios": "^1.7.7",
"bcryptjs": "^2.4.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"crypto-js": "^4.2.0",
"csv-parser": "^3.2.0",
"dotenv": "^17.0.0",
"date-fns": "^4.1.0",
"express-rate-limit": "^7.1.5",
"firebase": "^10.12.5",
"google-auth-library": "^9.14.1",
@ -62,15 +52,13 @@
"morgan": "^1.10.0",
"nest-winston": "^1.10.2",
"node-cache": "^5.1.2",
"nodemailer": "^7.0.5",
"nodemailer": "^6.9.10",
"onesignal-node": "^3.4.0",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"pg": "^8.11.3",
"reflect-metadata": "^0.2.2",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"typeorm": "^0.3.20",
"webpack": "^5.99.9",
"winston": "^3.17.0",
"ws": "^8.17.0"
},
@ -84,13 +72,10 @@
"@types/jest": "^29.5.2",
"@types/multer": "^1.4.12",
"@types/node": "^20.3.1",
"@types/nodemailer": "^6.4.17",
"@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"aws-cdk-lib": "^2.202.0",
"concurrently": "^8.2.2",
"constructs": "^10.4.2",
"eslint": "^8.42.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-import": "^2.31.0",
@ -104,9 +89,5 @@
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.3"
},
"engines": {
"node": "20.x",
"npm": "10.x"
}
}

View File

@ -44,7 +44,6 @@ import { OccupancyModule } from './occupancy/occupancy.module';
import { WeatherModule } from './weather/weather.module';
import { ScheduleModule as NestScheduleModule } from '@nestjs/schedule';
import { SchedulerModule } from './scheduler/scheduler.module';
import { BookingModule } from './booking';
@Module({
imports: [
ConfigModule.forRoot({
@ -99,7 +98,6 @@ import { BookingModule } from './booking';
AqiModule,
SchedulerModule,
NestScheduleModule.forRoot(),
BookingModule,
],
providers: [
{

View File

@ -1,17 +1,15 @@
import { AuthService } from '@app/common/auth/services/auth.service';
import { RoleTypeRepository } from '@app/common/modules/role-type/repositories';
import { UserSessionRepository } from '@app/common/modules/session/repositories/session.repository';
import {
UserOtpRepository,
UserRepository,
} from '@app/common/modules/user/repositories';
import { EmailService } from '@app/common/util/email/email.service';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { UserRepository } from '@app/common/modules/user/repositories';
import { UserSessionRepository } from '@app/common/modules/session/repositories/session.repository';
import { UserOtpRepository } from '@app/common/modules/user/repositories';
import { RoleTypeRepository } from '@app/common/modules/role-type/repositories';
import { RoleService } from 'src/role/services';
import { UserAuthController } from './controllers';
import { UserAuthService } from './services';
import { AuthService } from '@app/common/auth/services/auth.service';
import { EmailService } from '@app/common/util/email.service';
import { JwtService } from '@nestjs/jwt';
@Module({
imports: [ConfigModule],

View File

@ -1,25 +1,25 @@
import { RoleType } from '@app/common/constants/role.type.enum';
import { differenceInSeconds } from '@app/common/helper/differenceInSeconds';
import { UserRepository } from '../../../libs/common/src/modules/user/repositories';
import {
BadRequestException,
ForbiddenException,
Injectable,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as argon2 from 'argon2';
import { RoleService } from 'src/role/services';
import { LessThan, MoreThan } from 'typeorm';
import { AuthService } from '../../../libs/common/src/auth/services/auth.service';
import { OtpType } from '../../../libs/common/src/constants/otp-type.enum';
import { HelperHashService } from '../../../libs/common/src/helper/services';
import { UserSessionRepository } from '../../../libs/common/src/modules/session/repositories/session.repository';
import { UserEntity } from '../../../libs/common/src/modules/user/entities/user.entity';
import { UserRepository } from '../../../libs/common/src/modules/user/repositories';
import { UserOtpRepository } from '../../../libs/common/src/modules/user/repositories/user.repository';
import { EmailService } from '../../../libs/common/src/util/email/email.service';
import { ForgetPasswordDto, UserOtpDto, VerifyOtpDto } from '../dtos';
import { UserSignUpDto } from '../dtos/user-auth.dto';
import { HelperHashService } from '../../../libs/common/src/helper/services';
import { UserLoginDto } from '../dtos/user-login.dto';
import { AuthService } from '../../../libs/common/src/auth/services/auth.service';
import { UserSessionRepository } from '../../../libs/common/src/modules/session/repositories/session.repository';
import { UserOtpRepository } from '../../../libs/common/src/modules/user/repositories/user.repository';
import { ForgetPasswordDto, UserOtpDto, VerifyOtpDto } from '../dtos';
import { EmailService } from '../../../libs/common/src/util/email.service';
import { OtpType } from '../../../libs/common/src/constants/otp-type.enum';
import { UserEntity } from '../../../libs/common/src/modules/user/entities/user.entity';
import * as argon2 from 'argon2';
import { differenceInSeconds } from '@app/common/helper/differenceInSeconds';
import { LessThan, MoreThan } from 'typeorm';
import { ConfigService } from '@nestjs/config';
import { RoleService } from 'src/role/services';
import { RoleType } from '@app/common/constants/role.type.enum';
@Injectable()
export class UserAuthService {
@ -108,7 +108,7 @@ export class UserAuthService {
async userLogin(data: UserLoginDto) {
try {
let user: Omit<UserEntity, 'password'>;
let user;
if (data.googleCode) {
const googleUserData = await this.authService.login({
googleCode: data.googleCode,
@ -145,7 +145,7 @@ export class UserAuthService {
}
const session = await Promise.all([
await this.sessionRepository.update(
{ userId: user?.['id'] },
{ userId: user.id },
{
isLoggedOut: true,
},
@ -166,7 +166,6 @@ export class UserAuthService {
hasAcceptedAppAgreement: user.hasAcceptedAppAgreement,
project: user.project,
sessionId: session[1].uuid,
bookingPoints: user.bookingPoints,
});
return res;
} catch (error) {
@ -348,7 +347,6 @@ export class UserAuthService {
userId: user.uuid,
uuid: user.uuid,
type,
bookingPoints: user.bookingPoints,
sessionId,
});
await this.authService.updateRefreshToken(user.uuid, tokens.refreshToken);

View File

@ -1,29 +0,0 @@
import { BookingRepositoryModule } from '@app/common/modules/booking/booking.repository.module';
import { BookableSpaceEntityRepository } from '@app/common/modules/booking/repositories/bookable-space.repository';
import { BookingEntityRepository } from '@app/common/modules/booking/repositories/booking.repository';
import { SpaceRepository } from '@app/common/modules/space';
import { UserRepository } from '@app/common/modules/user/repositories';
import { EmailService } from '@app/common/util/email/email.service';
import { Global, Module } from '@nestjs/common';
import { BookableSpaceController } from './controllers/bookable-space.controller';
import { BookingController } from './controllers/booking.controller';
import { BookableSpaceService } from './services/bookable-space.service';
import { BookingService } from './services/booking.service';
@Global()
@Module({
imports: [BookingRepositoryModule],
controllers: [BookableSpaceController, BookingController],
providers: [
BookableSpaceService,
BookingService,
EmailService,
BookableSpaceEntityRepository,
BookingEntityRepository,
SpaceRepository,
UserRepository,
],
exports: [BookableSpaceService, BookingService],
})
export class BookingModule {}

View File

@ -1,107 +0,0 @@
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 {
Body,
Controller,
Get,
Param,
ParseUUIDPipe,
Post,
Put,
Query,
Req,
UseGuards,
} from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { PageResponse } from '@app/common/dto/pagination.response.dto';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { plainToInstance } from 'class-transformer';
import { BookableSpaceRequestDto } from '../dtos/bookable-space-request.dto';
import { BookableSpaceResponseDto } from '../dtos/bookable-space-response.dto';
import { CreateBookableSpaceDto } from '../dtos/create-bookable-space.dto';
import { UpdateBookableSpaceDto } from '../dtos/update-bookable-space.dto';
import { BookableSpaceService } from '../services/bookable-space.service';
@ApiTags('Booking Module')
@Controller({
version: EnableDisableStatusEnum.ENABLED,
path: ControllerRoute.BOOKABLE_SPACES.ROUTE,
})
export class BookableSpaceController {
constructor(private readonly bookableSpaceService: BookableSpaceService) {}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Post()
@ApiOperation({
summary:
ControllerRoute.BOOKABLE_SPACES.ACTIONS.ADD_BOOKABLE_SPACES_SUMMARY,
description:
ControllerRoute.BOOKABLE_SPACES.ACTIONS.ADD_BOOKABLE_SPACES_DESCRIPTION,
})
async create(@Body() dto: CreateBookableSpaceDto): Promise<BaseResponseDto> {
const result = await this.bookableSpaceService.create(dto);
return new SuccessResponseDto({
data: result,
message: 'Successfully created bookable spaces',
});
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get()
@ApiOperation({
summary:
ControllerRoute.BOOKABLE_SPACES.ACTIONS.GET_ALL_BOOKABLE_SPACES_SUMMARY,
description:
ControllerRoute.BOOKABLE_SPACES.ACTIONS
.GET_ALL_BOOKABLE_SPACES_DESCRIPTION,
})
async findAll(
@Query() query: BookableSpaceRequestDto,
@Req() req: Request,
): Promise<PageResponse<BookableSpaceResponseDto>> {
const project = req['user']?.project?.uuid;
if (!project) {
throw new Error('Project UUID is required in the request');
}
const { data, pagination } = await this.bookableSpaceService.findAll(
query,
project,
);
return new PageResponse<BookableSpaceResponseDto>(
{
data: data.map((space) =>
plainToInstance(BookableSpaceResponseDto, space, {
excludeExtraneousValues: true,
}),
),
message: 'Successfully fetched all bookable spaces',
},
pagination,
);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Put(':spaceUuid')
@ApiOperation({
summary:
ControllerRoute.BOOKABLE_SPACES.ACTIONS.UPDATE_BOOKABLE_SPACES_SUMMARY,
description:
ControllerRoute.BOOKABLE_SPACES.ACTIONS
.UPDATE_BOOKABLE_SPACES_DESCRIPTION,
})
async update(
@Param('spaceUuid', ParseUUIDPipe) spaceUuid: string,
@Body() dto: UpdateBookableSpaceDto,
): Promise<BaseResponseDto> {
const result = await this.bookableSpaceService.update(spaceUuid, dto);
return new SuccessResponseDto({
data: result,
message: 'Successfully updated bookable spaces',
});
}
}

View File

@ -1,107 +0,0 @@
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 {
Body,
Controller,
Get,
Post,
Query,
Req,
UseGuards,
} from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { plainToInstance } from 'class-transformer';
import { AdminRoleGuard } from 'src/guards/admin.role.guard';
import { BookingRequestDto } from '../dtos/booking-request.dto';
import { BookingResponseDto } from '../dtos/booking-response.dto';
import { CreateBookingDto } from '../dtos/create-booking.dto';
import { MyBookingRequestDto } from '../dtos/my-booking-request.dto';
import { BookingService } from '../services/booking.service';
@ApiTags('Booking Module')
@Controller({
version: EnableDisableStatusEnum.ENABLED,
path: ControllerRoute.BOOKING.ROUTE,
})
export class BookingController {
constructor(private readonly bookingService: BookingService) {}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Post()
@ApiOperation({
summary: ControllerRoute.BOOKING.ACTIONS.ADD_BOOKING_SUMMARY,
description: ControllerRoute.BOOKING.ACTIONS.ADD_BOOKING_DESCRIPTION,
})
async create(
@Body() dto: CreateBookingDto,
@Req() req: Request,
): Promise<BaseResponseDto> {
const userUuid = req['user']?.uuid;
if (!userUuid) {
throw new Error('User UUID is required in the request');
}
const result = await this.bookingService.create(userUuid, dto);
return new SuccessResponseDto({
data: result,
message: 'Successfully created booking',
});
}
@ApiBearerAuth()
@UseGuards(AdminRoleGuard)
@Get()
@ApiOperation({
summary: ControllerRoute.BOOKING.ACTIONS.GET_ALL_BOOKINGS_SUMMARY,
description: ControllerRoute.BOOKING.ACTIONS.GET_ALL_BOOKINGS_DESCRIPTION,
})
async findAll(
@Query() query: BookingRequestDto,
@Req() req: Request,
): Promise<BaseResponseDto> {
const project = req['user']?.project?.uuid;
if (!project) {
throw new Error('Project UUID is required in the request');
}
const result = await this.bookingService.findAll(query, project);
return new SuccessResponseDto({
data: plainToInstance(BookingResponseDto, result, {
excludeExtraneousValues: true,
}),
message: 'Successfully fetched all bookings',
});
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('my-bookings')
@ApiOperation({
summary: ControllerRoute.BOOKING.ACTIONS.GET_MY_BOOKINGS_SUMMARY,
description: ControllerRoute.BOOKING.ACTIONS.GET_MY_BOOKINGS_DESCRIPTION,
})
async findMyBookings(
@Query() query: MyBookingRequestDto,
@Req() req: Request,
): Promise<BaseResponseDto> {
const userUuid = req['user']?.uuid;
const project = req['user']?.project?.uuid;
if (!project) {
throw new Error('Project UUID is required in the request');
}
const result = await this.bookingService.findMyBookings(
query,
userUuid,
project,
);
return new SuccessResponseDto({
data: plainToInstance(BookingResponseDto, result, {
excludeExtraneousValues: true,
}),
message: 'Successfully fetched all bookings',
});
}
}

View File

@ -1,31 +0,0 @@
import { BooleanValues } from '@app/common/constants/boolean-values.enum';
import { PaginationRequestWithSearchGetListDto } from '@app/common/dto/pagination-with-search.request.dto';
import { ApiProperty, OmitType } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsBoolean, IsNotEmpty, IsOptional } from 'class-validator';
export class BookableSpaceRequestDto extends OmitType(
PaginationRequestWithSearchGetListDto,
['includeSpaces'],
) {
@ApiProperty({
type: Boolean,
required: false,
})
@IsBoolean()
@IsOptional()
@Transform(({ obj }) => {
return obj.active === BooleanValues.TRUE;
})
active?: boolean;
@ApiProperty({
type: Boolean,
})
@IsBoolean()
@IsNotEmpty()
@Transform(({ obj }) => {
return obj.configured === BooleanValues.TRUE;
})
configured: boolean;
}

View File

@ -1,59 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { Expose, Type } from 'class-transformer';
export class BookableSpaceConfigResponseDto {
@ApiProperty()
@Expose()
uuid: string;
@ApiProperty({
type: [String],
})
@Expose()
daysAvailable: string[];
@ApiProperty()
@Expose()
startTime: string;
@ApiProperty()
@Expose()
endTime: string;
@ApiProperty({
type: Boolean,
})
@Expose()
active: boolean;
@ApiProperty({
type: Number,
nullable: true,
})
@Expose()
points?: number;
}
export class BookableSpaceResponseDto {
@ApiProperty()
@Expose()
uuid: string;
@ApiProperty()
@Expose()
spaceUuid: string;
@ApiProperty()
@Expose()
spaceName: string;
@ApiProperty()
@Expose()
virtualLocation: string;
@ApiProperty({
type: BookableSpaceConfigResponseDto,
})
@Expose()
@Type(() => BookableSpaceConfigResponseDto)
bookableConfig: BookableSpaceConfigResponseDto;
}

View File

@ -1,23 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsOptional, IsUUID, Matches } from 'class-validator';
export class BookingRequestDto {
@ApiProperty({
description: 'Month in MM/YYYY format',
example: '07/2025',
})
@IsNotEmpty()
@Matches(/^(0[1-9]|1[0-2])\/\d{4}$/, {
message: 'Date must be in MM/YYYY format',
})
month: string;
@ApiProperty({
description: 'Space UUID',
example: '550e8400-e29b-41d4-a716-446655440000',
required: false,
})
@IsOptional()
@IsUUID('4')
space?: string;
}

View File

@ -1,88 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { Expose, Transform, Type } from 'class-transformer';
export class BookingUserResponseDto {
@ApiProperty()
@Expose()
uuid: string;
@ApiProperty()
@Expose()
firstName: string;
@ApiProperty()
@Expose()
lastName: string;
@ApiProperty({
type: String,
nullable: true,
})
@Expose()
email: string;
@ApiProperty({
type: String,
nullable: true,
})
@Expose()
@Transform(({ obj }) => obj.inviteUser?.companyName || null)
companyName: string;
@ApiProperty({
type: String,
nullable: true,
})
@Expose()
phoneNumber: string;
}
export class BookingSpaceResponseDto {
@ApiProperty()
@Expose()
uuid: string;
@ApiProperty()
@Expose()
spaceName: string;
}
export class BookingResponseDto {
@ApiProperty()
@Expose()
uuid: string;
@ApiProperty({
type: Date,
})
@Expose()
date: Date;
@ApiProperty()
@Expose()
startTime: string;
@ApiProperty()
@Expose()
endTime: string;
@ApiProperty({
type: Number,
})
@Expose()
cost: number;
@ApiProperty({
type: BookingUserResponseDto,
})
@Type(() => BookingUserResponseDto)
@Expose()
user: BookingUserResponseDto;
@ApiProperty({
type: BookingSpaceResponseDto,
})
@Type(() => BookingSpaceResponseDto)
@Expose()
space: BookingSpaceResponseDto;
}

View File

@ -1,63 +0,0 @@
import { DaysEnum } from '@app/common/constants/days.enum';
import { ApiProperty } from '@nestjs/swagger';
import {
ArrayMinSize,
IsArray,
IsEnum,
IsInt,
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
Matches,
Max,
Min,
} from 'class-validator';
export class CreateBookableSpaceDto {
@ApiProperty({
type: 'string',
isArray: true,
example: [
'3fa85f64-5717-4562-b3fc-2c963f66afa6',
'4fa85f64-5717-4562-b3fc-2c963f66afa7',
],
})
@IsArray()
@ArrayMinSize(1, { message: 'At least one space must be selected' })
@IsUUID('all', { each: true, message: 'Invalid space UUID provided' })
spaceUuids: string[];
@ApiProperty({
enum: DaysEnum,
isArray: true,
example: [DaysEnum.MON, DaysEnum.WED, DaysEnum.FRI],
})
@IsArray()
@ArrayMinSize(1, { message: 'At least one day must be selected' })
@IsEnum(DaysEnum, { each: true, message: 'Invalid day provided' })
daysAvailable: DaysEnum[];
@ApiProperty({ example: '09:00' })
@IsString()
@IsNotEmpty({ message: 'Start time cannot be empty' })
@Matches(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/, {
message: 'Start time must be in HH:mm format (24-hour)',
})
startTime: string;
@ApiProperty({ example: '17:00' })
@IsString()
@IsNotEmpty({ message: 'End time cannot be empty' })
@Matches(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/, {
message: 'End time must be in HH:mm format (24-hour)',
})
endTime: string;
@ApiProperty({ example: 10, required: false })
@IsOptional()
@IsInt()
@Min(0, { message: 'Points cannot be negative' })
@Max(1000, { message: 'Points cannot exceed 1000' })
points?: number;
}

View File

@ -1,35 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsDate, IsNotEmpty, IsString, IsUUID, Matches } from 'class-validator';
export class CreateBookingDto {
@ApiProperty({
type: 'string',
example: '4fa85f64-5717-4562-b3fc-2c963f66afa7',
})
@IsNotEmpty()
@IsUUID('4', { message: 'Invalid space UUID provided' })
spaceUuid: string;
@ApiProperty({
type: Date,
})
@IsNotEmpty()
@IsDate()
date: Date;
@ApiProperty({ example: '09:00' })
@IsString()
@IsNotEmpty({ message: 'Start time cannot be empty' })
@Matches(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/, {
message: 'Start time must be in HH:mm format (24-hour)',
})
startTime: string;
@ApiProperty({ example: '17:00' })
@IsString()
@IsNotEmpty({ message: 'End time cannot be empty' })
@Matches(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/, {
message: 'End time must be in HH:mm format (24-hour)',
})
endTime: string;
}

View File

@ -1,14 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsIn, IsOptional } from 'class-validator';
export class MyBookingRequestDto {
@ApiProperty({
description: 'Filter bookings by time period',
example: 'past',
enum: ['past', 'future'],
required: false,
})
@IsOptional()
@IsIn(['past', 'future'])
when?: 'past' | 'future';
}

View File

@ -1,12 +0,0 @@
import { ApiProperty, OmitType, PartialType } from '@nestjs/swagger';
import { IsBoolean, IsOptional } from 'class-validator';
import { CreateBookableSpaceDto } from './create-bookable-space.dto';
export class UpdateBookableSpaceDto extends PartialType(
OmitType(CreateBookableSpaceDto, ['spaceUuids']),
) {
@ApiProperty({ type: Boolean })
@IsOptional()
@IsBoolean()
active?: boolean;
}

View File

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

View File

@ -1,370 +0,0 @@
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { PageResponseDto } from '@app/common/dto/pagination.response.dto';
import { timeToMinutes } from '@app/common/helper/timeToMinutes';
import { TypeORMCustomModel } from '@app/common/models/typeOrmCustom.model';
import { BookableSpaceEntityRepository } from '@app/common/modules/booking/repositories/bookable-space.repository';
import { BookingEntityRepository } from '@app/common/modules/booking/repositories/booking.repository';
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
import { SpaceRepository } from '@app/common/modules/space/repositories/space.repository';
import { EmailService } from '@app/common/util/email/email.service';
import { to12HourFormat } from '@app/common/util/time-to-12-hours-convetion';
import {
BadRequestException,
ConflictException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { format } from 'date-fns';
import { Brackets, In } from 'typeorm';
import { BookableSpaceRequestDto } from '../dtos/bookable-space-request.dto';
import { CreateBookableSpaceDto } from '../dtos/create-bookable-space.dto';
import { UpdateBookableSpaceDto } from '../dtos/update-bookable-space.dto';
@Injectable()
export class BookableSpaceService {
constructor(
private readonly emailService: EmailService,
private readonly bookableSpaceEntityRepository: BookableSpaceEntityRepository,
private readonly bookingEntityRepository: BookingEntityRepository,
private readonly spaceRepository: SpaceRepository,
) {}
async create(dto: CreateBookableSpaceDto) {
// Validate time slots first
this.validateTimeSlot(dto.startTime, dto.endTime);
// fetch spaces exist
const spaces = await this.getSpacesOrFindMissing(dto.spaceUuids);
// Validate no duplicate bookable configurations
await this.validateNoDuplicateBookableConfigs(dto.spaceUuids);
// Create and save bookable spaces
return this.createBookableSpaces(spaces, dto);
}
async findAll(
{ active, page, size, configured, search }: BookableSpaceRequestDto,
project: string,
): Promise<{
data: BaseResponseDto['data'];
pagination: PageResponseDto;
}> {
let qb = this.spaceRepository
.createQueryBuilder('space')
.leftJoinAndSelect('space.parent', 'parentSpace')
.leftJoinAndSelect('space.community', 'community')
.where('community.project = :project', { project });
if (search) {
qb = qb.andWhere(
'(space.spaceName ILIKE :search OR community.name ILIKE :search OR parentSpace.spaceName ILIKE :search)',
{ search: `%${search}%` },
);
}
if (configured) {
qb = qb
.leftJoinAndSelect('space.bookableConfig', 'bookableConfig')
.andWhere('bookableConfig.uuid IS NOT NULL');
if (active !== undefined) {
qb = qb.andWhere('bookableConfig.active = :active', { active });
}
} else {
qb = qb
.leftJoinAndSelect('space.bookableConfig', 'bookableConfig')
.andWhere('bookableConfig.uuid IS NULL');
}
const customModel = TypeORMCustomModel(this.spaceRepository);
const { baseResponseDto, paginationResponseDto } =
await customModel.findAll({ page, size, modelName: 'space' }, qb);
return {
data: baseResponseDto.data.map((space) => {
return {
...space,
virtualLocation: `${space.community?.name} - ${space.parent ? space.parent?.spaceName + ' - ' : ''}${space.spaceName}`,
};
}),
pagination: paginationResponseDto,
};
}
/**
* Update bookable space configuration
*/
async update(spaceUuid: string, dto: UpdateBookableSpaceDto) {
// fetch spaces exist
const space = (await this.getSpacesOrFindMissing([spaceUuid]))[0];
if (!space.bookableConfig) {
throw new NotFoundException(
`Bookable configuration not found for space: ${spaceUuid}`,
);
}
if (dto.startTime || dto.endTime) {
// Validate time slots first
this.validateTimeSlot(
dto.startTime || space.bookableConfig.startTime,
dto.endTime || space.bookableConfig.endTime,
);
if (
dto.startTime != space.bookableConfig.startTime ||
dto.endTime != space.bookableConfig.endTime ||
dto.daysAvailable != space.bookableConfig.daysAvailable
) {
this.handleTimingUpdate(
{
daysAvailable:
dto.daysAvailable || space.bookableConfig.daysAvailable,
startTime: dto.startTime || space.bookableConfig.startTime,
endTime: dto.endTime || space.bookableConfig.endTime,
},
space,
);
}
}
if (
dto.active !== undefined &&
dto.active !== space.bookableConfig.active
) {
this.handleAvailabilityUpdate(dto.active, space);
}
Object.assign(space.bookableConfig, dto);
return this.bookableSpaceEntityRepository.save(space.bookableConfig);
}
private async handleTimingUpdate(
dto: UpdateBookableSpaceDto,
space: SpaceEntity,
): Promise<void> {
const affectedUsers = await this.getAffectedBookings(space.uuid);
if (!affectedUsers.length) return;
const groupedParams = this.groupBookingsByUser(affectedUsers);
return this.emailService.sendUpdateBookingTimingEmailWithTemplate(
groupedParams,
{
space_name: space.spaceName,
start_time: to12HourFormat(dto.startTime),
end_time: to12HourFormat(dto.endTime),
days: dto.daysAvailable.join(', '),
},
);
}
private async getAffectedBookings(spaceUuid: string) {
const today = new Date();
const nowTime = format(today, 'HH:mm');
const bookingWithDayCte = this.bookingEntityRepository
.createQueryBuilder('b')
.select('b.*')
.addSelect(
`
CASE EXTRACT(DOW FROM b.date)
WHEN 0 THEN 'Sun'
WHEN 1 THEN 'Mon'
WHEN 2 THEN 'Tue'
WHEN 3 THEN 'Wed'
WHEN 4 THEN 'Thu'
WHEN 5 THEN 'Fri'
WHEN 6 THEN 'Sat'
END::"bookable-space_days_available_enum"
`,
'booking_day',
)
.where(
`(DATE(b.date) > :today OR (DATE(b.date) = :today AND b.startTime >= :nowTime))`,
{ today, nowTime },
)
.andWhere('b.space_uuid = :spaceUuid', { spaceUuid });
const query = this.bookableSpaceEntityRepository
.createQueryBuilder('bs')
.distinct(true)
.addCommonTableExpression(bookingWithDayCte, 'booking_with_day')
.select('u.first_name', 'name')
.addSelect('u.email', 'email')
.addSelect('DATE(bwd.date)', 'date')
.addSelect('bwd.start_time', 'start_time')
.addSelect('bwd.end_time', 'end_time')
.from('booking_with_day', 'bwd')
.innerJoin('user', 'u', 'u.uuid = bwd.user_uuid')
.where('bs.space_uuid = :spaceUuid', { spaceUuid })
.andWhere(
new Brackets((qb) => {
qb.where('NOT (bwd.booking_day = ANY(bs.days_available))')
.orWhere('bwd.start_time < bs.start_time')
.orWhere('bwd.end_time > bs.end_time');
}),
);
return query.getRawMany<{
name: string;
email: string;
date: string;
start_time: string;
end_time: string;
}>();
}
private groupBookingsByUser(
bookings: {
name: string;
email: string;
date: string;
start_time: string;
end_time: string;
}[],
): {
name: string;
email: string;
bookings: { date: string; start_time: string; end_time: string }[];
}[] {
const grouped: Record<
string,
{
name: string;
email: string;
bookings: { date: string; start_time: string; end_time: string }[];
}
> = {};
for (const { name, email, date, start_time, end_time } of bookings) {
const formattedDate = format(new Date(date), 'yyyy-MM-dd');
const formattedStartTime = to12HourFormat(start_time);
const formattedEndTime = to12HourFormat(end_time);
if (!grouped[email]) {
grouped[email] = {
name,
email,
bookings: [],
};
}
grouped[email].bookings.push({
date: formattedDate,
start_time: formattedStartTime,
end_time: formattedEndTime,
});
}
return Object.values(grouped);
}
private async handleAvailabilityUpdate(
active: boolean,
space: SpaceEntity,
): Promise<void> {
space = await this.spaceRepository.findOne({
where: { uuid: space.uuid },
relations: ['userSpaces', 'userSpaces.user'],
});
const emails = space.userSpaces.map((userSpace) => ({
email: userSpace.user.email,
name: userSpace.user.firstName,
}));
if (!emails.length) return Promise.resolve();
return this.emailService.sendUpdateBookingAvailabilityEmailWithTemplate(
emails,
{
availability: active ? 'Available' : 'Unavailable',
space_name: space.spaceName,
isAvailable: active,
},
);
}
/**
* Fetch spaces by UUIDs and throw an error if any are missing
*/
private async getSpacesOrFindMissing(
spaceUuids: string[],
): Promise<SpaceEntity[]> {
const spaces = await this.spaceRepository.find({
where: { uuid: In(spaceUuids) },
relations: ['bookableConfig'],
});
if (spaces.length !== spaceUuids.length) {
const foundUuids = spaces.map((s) => s.uuid);
const missingUuids = spaceUuids.filter(
(uuid) => !foundUuids.includes(uuid),
);
throw new NotFoundException(
`Spaces not found: ${missingUuids.join(', ')}`,
);
}
return spaces;
}
/**
* Validate there are no existing bookable configurations for these spaces
*/
private async validateNoDuplicateBookableConfigs(
spaceUuids: string[],
): Promise<void> {
const existingBookables = await this.bookableSpaceEntityRepository.find({
where: { space: { uuid: In(spaceUuids) } },
relations: ['space'],
});
if (existingBookables.length > 0) {
const existingUuids = [
...new Set(existingBookables.map((b) => b.space.uuid)),
];
throw new ConflictException(
`Bookable configuration already exists for spaces: ${existingUuids.join(', ')}`,
);
}
}
/**
* Ensure the slot start time is before the end time
*/
private validateTimeSlot(startTime: string, endTime: string): void {
const start = timeToMinutes(startTime);
const end = timeToMinutes(endTime);
if (start >= end) {
throw new BadRequestException(
`End time must be after start time for slot: ${startTime}-${endTime}`,
);
}
}
/**
* Create bookable space entries after all validations pass
*/
private async createBookableSpaces(
spaces: SpaceEntity[],
dto: CreateBookableSpaceDto,
) {
try {
const entries = spaces.map((space) =>
this.bookableSpaceEntityRepository.create({
space,
daysAvailable: dto.daysAvailable,
startTime: dto.startTime,
endTime: dto.endTime,
points: dto.points,
}),
);
return this.bookableSpaceEntityRepository.save(entries);
} catch (error) {
if (error.code === '23505') {
throw new ConflictException(
'Duplicate bookable space configuration detected',
);
}
throw error;
}
}
}

View File

@ -1,218 +0,0 @@
import { DaysEnum } from '@app/common/constants/days.enum';
import { timeToMinutes } from '@app/common/helper/timeToMinutes';
import { BookingEntityRepository } from '@app/common/modules/booking/repositories/booking.repository';
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
import { SpaceRepository } from '@app/common/modules/space/repositories/space.repository';
import { UserRepository } from '@app/common/modules/user/repositories/user.repository';
import {
BadRequestException,
ConflictException,
ForbiddenException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { format } from 'date-fns';
import { Between } from 'typeorm/find-options/operator/Between';
import { BookingRequestDto } from '../dtos/booking-request.dto';
import { CreateBookingDto } from '../dtos/create-booking.dto';
import { MyBookingRequestDto } from '../dtos/my-booking-request.dto';
@Injectable()
export class BookingService {
constructor(
private readonly bookingEntityRepository: BookingEntityRepository,
private readonly spaceRepository: SpaceRepository,
private readonly userRepository: UserRepository,
) {}
async create(userUuid: string, dto: CreateBookingDto) {
console.log(userUuid);
const user = await this.userRepository.findOne({
where: { uuid: userUuid },
relations: ['userSpaces', 'userSpaces.space'],
});
console.log(user.userSpaces);
if (!user.userSpaces.some(({ space }) => space.uuid === dto.spaceUuid)) {
throw new ForbiddenException(
`User does not have permission to book this space: ${dto.spaceUuid}`,
);
}
// Validate time slots first
this.validateTimeSlot(dto.startTime, dto.endTime);
// fetch spaces exist
const space = await this.getSpaceConfigurationAndBookings(dto.spaceUuid);
// Validate booking availability
this.validateBookingAvailability(space, dto);
// Create and save booking
return this.createBookings(space, userUuid, dto);
}
async findAll({ month, space }: BookingRequestDto, project: string) {
const [monthNumber, year] = month.split('/').map(Number);
const fromDate = new Date(year, monthNumber - 1, 1);
const toDate = new Date(year, monthNumber, 0, 23, 59, 59);
return this.bookingEntityRepository.find({
where: {
space: {
community: { project: { uuid: project } },
uuid: space ? space : undefined,
},
date: Between(fromDate, toDate),
},
relations: ['space', 'user', 'user.inviteUser'],
order: { date: 'DESC' },
});
}
async findMyBookings(
{ when }: MyBookingRequestDto,
userUuid: string,
project: string,
) {
const now = new Date();
const nowTime = format(now, 'HH:mm');
const query = this.bookingEntityRepository
.createQueryBuilder('booking')
.leftJoinAndSelect('booking.space', 'space')
.innerJoin(
'space.community',
'community',
'community.project = :project',
{ project },
)
.leftJoinAndSelect('booking.user', 'user')
.where('user.uuid = :userUuid', { userUuid });
if (when === 'past') {
query.andWhere(
`(DATE(booking.date) < :today OR (DATE(booking.date) = :today AND booking.startTime < :nowTime))`,
{ today: now, nowTime },
);
} else if (when === 'future') {
query.andWhere(
`(DATE(booking.date) > :today OR (DATE(booking.date) = :today AND booking.startTime >= :nowTime))`,
{ today: now, nowTime },
);
}
query.orderBy({
'DATE(booking.date)': 'DESC',
'booking.startTime': 'DESC',
});
return query.getMany();
}
/**
* Fetch space by UUID and throw an error if not found or if not configured for booking
*/
private async getSpaceConfigurationAndBookings(
spaceUuid: string,
): Promise<SpaceEntity> {
const space = await this.spaceRepository.findOne({
where: { uuid: spaceUuid },
relations: ['bookableConfig', 'bookings'],
});
if (!space) {
throw new NotFoundException(`Space not found: ${spaceUuid}`);
}
if (!space.bookableConfig) {
throw new NotFoundException(
`This space is not configured for booking: ${spaceUuid}`,
);
}
return space;
}
/**
* Ensure the slot start time is before the end time
*/
private validateTimeSlot(startTime: string, endTime: string): void {
const start = timeToMinutes(startTime);
const end = timeToMinutes(endTime);
if (start >= end) {
throw new BadRequestException(
`End time must be after start time for slot: ${startTime}-${endTime}`,
);
}
}
/**
* check if the space is available for booking on the requested day
* and if the requested time slot is within the available hours
*/
private validateBookingAvailability(
space: SpaceEntity,
dto: CreateBookingDto,
): void {
// Check if the space is available for booking on the requested day
const availableDays = space.bookableConfig?.daysAvailable || [];
const requestedDay = new Date(dto.date).toLocaleDateString('en-US', {
weekday: 'short',
}) as DaysEnum;
if (!availableDays.includes(requestedDay)) {
const dayFullName = new Date(dto.date).toLocaleDateString('en-US', {
weekday: 'long',
});
throw new BadRequestException(
`Space is not available for booking on ${dayFullName}s`,
);
}
const dtoStartTimeInMinutes = timeToMinutes(dto.startTime);
const dtoEndTimeInMinutes = timeToMinutes(dto.endTime);
if (
dtoStartTimeInMinutes < timeToMinutes(space.bookableConfig.startTime) ||
dtoEndTimeInMinutes > timeToMinutes(space.bookableConfig.endTime)
) {
throw new BadRequestException(
`Booking time must be within the available hours for space: ${space.spaceName}`,
);
}
const previousBookings = space.bookings.filter(
(booking) =>
timeToMinutes(booking.startTime) < dtoEndTimeInMinutes &&
timeToMinutes(booking.endTime) > dtoStartTimeInMinutes &&
format(new Date(booking.date), 'yyyy-MM-dd') ===
format(new Date(dto.date), 'yyyy-MM-dd'),
);
if (previousBookings.length > 0) {
// tell the user what time is unavailable
const unavailableTimes = previousBookings.map((booking) => {
return `${booking.startTime}-${booking.endTime}`;
});
throw new ConflictException(
`Space is already booked during this times: ${unavailableTimes.join(', ')}`,
);
}
}
/**
* Create bookable space entries after all validations pass
*/
private async createBookings(
space: SpaceEntity,
user: string,
{ spaceUuid, date, ...dto }: CreateBookingDto,
) {
const entry = this.bookingEntityRepository.create({
space: { uuid: spaceUuid },
user: { uuid: user },
...dto,
date: new Date(date),
cost: space.bookableConfig?.points || null,
});
return this.bookingEntityRepository.save(entry);
}
}

View File

@ -3,7 +3,6 @@ import * as fs from 'fs';
import { ProjectParam } from '@app/common/dto/project-param.dto';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { DeviceStatusFirebaseService } from '@app/common/firebase/devices-status/services/devices-status.service';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { CommunityRepository } from '@app/common/modules/community/repositories';
import { DeviceRepository } from '@app/common/modules/device/repositories';
@ -21,7 +20,6 @@ export class DeviceCommissionService {
constructor(
private readonly tuyaService: TuyaService,
private readonly deviceService: DeviceService,
private readonly deviceStatusFirebaseService: DeviceStatusFirebaseService,
private readonly communityRepository: CommunityRepository,
private readonly spaceRepository: SpaceRepository,
private readonly subspaceRepository: SubspaceRepository,
@ -211,10 +209,6 @@ export class DeviceCommissionService {
rawDeviceId,
tuyaSpaceId,
);
await this.deviceStatusFirebaseService.addDeviceStatusByDeviceUuid(
rawDeviceId,
);
successCount.value++;
console.log(
`Device ${rawDeviceId} successfully processed and transferred to Tuya space ${tuyaSpaceId}`,

View File

@ -5,6 +5,7 @@ import { ConfigModule } from '@nestjs/config';
import { SpaceRepositoryModule } from '@app/common/modules/space/space.repository.module';
import {
InviteSpaceRepository,
SpaceLinkRepository,
SpaceProductAllocationRepository,
SpaceRepository,
} from '@app/common/modules/space/repositories';
@ -15,12 +16,14 @@ import { CommunityRepository } from '@app/common/modules/community/repositories'
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { ProjectRepository } from '@app/common/modules/project/repositiories';
import {
SpaceLinkService,
SpaceService,
SubspaceDeviceService,
SubSpaceService,
ValidationService,
} from 'src/space/services';
import { TagService as NewTagService } from 'src/tags/services';
import { TagService } from 'src/space/services/tag';
import {
SpaceModelService,
SubSpaceModelService,
@ -78,13 +81,16 @@ import { AqiSpaceDailyPollutantStatsRepository } from '@app/common/modules/aqi/r
SpaceService,
InviteSpaceRepository,
// Todo: find out why this is needed
SpaceLinkService,
SubSpaceService,
ValidationService,
NewTagService,
SpaceModelService,
SpaceProductAllocationService,
SpaceLinkRepository,
SubspaceRepository,
// Todo: find out why this is needed
TagService,
SubspaceDeviceService,
SubspaceProductAllocationService,
SpaceModelRepository,

View File

@ -120,7 +120,6 @@ export class CommunityService {
.leftJoin('c.spaces', 's', 's.disabled = false')
.where('c.project = :projectUuid', { projectUuid })
.andWhere(`c.name != '${ORPHAN_COMMUNITY_NAME}-${project.name}'`)
.orderBy('c.createdAt', 'DESC')
.distinct(true);
if (pageable.search) {
qb.andWhere(
@ -210,7 +209,7 @@ export class CommunityService {
if (search) {
qb.andWhere(
`c.name ILIKE :search ${includeSpaces ? 'OR space.space_name ILIKE :search' : ''}`,
{ search: `%${search}%` },
{ search },
);
}

View File

@ -1,41 +1,39 @@
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 { DeviceService } from '../services/device.service';
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
Query,
Req,
UnauthorizedException,
Param,
UseGuards,
Put,
Delete,
Req,
} from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { Permissions } from 'src/decorators/permissions.decorator';
import { PermissionsGuard } from 'src/guards/permissions.guard';
import { CheckRoomGuard } from 'src/guards/room.guard';
import { CheckFourAndSixSceneDeviceTypeGuard } from 'src/guards/scene.device.type.guard';
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
import {
AddDeviceDto,
AddSceneToFourSceneDeviceDto,
AssignDeviceToSpaceDto,
UpdateDeviceDto,
} from '../dtos/add.device.dto';
import { GetDeviceLogsDto } from '../dtos/get.device.dto';
import {
ControlDeviceDto,
BatchControlDevicesDto,
BatchStatusDevicesDto,
ControlDeviceDto,
GetSceneFourSceneDeviceDto,
} from '../dtos/control.device.dto';
import { DeleteSceneFromSceneDeviceDto } from '../dtos/delete.device.dto';
import { CheckRoomGuard } from 'src/guards/room.guard';
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
import { CheckFourAndSixSceneDeviceTypeGuard } from 'src/guards/scene.device.type.guard';
import { ControllerRoute } from '@app/common/constants/controller-route';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { DeviceSceneParamDto } from '../dtos/device.param.dto';
import { GetDeviceLogsDto } from '../dtos/get.device.dto';
import { DeviceService } from '../services/device.service';
import { DeleteSceneFromSceneDeviceDto } from '../dtos/delete.device.dto';
import { PermissionsGuard } from 'src/guards/permissions.guard';
import { Permissions } from 'src/decorators/permissions.decorator';
@ApiTags('Device Module')
@Controller({
@ -342,22 +340,4 @@ export class DeviceController {
projectUuid,
);
}
@ApiBearerAuth()
@UseGuards(PermissionsGuard)
@Permissions('DEVICE_UPDATE')
@Post('/populate-tuya-const-uuids')
@ApiOperation({
summary: ControllerRoute.DEVICE.ACTIONS.POPULATE_TUYA_CONST_UUID_SUMMARY,
description:
ControllerRoute.DEVICE.ACTIONS.POPULATE_TUYA_CONST_UUID_DESCRIPTION,
})
async populateTuyaConstUuid(@Req() req: any): Promise<void> {
const userUuid = req['user']?.userUuid;
const userRole = req['user']?.role;
if (!userUuid || (userRole && userRole !== RoleType.SUPER_ADMIN)) {
throw new UnauthorizedException('Unauthorized to perform this action');
}
return this.deviceService.addTuyaConstUuidToDevices();
}
}

View File

@ -18,14 +18,6 @@ export class AddDeviceDto {
@IsNotEmpty()
public spaceUuid: string;
@ApiProperty({
description: 'tagUuid',
required: true,
})
@IsString()
@IsNotEmpty()
public tagUuid: string;
@ApiProperty({
description: 'deviceName',
required: true,

View File

@ -1,7 +1,7 @@
import { DeviceTypeEnum } from '@app/common/constants/device-type.enum';
import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import {
IsArray,
IsEnum,
IsNotEmpty,
IsOptional,
@ -74,34 +74,13 @@ export class GetDevicesFilterDto {
@IsEnum(DeviceTypeEnum)
@IsOptional()
public deviceType: DeviceTypeEnum;
@ApiProperty({
description: 'List of Space IDs to filter devices',
required: false,
example: ['60d21b4667d0d8992e610c85', '60d21b4967d0d8992e610c86'],
})
@IsOptional()
@Transform(({ value }) => {
if (!Array.isArray(value)) {
return [value];
}
return value;
})
@IsArray()
@IsUUID('4', { each: true })
public spaces?: string[];
@ApiProperty({
description: 'List of Community IDs to filter devices',
required: false,
example: ['60d21b4667d0d8992e610c85', '60d21b4967d0d8992e610c86'],
})
@Transform(({ value }) => {
if (!Array.isArray(value)) {
return [value];
}
return value;
})
@IsOptional()
@IsUUID('4', { each: true })
public communities?: string[];
}

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,6 @@ import {
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
} from 'class-validator';
export class AddUserInvitationDto {
@ -45,15 +44,6 @@ export class AddUserInvitationDto {
@IsOptional()
public jobTitle?: string;
@ApiProperty({
description: 'The company name of the user',
example: 'Tech Corp',
required: false,
})
@IsString()
@IsOptional()
public companyName?: string;
@ApiProperty({
description: 'The phone number of the user',
example: '+1234567890',
@ -68,7 +58,7 @@ export class AddUserInvitationDto {
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
required: true,
})
@IsUUID('4')
@IsString()
@IsNotEmpty()
public roleUuid: string;
@ApiProperty({
@ -76,17 +66,15 @@ export class AddUserInvitationDto {
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
required: true,
})
@IsUUID('4')
@IsString()
@IsNotEmpty()
public projectUuid: string;
@ApiProperty({
description: 'The array of space UUIDs (at least one required)',
example: ['b5f3c9d2-58b7-4377-b3f7-60acb711d5d9'],
required: true,
})
@IsArray()
@IsUUID('4', { each: true })
@ArrayMinSize(1)
public spaceUuids: string[];
constructor(dto: Partial<AddUserInvitationDto>) {

View File

@ -1,10 +1,78 @@
import { ApiProperty, OmitType } from '@nestjs/swagger';
import { IsBoolean, IsNotEmpty, IsString } from 'class-validator';
import { AddUserInvitationDto } from './add.invite-user.dto';
import { ApiProperty } from '@nestjs/swagger';
import {
ArrayMinSize,
IsArray,
IsBoolean,
IsNotEmpty,
IsOptional,
IsString,
} from 'class-validator';
export class UpdateUserInvitationDto extends OmitType(AddUserInvitationDto, [
'email',
]) {}
export class UpdateUserInvitationDto {
@ApiProperty({
description: 'The first name of the user',
example: 'John',
required: true,
})
@IsString()
@IsNotEmpty()
public firstName: string;
@ApiProperty({
description: 'The last name of the user',
example: 'Doe',
required: true,
})
@IsString()
@IsNotEmpty()
public lastName: string;
@ApiProperty({
description: 'The job title of the user',
example: 'Software Engineer',
required: false,
})
@IsString()
@IsOptional()
public jobTitle?: string;
@ApiProperty({
description: 'The phone number of the user',
example: '+1234567890',
required: false,
})
@IsString()
@IsOptional()
public phoneNumber?: string;
@ApiProperty({
description: 'The role uuid of the user',
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
required: true,
})
@IsString()
@IsNotEmpty()
public roleUuid: string;
@ApiProperty({
description: 'The project uuid of the user',
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
required: true,
})
@IsString()
@IsNotEmpty()
public projectUuid: string;
@ApiProperty({
description: 'The array of space UUIDs (at least one required)',
example: ['b5f3c9d2-58b7-4377-b3f7-60acb711d5d9'],
required: true,
})
@IsArray()
@ArrayMinSize(1)
public spaceUuids: string[];
constructor(dto: Partial<UpdateUserInvitationDto>) {
Object.assign(this, dto);
}
}
export class DisableUserInvitationDto {
@ApiProperty({
description: 'The disable status of the user',

View File

@ -9,7 +9,6 @@ import { OccupancyService } from '@app/common/helper/services/occupancy.service'
import { PowerClampService } from '@app/common/helper/services/power.clamp.service';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service';
import { AqiSpaceDailyPollutantStatsRepository } from '@app/common/modules/aqi/repositories';
import { AutomationRepository } from '@app/common/modules/automation/repositories';
import { CommunityRepository } from '@app/common/modules/community/repositories';
import { DeviceStatusLogRepository } from '@app/common/modules/device-status-log/repositories';
@ -28,7 +27,6 @@ import {
PowerClampHourlyRepository,
PowerClampMonthlyRepository,
} from '@app/common/modules/power-clamp/repositories';
import { PresenceSensorDailySpaceRepository } from '@app/common/modules/presence-sensor/repositories';
import { ProductRepository } from '@app/common/modules/product/repositories';
import { ProjectRepository } from '@app/common/modules/project/repositiories';
import { RegionRepository } from '@app/common/modules/region/repositories';
@ -40,6 +38,7 @@ import {
} from '@app/common/modules/scene/repositories';
import {
InviteSpaceRepository,
SpaceLinkRepository,
SpaceProductAllocationRepository,
SpaceRepository,
} from '@app/common/modules/space';
@ -59,7 +58,7 @@ import {
UserRepository,
UserSpaceRepository,
} from '@app/common/modules/user/repositories';
import { EmailService } from '@app/common/util/email/email.service';
import { EmailService } from '@app/common/util/email.service';
import { CommunityModule } from 'src/community/community.module';
import { CommunityService } from 'src/community/services';
import { DeviceService } from 'src/device/services';
@ -83,6 +82,8 @@ import { SubspaceProductAllocationService } from 'src/space/services/subspace/su
import { TagService as NewTagService } from 'src/tags/services';
import { UserDevicePermissionService } from 'src/user-device-permission/services';
import { UserService, UserSpaceService } from 'src/users/services';
import { PresenceSensorDailySpaceRepository } from '@app/common/modules/presence-sensor/repositories';
import { AqiSpaceDailyPollutantStatsRepository } from '@app/common/modules/aqi/repositories';
@Module({
imports: [ConfigModule, InviteUserRepositoryModule, CommunityModule],
@ -120,6 +121,7 @@ import { UserService, UserSpaceService } from 'src/users/services';
NewTagService,
SpaceModelService,
SpaceProductAllocationService,
SpaceLinkRepository,
SubspaceRepository,
SubspaceDeviceService,
SubspaceProductAllocationService,

View File

@ -8,14 +8,12 @@ import {
InviteUserRepository,
InviteUserSpaceRepository,
} from '@app/common/modules/Invite-user/repositiories';
import { ProjectEntity } from '@app/common/modules/project/entities';
import { RoleTypeEntity } from '@app/common/modules/role-type/entities';
import { RoleTypeRepository } from '@app/common/modules/role-type/repositories';
import { SpaceRepository } from '@app/common/modules/space';
import { SpaceEntity } from '@app/common/modules/space/entities/space.entity';
import { UserEntity } from '@app/common/modules/user/entities';
import { UserRepository } from '@app/common/modules/user/repositories';
import { EmailService } from '@app/common/util/email/email.service';
import { EmailService } from '@app/common/util/email.service';
import {
BadRequestException,
HttpException,
@ -63,7 +61,6 @@ export class InviteUserService {
lastName,
email,
jobTitle,
companyName,
phoneNumber,
roleUuid,
spaceUuids,
@ -93,8 +90,6 @@ export class InviteUserService {
);
}
await this.checkRole(roleUuid, queryRunner);
await this.checkProject(projectUuid, queryRunner);
// Validate spaces
const validSpaces = await this.validateSpaces(
spaceUuids,
@ -107,7 +102,6 @@ export class InviteUserService {
lastName,
email,
jobTitle,
companyName,
phoneNumber,
roleType: { uuid: roleUuid },
status: UserStatusEnum.INVITED,
@ -117,7 +111,6 @@ export class InviteUserService {
});
const invitedUser = await queryRunner.manager.save(inviteUser);
const invitedRoleType = await this.getRoleTypeByUuid(roleUuid);
// Link user to spaces
const spacePromises = validSpaces.map(async (space) => {
@ -135,7 +128,7 @@ export class InviteUserService {
await this.emailService.sendEmailWithInvitationTemplate(email, {
name: firstName,
invitationCode,
role: invitedRoleType.replace(/_/g, ' '),
role: roleType,
spacesList: spaceNames,
});
@ -163,6 +156,185 @@ export class InviteUserService {
await queryRunner.release();
}
}
private async validateSpaces(
spaceUuids: string[],
entityManager: EntityManager,
): Promise<SpaceEntity[]> {
const spaceRepo = entityManager.getRepository(SpaceEntity);
const validSpaces = await spaceRepo.find({
where: { uuid: In(spaceUuids) },
});
if (validSpaces.length !== spaceUuids.length) {
const validSpaceUuids = validSpaces.map((space) => space.uuid);
const invalidSpaceUuids = spaceUuids.filter(
(uuid) => !validSpaceUuids.includes(uuid),
);
throw new HttpException(
`Invalid space UUIDs: ${invalidSpaceUuids.join(', ')}`,
HttpStatus.BAD_REQUEST,
);
}
return validSpaces;
}
async checkEmailAndProject(dto: CheckEmailDto): Promise<BaseResponseDto> {
const { email } = dto;
try {
const user = await this.userRepository.findOne({
where: { email },
relations: ['project'],
});
this.validateUserOrInvite(user, 'User');
const invitedUser = await this.inviteUserRepository.findOne({
where: { email },
relations: ['project'],
});
this.validateUserOrInvite(invitedUser, 'Invited User');
return new SuccessResponseDto({
statusCode: HttpStatus.OK,
success: true,
message: 'Valid email',
});
} catch (error) {
console.error('Error checking email and project:', error);
throw new HttpException(
error.message ||
'An unexpected error occurred while checking the email',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private validateUserOrInvite(user: any, userType: string): void {
if (user) {
if (!user.isActive) {
throw new HttpException(
`${userType} is deleted`,
HttpStatus.BAD_REQUEST,
);
}
if (user.project) {
throw new HttpException(
`${userType} already has a project`,
HttpStatus.BAD_REQUEST,
);
}
}
}
async activationCode(dto: ActivateCodeDto): Promise<BaseResponseDto> {
const { activationCode, userUuid } = dto;
try {
const user = await this.getUser(userUuid);
const invitedUser = await this.inviteUserRepository.findOne({
where: {
email: user.email,
status: UserStatusEnum.INVITED,
isActive: true,
},
relations: ['project', 'spaces.space.community', 'roleType'],
});
if (invitedUser) {
if (invitedUser.invitationCode !== activationCode) {
throw new HttpException(
'Invalid activation code',
HttpStatus.BAD_REQUEST,
);
}
// Handle invited user with valid activation code
await this.handleInvitedUser(user, invitedUser);
} else {
// Handle case for non-invited user
await this.handleNonInvitedUser(activationCode, userUuid);
}
return new SuccessResponseDto({
statusCode: HttpStatus.OK,
success: true,
message: 'The code has been successfully activated',
});
} catch (error) {
console.error('Error activating the code:', error);
throw new HttpException(
error.message ||
'An unexpected error occurred while activating the code',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private async getUser(userUuid: string): Promise<UserEntity> {
const user = await this.userRepository.findOne({
where: { uuid: userUuid, isActive: true, isUserVerified: true },
});
if (!user) {
throw new HttpException('User not found', HttpStatus.NOT_FOUND);
}
return user;
}
private async handleNonInvitedUser(
activationCode: string,
userUuid: string,
): Promise<void> {
await this.userSpaceService.verifyCodeAndAddUserSpace(
{ inviteCode: activationCode },
userUuid,
);
}
private async handleInvitedUser(
user: UserEntity,
invitedUser: InviteUserEntity,
): Promise<void> {
for (const invitedSpace of invitedUser.spaces) {
try {
const deviceUUIDs = await this.userSpaceService.getDeviceUUIDsForSpace(
invitedSpace.space.uuid,
);
await this.userSpaceService.addUserPermissionsToDevices(
user.uuid,
deviceUUIDs,
);
await this.spaceUserService.associateUserToSpace({
communityUuid: invitedSpace.space.community.uuid,
spaceUuid: invitedSpace.space.uuid,
userUuid: user.uuid,
projectUuid: invitedUser.project.uuid,
});
} catch (spaceError) {
console.error(
`Error processing space ${invitedSpace.space.uuid}:`,
spaceError,
);
continue; // Skip to the next space
}
}
// Update invited user and associated user data
await this.inviteUserRepository.update(
{ uuid: invitedUser.uuid },
{ status: UserStatusEnum.ACTIVE, user: { uuid: user.uuid } },
);
await this.userRepository.update(
{ uuid: user.uuid },
{
project: { uuid: invitedUser.project.uuid },
roleType: { uuid: invitedUser.roleType.uuid },
},
);
}
async updateUserInvitation(
dto: UpdateUserInvitationDto,
@ -184,9 +356,6 @@ export class InviteUserService {
throw new HttpException('User not found', HttpStatus.NOT_FOUND);
}
await this.checkRole(dto.roleUuid, queryRunner);
await this.checkProject(projectUuid, queryRunner);
// Perform update actions if status is 'INVITED'
if (userOldData.status === UserStatusEnum.INVITED) {
await this.updateWhenUserIsInvite(queryRunner, dto, invitedUserUuid);
@ -269,7 +438,174 @@ export class InviteUserService {
await queryRunner.release();
}
}
private async getRoleTypeByUuid(roleUuid: string) {
const role = await this.roleTypeRepository.findOne({
where: { uuid: roleUuid },
});
return role.type;
}
private async updateWhenUserIsInvite(
queryRunner: QueryRunner,
dto: UpdateUserInvitationDto,
invitedUserUuid: string,
): Promise<void> {
const { firstName, lastName, jobTitle, phoneNumber, roleUuid, spaceUuids } =
dto;
// Update user invitation details
await queryRunner.manager.update(
this.inviteUserRepository.target,
{ uuid: invitedUserUuid },
{
firstName,
lastName,
jobTitle,
phoneNumber,
roleType: { uuid: roleUuid },
},
);
// Remove old space associations
await queryRunner.manager.delete(this.inviteUserSpaceRepository.target, {
inviteUser: { uuid: invitedUserUuid },
});
// Save new space associations
const spaceData = spaceUuids.map((spaceUuid) => ({
inviteUser: { uuid: invitedUserUuid },
space: { uuid: spaceUuid },
}));
await queryRunner.manager.save(
this.inviteUserSpaceRepository.target,
spaceData,
);
}
private async updateWhenUserIsActive(
queryRunner: QueryRunner,
dto: UpdateUserInvitationDto,
invitedUserUuid: string,
): Promise<void> {
const {
firstName,
lastName,
jobTitle,
phoneNumber,
roleUuid,
spaceUuids,
projectUuid,
} = dto;
const userData = await this.inviteUserRepository.findOne({
where: { uuid: invitedUserUuid },
relations: ['user.userSpaces.space', 'user.userSpaces.space.community'],
});
if (!userData) {
throw new HttpException(
`User with invitedUserUuid ${invitedUserUuid} not found`,
HttpStatus.NOT_FOUND,
);
}
// Update user details
await queryRunner.manager.update(
this.inviteUserRepository.target,
{ uuid: invitedUserUuid },
{
firstName,
lastName,
jobTitle,
phoneNumber,
roleType: { uuid: roleUuid },
},
);
await this.userRepository.update(
{ uuid: userData.user.uuid },
{
roleType: { uuid: roleUuid },
},
);
// Disassociate the user from all current spaces
const disassociatePromises = userData.user.userSpaces.map((userSpace) =>
this.spaceUserService
.disassociateUserFromSpace({
communityUuid: userSpace.space.community.uuid,
spaceUuid: userSpace.space.uuid,
userUuid: userData.user.uuid,
projectUuid,
})
.catch((error) => {
console.error(
`Failed to disassociate user from space ${userSpace.space.uuid}:`,
error,
);
throw error;
}),
);
await Promise.allSettled(disassociatePromises);
// Process new spaces
const associatePromises = spaceUuids.map(async (spaceUuid) => {
try {
// Fetch space details
const spaceDetails = await this.getSpaceByUuid(spaceUuid);
// Fetch device UUIDs for the space
const deviceUUIDs =
await this.userSpaceService.getDeviceUUIDsForSpace(spaceUuid);
// Grant permissions to the user for all devices in the space
await this.userSpaceService.addUserPermissionsToDevices(
userData.user.uuid,
deviceUUIDs,
);
// Associate the user with the new space
await this.spaceUserService.associateUserToSpace({
communityUuid: spaceDetails.communityUuid,
spaceUuid: spaceUuid,
userUuid: userData.user.uuid,
projectUuid,
});
} catch (error) {
console.error(`Failed to process space ${spaceUuid}:`, error);
throw error;
}
});
await Promise.all(associatePromises);
}
async getSpaceByUuid(spaceUuid: string) {
try {
const space = await this.spaceRepository.findOne({
where: {
uuid: spaceUuid,
},
relations: ['community'],
});
if (!space) {
throw new BadRequestException(`Invalid space UUID ${spaceUuid}`);
}
return {
uuid: space.uuid,
createdAt: space.createdAt,
updatedAt: space.updatedAt,
name: space.spaceName,
spaceTuyaUuid: space.community.externalId,
communityUuid: space.community.uuid,
};
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException('Space not found', HttpStatus.NOT_FOUND);
}
}
}
async disableUserInvitation(
dto: DisableUserInvitationDto,
invitedUserUuid: string,
@ -349,6 +685,74 @@ export class InviteUserService {
}
}
private async updateUserStatus(
invitedUserUuid: string,
projectUuid: string,
isEnabled: boolean,
) {
await this.inviteUserRepository.update(
{ uuid: invitedUserUuid, project: { uuid: projectUuid } },
{ isEnabled },
);
}
private async disassociateUserFromSpaces(user: any, projectUuid: string) {
const disassociatePromises = user.userSpaces.map((userSpace) =>
this.spaceUserService.disassociateUserFromSpace({
communityUuid: userSpace.space.community.uuid,
spaceUuid: userSpace.space.uuid,
userUuid: user.uuid,
projectUuid,
}),
);
const results = await Promise.allSettled(disassociatePromises);
results.forEach((result, index) => {
if (result.status === 'rejected') {
console.error(
`Failed to disassociate user from space ${user.userSpaces[index].space.uuid}:`,
result.reason,
);
}
});
}
private async associateUserToSpaces(
user: any,
userData: any,
projectUuid: string,
invitedUserUuid: string,
disable: boolean,
) {
const spaceUuids = userData.spaces.map((space) => space.space.uuid);
const associatePromises = spaceUuids.map(async (spaceUuid) => {
try {
const spaceDetails = await this.getSpaceByUuid(spaceUuid);
const deviceUUIDs =
await this.userSpaceService.getDeviceUUIDsForSpace(spaceUuid);
await this.userSpaceService.addUserPermissionsToDevices(
user.uuid,
deviceUUIDs,
);
await this.spaceUserService.associateUserToSpace({
communityUuid: spaceDetails.communityUuid,
spaceUuid,
userUuid: user.uuid,
projectUuid,
});
await this.updateUserStatus(invitedUserUuid, projectUuid, disable);
} catch (error) {
console.error(`Failed to associate user to space ${spaceUuid}:`, error);
}
});
await Promise.allSettled(associatePromises);
}
async deleteUserInvitation(
invitedUserUuid: string,
): Promise<BaseResponseDto> {
@ -419,486 +823,4 @@ export class InviteUserService {
await queryRunner.release();
}
}
async activationCode(dto: ActivateCodeDto): Promise<BaseResponseDto> {
const { activationCode, userUuid } = dto;
try {
const user = await this.getUser(userUuid);
const invitedUser = await this.inviteUserRepository.findOne({
where: {
email: user.email,
status: UserStatusEnum.INVITED,
isActive: true,
},
relations: ['project', 'spaces.space.community', 'roleType'],
});
if (invitedUser) {
if (invitedUser.invitationCode !== activationCode) {
throw new HttpException(
'Invalid activation code',
HttpStatus.BAD_REQUEST,
);
}
// Handle invited user with valid activation code
await this.handleInvitedUser(user, invitedUser);
} else {
// Handle case for non-invited user
await this.handleNonInvitedUser(activationCode, userUuid);
}
return new SuccessResponseDto({
statusCode: HttpStatus.OK,
success: true,
message: 'The code has been successfully activated',
});
} catch (error) {
console.error('Error activating the code:', error);
throw new HttpException(
error.message ||
'An unexpected error occurred while activating the code',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async checkEmailAndProject(dto: CheckEmailDto): Promise<BaseResponseDto> {
const { email } = dto;
try {
const user = await this.userRepository.findOne({
where: { email },
relations: ['project'],
});
this.validateUserOrInvite(user, 'User');
const invitedUser = await this.inviteUserRepository.findOne({
where: { email },
relations: ['project'],
});
this.validateUserOrInvite(invitedUser, 'Invited User');
return new SuccessResponseDto({
statusCode: HttpStatus.OK,
success: true,
message: 'Valid email',
});
} catch (error) {
console.error('Error checking email and project:', error);
throw new HttpException(
error.message ||
'An unexpected error occurred while checking the email',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private async getSpaceByUuid(spaceUuid: string) {
try {
const space = await this.spaceRepository.findOne({
where: {
uuid: spaceUuid,
},
relations: ['community'],
});
if (!space) {
throw new BadRequestException(`Invalid space UUID ${spaceUuid}`);
}
return {
uuid: space.uuid,
createdAt: space.createdAt,
updatedAt: space.updatedAt,
name: space.spaceName,
spaceTuyaUuid: space.community.externalId,
communityUuid: space.community.uuid,
};
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException('Space not found', HttpStatus.NOT_FOUND);
}
}
}
private async validateSpaces(
spaceUuids: string[],
entityManager: EntityManager,
): Promise<SpaceEntity[]> {
const spaceRepo = entityManager.getRepository(SpaceEntity);
const validSpaces = await spaceRepo.find({
where: { uuid: In(spaceUuids) },
});
if (validSpaces.length !== spaceUuids.length) {
const validSpaceUuids = validSpaces.map((space) => space.uuid);
const invalidSpaceUuids = spaceUuids.filter(
(uuid) => !validSpaceUuids.includes(uuid),
);
throw new HttpException(
`Invalid space UUIDs: ${invalidSpaceUuids.join(', ')}`,
HttpStatus.BAD_REQUEST,
);
}
return validSpaces;
}
private async checkRole(
roleUuid: string,
queryRunner: QueryRunner,
): Promise<BaseResponseDto> {
try {
const role = await queryRunner.manager.findOne(RoleTypeEntity, {
where: { uuid: roleUuid },
});
if (!role) {
throw new HttpException('Role not found', HttpStatus.NOT_FOUND);
}
return new SuccessResponseDto({
statusCode: HttpStatus.OK,
success: true,
message: 'Valid role',
});
} catch (error) {
console.error('Error checking role:', error);
throw new HttpException(
error.message || 'An unexpected error occurred while checking the role',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private async checkProject(
projectUuid: string,
queryRunner: QueryRunner,
): Promise<BaseResponseDto> {
try {
const project = await queryRunner.manager.findOne(ProjectEntity, {
where: { uuid: projectUuid },
});
if (!project) {
throw new HttpException('Project not found', HttpStatus.NOT_FOUND);
}
return new SuccessResponseDto({
statusCode: HttpStatus.OK,
success: true,
message: 'Valid project',
});
} catch (error) {
console.error('Error checking project:', error);
throw new HttpException(
error.message ||
'An unexpected error occurred while checking the project',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
private validateUserOrInvite(user: any, userType: string): void {
if (user) {
if (!user.isActive) {
throw new HttpException(
`${userType} is deleted`,
HttpStatus.BAD_REQUEST,
);
}
if (user.project) {
throw new HttpException(
`${userType} already has a project`,
HttpStatus.BAD_REQUEST,
);
}
}
}
private async getUser(userUuid: string): Promise<UserEntity> {
const user = await this.userRepository.findOne({
where: { uuid: userUuid, isActive: true, isUserVerified: true },
});
if (!user) {
throw new HttpException('User not found', HttpStatus.NOT_FOUND);
}
return user;
}
private async handleNonInvitedUser(
activationCode: string,
userUuid: string,
): Promise<void> {
await this.userSpaceService.verifyCodeAndAddUserSpace(
{ inviteCode: activationCode },
userUuid,
);
}
private async handleInvitedUser(
user: UserEntity,
invitedUser: InviteUserEntity,
): Promise<void> {
for (const invitedSpace of invitedUser.spaces) {
try {
const deviceUUIDs = await this.userSpaceService.getDeviceUUIDsForSpace(
invitedSpace.space.uuid,
);
await this.userSpaceService.addUserPermissionsToDevices(
user.uuid,
deviceUUIDs,
);
await this.spaceUserService.associateUserToSpace({
communityUuid: invitedSpace.space.community.uuid,
spaceUuid: invitedSpace.space.uuid,
userUuid: user.uuid,
projectUuid: invitedUser.project.uuid,
});
} catch (spaceError) {
console.error(
`Error processing space ${invitedSpace.space.uuid}:`,
spaceError,
);
continue; // Skip to the next space
}
}
// Update invited user and associated user data
await this.inviteUserRepository.update(
{ uuid: invitedUser.uuid },
{ status: UserStatusEnum.ACTIVE, user: { uuid: user.uuid } },
);
await this.userRepository.update(
{ uuid: user.uuid },
{
project: { uuid: invitedUser.project.uuid },
roleType: { uuid: invitedUser.roleType.uuid },
},
);
}
private async getRoleTypeByUuid(roleUuid: string) {
const role = await this.roleTypeRepository.findOne({
where: { uuid: roleUuid },
});
return role.type;
}
private async updateWhenUserIsInvite(
queryRunner: QueryRunner,
dto: UpdateUserInvitationDto,
invitedUserUuid: string,
): Promise<void> {
const {
firstName,
lastName,
jobTitle,
companyName,
phoneNumber,
roleUuid,
spaceUuids,
} = dto;
// Update user invitation details
await queryRunner.manager.update(
this.inviteUserRepository.target,
{ uuid: invitedUserUuid },
{
firstName,
lastName,
jobTitle,
companyName,
phoneNumber,
roleType: { uuid: roleUuid },
},
);
// Remove old space associations
await queryRunner.manager.delete(this.inviteUserSpaceRepository.target, {
inviteUser: { uuid: invitedUserUuid },
});
// Save new space associations
const spaceData = spaceUuids.map((spaceUuid) => ({
inviteUser: { uuid: invitedUserUuid },
space: { uuid: spaceUuid },
}));
await queryRunner.manager.save(
this.inviteUserSpaceRepository.target,
spaceData,
);
}
private async updateWhenUserIsActive(
queryRunner: QueryRunner,
dto: UpdateUserInvitationDto,
invitedUserUuid: string,
): Promise<void> {
const {
firstName,
lastName,
jobTitle,
companyName,
phoneNumber,
roleUuid,
spaceUuids,
projectUuid,
} = dto;
const userData = await this.inviteUserRepository.findOne({
where: { uuid: invitedUserUuid },
relations: ['user.userSpaces.space', 'user.userSpaces.space.community'],
});
if (!userData) {
throw new HttpException(
`User with invitedUserUuid ${invitedUserUuid} not found`,
HttpStatus.NOT_FOUND,
);
}
// Update user details
await queryRunner.manager.update(
this.inviteUserRepository.target,
{ uuid: invitedUserUuid },
{
firstName,
lastName,
jobTitle,
companyName,
phoneNumber,
roleType: { uuid: roleUuid },
},
);
await this.userRepository.update(
{ uuid: userData.user.uuid },
{
roleType: { uuid: roleUuid },
},
);
// Disassociate the user from all current spaces
const disassociatePromises = userData.user.userSpaces.map((userSpace) =>
this.spaceUserService
.disassociateUserFromSpace({
communityUuid: userSpace.space.community.uuid,
spaceUuid: userSpace.space.uuid,
userUuid: userData.user.uuid,
projectUuid,
})
.catch((error) => {
console.error(
`Failed to disassociate user from space ${userSpace.space.uuid}:`,
error,
);
throw error;
}),
);
await Promise.allSettled(disassociatePromises);
// Process new spaces
const associatePromises = spaceUuids.map(async (spaceUuid) => {
try {
// Fetch space details
const spaceDetails = await this.getSpaceByUuid(spaceUuid);
// Fetch device UUIDs for the space
const deviceUUIDs =
await this.userSpaceService.getDeviceUUIDsForSpace(spaceUuid);
// Grant permissions to the user for all devices in the space
await this.userSpaceService.addUserPermissionsToDevices(
userData.user.uuid,
deviceUUIDs,
);
// Associate the user with the new space
await this.spaceUserService.associateUserToSpace({
communityUuid: spaceDetails.communityUuid,
spaceUuid: spaceUuid,
userUuid: userData.user.uuid,
projectUuid,
});
} catch (error) {
console.error(`Failed to process space ${spaceUuid}:`, error);
throw error;
}
});
await Promise.all(associatePromises);
}
private async updateUserStatus(
invitedUserUuid: string,
projectUuid: string,
isEnabled: boolean,
) {
await this.inviteUserRepository.update(
{ uuid: invitedUserUuid, project: { uuid: projectUuid } },
{ isEnabled },
);
}
private async disassociateUserFromSpaces(user: any, projectUuid: string) {
const disassociatePromises = user.userSpaces.map((userSpace) =>
this.spaceUserService.disassociateUserFromSpace({
communityUuid: userSpace.space.community.uuid,
spaceUuid: userSpace.space.uuid,
userUuid: user.uuid,
projectUuid,
}),
);
const results = await Promise.allSettled(disassociatePromises);
results.forEach((result, index) => {
if (result.status === 'rejected') {
console.error(
`Failed to disassociate user from space ${user.userSpaces[index].space.uuid}:`,
result.reason,
);
}
});
}
private async associateUserToSpaces(
user: any,
userData: any,
projectUuid: string,
invitedUserUuid: string,
disable: boolean,
) {
const spaceUuids = userData.spaces.map((space) => space.space.uuid);
const associatePromises = spaceUuids.map(async (spaceUuid) => {
try {
const spaceDetails = await this.getSpaceByUuid(spaceUuid);
const deviceUUIDs =
await this.userSpaceService.getDeviceUUIDsForSpace(spaceUuid);
await this.userSpaceService.addUserPermissionsToDevices(
user.uuid,
deviceUUIDs,
);
await this.spaceUserService.associateUserToSpace({
communityUuid: spaceDetails.communityUuid,
spaceUuid,
userUuid: user.uuid,
projectUuid,
});
await this.updateUserStatus(invitedUserUuid, projectUuid, disable);
} catch (error) {
console.error(`Failed to associate user to space ${spaceUuid}:`, error);
}
});
await Promise.allSettled(associatePromises);
}
}

View File

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

View File

@ -1,24 +1,25 @@
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 {
ApiTags,
ApiBearerAuth,
ApiOperation,
ApiParam,
ApiQuery,
ApiTags,
} from '@nestjs/swagger';
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
import { ControllerRoute } from '@app/common/constants/controller-route';
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
import { PowerClampService } from '../services/power-clamp.service';
import {
GetPowerClampBySpaceDto,
GetPowerClampDto,
} from '../dto/get-power-clamp.dto';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import {
PowerClampParamsDto,
ResourceParamsDto,
} from '../dto/power-clamp-params.dto';
import { PowerClampService } from '../services/power-clamp.service';
@ApiTags('Power Clamp Module')
@Controller({
version: EnableDisableStatusEnum.ENABLED,
@ -26,6 +27,7 @@ import { PowerClampService } from '../services/power-clamp.service';
})
export class PowerClampController {
constructor(private readonly powerClampService: PowerClampService) {}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get(':powerClampUuid/historical')

View File

@ -22,6 +22,7 @@ import {
} from '@app/common/modules/scene/repositories';
import {
InviteSpaceRepository,
SpaceLinkRepository,
SpaceProductAllocationRepository,
SpaceRepository,
} from '@app/common/modules/space';
@ -95,6 +96,7 @@ import { AqiSpaceDailyPollutantStatsRepository } from '@app/common/modules/aqi/r
SpaceModelService,
SpaceProductAllocationService,
SqlLoaderService,
SpaceLinkRepository,
SubspaceRepository,
SubspaceDeviceService,
SubspaceProductAllocationService,

View File

@ -23,10 +23,10 @@ import { SpaceDeviceService } from 'src/space/services';
import { SqlLoaderService } from '@app/common/helper/services/sql-loader.service';
import { DataSource } from 'typeorm';
import { SQL_PROCEDURES_PATH } from '@app/common/constants/sql-query-path';
import { filterByMonth, toMMYYYY } from '@app/common/helper/date-format';
import { ProductType } from '@app/common/constants/product-type.enum';
import { CommunityService } from 'src/community/services';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { filterByMonth, toMMYYYY } from '@app/common/helper/date-format';
@Injectable()
export class PowerClampService {

View File

@ -23,6 +23,7 @@ import {
} from '@app/common/modules/scene/repositories';
import {
InviteSpaceRepository,
SpaceLinkRepository,
SpaceProductAllocationRepository,
SpaceRepository,
} from '@app/common/modules/space';
@ -93,6 +94,7 @@ const CommandHandlers = [CreateOrphanSpaceHandler];
SpaceModelService,
DeviceService,
SpaceProductAllocationService,
SpaceLinkRepository,
SubspaceRepository,
SubspaceDeviceService,
SubspaceProductAllocationService,

View File

@ -33,7 +33,6 @@ export class ProjectUserService {
'status',
'phoneNumber',
'jobTitle',
'companyName',
'invitedBy',
'isEnabled',
],
@ -92,7 +91,6 @@ export class ProjectUserService {
'status',
'phoneNumber',
'jobTitle',
'companyName',
'invitedBy',
'isEnabled',
],

View File

@ -96,26 +96,6 @@ export class ScheduleService {
throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND);
}
if (
deviceDetails.productDevice.prodType == ProductType.CUR_2 &&
addScheduleDto.category != 'Timer'
) {
throw new HttpException(
'Invalid category for CUR_2 devices',
HttpStatus.BAD_REQUEST,
);
}
if (
deviceDetails.productDevice.prodType == ProductType.CUR_2 &&
addScheduleDto.category != 'Timer'
) {
throw new HttpException(
'Invalid category for CUR_2 devices',
HttpStatus.BAD_REQUEST,
);
}
this.ensureProductTypeSupportedForSchedule(
deviceDetails.productDevice.prodType as ProductType,
);
@ -123,7 +103,6 @@ export class ScheduleService {
await this.addScheduleDeviceInTuya(
deviceDetails.deviceTuyaUuid,
addScheduleDto,
deviceDetails.productDevice.prodType as ProductType,
);
} catch (error) {
throw new HttpException(
@ -146,14 +125,10 @@ export class ScheduleService {
const schedules = await this.getScheduleDeviceInTuya(
deviceDetails.deviceTuyaUuid,
category,
deviceDetails.productDevice.prodType as ProductType,
);
const result = schedules.result.map((schedule: any) => {
return {
category:
deviceDetails.productDevice.prodType == ProductType.CUR_2
? schedule.category
: schedule.category.replace('category_', ''),
category: schedule.category.replace('category_', ''),
enable: schedule.enable,
function: {
code: schedule.functions[0].code,
@ -184,26 +159,6 @@ export class ScheduleService {
throw new HttpException('Device Not Found', HttpStatus.NOT_FOUND);
}
if (
deviceDetails.productDevice.prodType == ProductType.CUR_2 &&
updateScheduleDto.category != 'Timer'
) {
throw new HttpException(
'Invalid category for CUR_2 devices',
HttpStatus.BAD_REQUEST,
);
}
if (
deviceDetails.productDevice.prodType == ProductType.CUR_2 &&
updateScheduleDto.category != 'Timer'
) {
throw new HttpException(
'Invalid category for CUR_2 devices',
HttpStatus.BAD_REQUEST,
);
}
// Corrected condition for supported device types
this.ensureProductTypeSupportedForSchedule(
deviceDetails.productDevice.prodType as ProductType,
@ -212,7 +167,6 @@ export class ScheduleService {
await this.updateScheduleDeviceInTuya(
deviceDetails.deviceTuyaUuid,
updateScheduleDto,
deviceDetails.productDevice.prodType as ProductType,
);
} catch (error) {
throw new HttpException(
@ -238,7 +192,6 @@ export class ScheduleService {
private async addScheduleDeviceInTuya(
deviceId: string,
addScheduleDto: AddScheduleDto,
deviceType: ProductType,
): Promise<addScheduleDeviceInterface> {
try {
const convertedTime = convertTimestampToDubaiTime(addScheduleDto.time);
@ -257,10 +210,7 @@ export class ScheduleService {
...addScheduleDto.function,
},
],
category:
deviceType == ProductType.CUR_2
? addScheduleDto.category
: `category_${addScheduleDto.category}`,
category: `category_${addScheduleDto.category}`,
},
});
@ -276,12 +226,9 @@ export class ScheduleService {
private async getScheduleDeviceInTuya(
deviceId: string,
category: string,
deviceType: ProductType,
): Promise<getDeviceScheduleInterface> {
try {
const categoryToSent =
deviceType == ProductType.CUR_2 ? category : `category_${category}`;
const path = `/v2.0/cloud/timer/device/${deviceId}?category=${categoryToSent}`;
const path = `/v2.0/cloud/timer/device/${deviceId}?category=category_${category}`;
const response = await this.tuya.request({
method: 'GET',
path,
@ -301,7 +248,6 @@ export class ScheduleService {
private async updateScheduleDeviceInTuya(
deviceId: string,
updateScheduleDto: UpdateScheduleDto,
deviceType: ProductType,
): Promise<addScheduleDeviceInterface> {
try {
const convertedTime = convertTimestampToDubaiTime(updateScheduleDto.time);
@ -322,10 +268,7 @@ export class ScheduleService {
value: updateScheduleDto.function.value,
},
],
category:
deviceType == ProductType.CUR_2
? updateScheduleDto.category
: `category_${updateScheduleDto.category}`,
category: `category_${updateScheduleDto.category}`,
},
});

View File

@ -22,6 +22,7 @@ import {
} from '@app/common/modules/scene/repositories';
import {
InviteSpaceRepository,
SpaceLinkRepository,
SpaceProductAllocationRepository,
SpaceRepository,
} from '@app/common/modules/space';
@ -92,6 +93,7 @@ const CommandHandlers = [
DeviceRepository,
TuyaService,
CommunityRepository,
SpaceLinkRepository,
InviteSpaceRepository,
NewTagService,
DeviceService,

View File

@ -1,26 +1,23 @@
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { SpaceService } from '../services';
import { ControllerRoute } from '@app/common/constants/controller-route';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import {
Body,
Controller,
Delete,
Get,
Param,
ParseUUIDPipe,
Post,
Put,
Query,
UseGuards,
} from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { Permissions } from 'src/decorators/permissions.decorator';
import { PermissionsGuard } from 'src/guards/permissions.guard';
import { AddSpaceDto, CommunitySpaceParam, UpdateSpaceDto } from '../dtos';
import { GetSpaceDto } from '../dtos/get.space.dto';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { GetSpaceParam } from '../dtos/get.space.param';
import { OrderSpacesDto } from '../dtos/order.spaces.dto';
import { SpaceService } from '../services';
import { PermissionsGuard } from 'src/guards/permissions.guard';
import { Permissions } from 'src/decorators/permissions.decorator';
import { GetSpaceDto } from '../dtos/get.space.dto';
@ApiTags('Space Module')
@Controller({
@ -68,30 +65,6 @@ export class SpaceController {
);
}
@ApiBearerAuth()
@UseGuards(PermissionsGuard)
@Permissions('SPACE_UPDATE')
@ApiOperation({
summary:
ControllerRoute.SPACE.ACTIONS
.UPDATE_CHILDREN_SPACES_ORDER_OF_A_SPACE_SUMMARY,
description:
ControllerRoute.SPACE.ACTIONS
.UPDATE_CHILDREN_SPACES_ORDER_OF_A_SPACE_DESCRIPTION,
})
@Post(':parentSpaceUuid/spaces/order')
async updateSpacesOrder(
@Body() orderSpacesDto: OrderSpacesDto,
@Param() communitySpaceParam: CommunitySpaceParam,
@Param('parentSpaceUuid', ParseUUIDPipe) parentSpaceUuid: string,
) {
await this.spaceService.updateSpacesOrder(parentSpaceUuid, orderSpacesDto);
return new SuccessResponseDto({
statusCode: 200,
message: 'Spaces order updated successfully',
});
}
@ApiBearerAuth()
@UseGuards(PermissionsGuard)
@Permissions('SPACE_DELETE')

View File

@ -1,13 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { ArrayUnique, IsNotEmpty, IsUUID } from 'class-validator';
export class OrderSpacesDto {
@ApiProperty({
description: 'List of children spaces associated with the space',
type: [String],
})
@IsNotEmpty()
@ArrayUnique()
@IsUUID('4', { each: true, message: 'Invalid space UUID provided' })
spacesUuids: string[];
}

View File

@ -2,7 +2,6 @@ import { ORPHAN_SPACE_NAME } from '@app/common/constants/orphan-constant';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
ArrayUnique,
IsArray,
IsNumber,
IsOptional,
@ -50,21 +49,6 @@ export class UpdateSpaceDto {
description: 'List of subspace modifications',
type: [UpdateSubspaceDto],
})
@ArrayUnique((subspace) => subspace.subspaceName ?? subspace.uuid, {
message(validationArguments) {
const subspaces = validationArguments.value;
const nameCounts = subspaces.reduce((acc, curr) => {
acc[curr.subspaceName ?? curr.uuid] =
(acc[curr.subspaceName ?? curr.uuid] || 0) + 1;
return acc;
}, {});
// Find duplicates
const duplicates = Object.keys(nameCounts).filter(
(name) => nameCounts[name] > 1,
);
return `Duplicate subspace names found: ${duplicates.join(', ')}`;
},
})
@IsOptional()
@IsArray()
@ValidateNested({ each: true })

View File

@ -2,5 +2,6 @@ export * from './space.service';
export * from './space-user.service';
export * from './space-device.service';
export * from './subspace';
export * from './space-link';
export * from './space-scene.service';
export * from './space-validation.service';

View File

@ -0,0 +1 @@
export * from './space-link.service';

View File

@ -0,0 +1,6 @@
import { Injectable } from '@nestjs/common';
// todo: find out why we need to import this
// in community module in order for the whole system to work
@Injectable()
export class SpaceLinkService {}

View File

@ -33,7 +33,6 @@ import {
} from '../dtos';
import { CreateProductAllocationDto } from '../dtos/create-product-allocation.dto';
import { GetSpaceDto } from '../dtos/get.space.dto';
import { OrderSpacesDto } from '../dtos/order.spaces.dto';
import { SpaceWithParentsDto } from '../dtos/space.parents.dto';
import { SpaceProductAllocationService } from './space-product-allocation.service';
import { ValidationService } from './space-validation.service';
@ -356,54 +355,6 @@ export class SpaceService {
}
}
async updateSpacesOrder(
parentSpaceUuid: string,
{ spacesUuids }: OrderSpacesDto,
) {
const parentSpace = await this.spaceRepository.findOne({
where: { uuid: parentSpaceUuid, disabled: false },
relations: ['children'],
});
if (!parentSpace) {
throw new HttpException(
`Parent space with ID ${parentSpaceUuid} not found`,
HttpStatus.NOT_FOUND,
);
}
// ensure that all sent spaces belong to the parent space
const missingSpaces = spacesUuids.filter(
(uuid) => !parentSpace.children.some((child) => child.uuid === uuid),
);
if (missingSpaces.length > 0) {
throw new HttpException(
`Some spaces with IDs ${missingSpaces.join(
', ',
)} do not belong to the parent space with ID ${parentSpaceUuid}`,
HttpStatus.BAD_REQUEST,
);
}
try {
await this.spaceRepository.update(
{ uuid: In(spacesUuids), parent: { uuid: parentSpaceUuid } },
{
order: () =>
'CASE ' +
spacesUuids
.map((s, index) => `WHEN uuid = '${s}' THEN ${index + 1}`)
.join(' ') +
' END',
},
);
return;
} catch (error) {
console.error('Error updating spaces order:', error);
throw new HttpException(
'An error occurred while updating spaces order',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
async delete(params: GetSpaceParam): Promise<BaseResponseDto> {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
@ -475,7 +426,7 @@ export class SpaceService {
}
}
private async disableSpace(space: SpaceEntity, orphanSpace: SpaceEntity) {
async disableSpace(space: SpaceEntity, orphanSpace: SpaceEntity) {
await this.commandBus.execute(
new DisableSpaceCommand({ spaceUuid: space.uuid, orphanSpace }),
);
@ -758,19 +709,8 @@ export class SpaceService {
rootSpaces.push(map.get(space.uuid)!); // Push only root spaces
}
});
rootSpaces.forEach(this.sortSpaceChildren.bind(this));
return rootSpaces;
}
private sortSpaceChildren(space: SpaceEntity) {
if (space.children && space.children.length > 0) {
space.children.sort((a, b) => {
const aOrder = a.order ?? Infinity;
const bOrder = b.order ?? Infinity;
return aOrder - bOrder;
});
space.children.forEach(this.sortSpaceChildren.bind(this)); // Recursively sort children of children
}
return rootSpaces;
}
private validateSpaceCreationCriteria({

View File

@ -23,7 +23,7 @@ export class SubspaceProductAllocationService {
// spaceAllocationsToExclude?: SpaceProductAllocationEntity[],
): Promise<void> {
try {
if (!allocationsData?.length) return;
if (!allocationsData.length) return;
const allocations: SubspaceProductAllocationEntity[] = [];
@ -112,7 +112,7 @@ export class SubspaceProductAllocationService {
);
// Create the product-tag mapping based on the processed tags
const productTagMapping = subspace.productAllocations?.map(
const productTagMapping = subspace.productAllocations.map(
({ tagUuid, tagName, productUuid }) => {
const inputTag = tagUuid
? createdTagsByUUID.get(tagUuid)

View File

@ -39,7 +39,7 @@ export class SubSpaceService {
private readonly subspaceProductAllocationService: SubspaceProductAllocationService,
) {}
private async createSubspaces(
async createSubspaces(
subspaceData: Array<{
subspaceName: string;
space: SpaceEntity;
@ -342,37 +342,26 @@ export class SubSpaceService {
})),
);
const existingSubspaces = await this.subspaceRepository.find({
where: {
uuid: In(
subspaceDtos.filter((dto) => dto.uuid).map((dto) => dto.uuid),
),
},
});
if (
existingSubspaces.length !==
subspaceDtos.filter((dto) => dto.uuid).length
) {
throw new HttpException(
`Some subspaces with provided UUIDs do not exist in the space.`,
HttpStatus.NOT_FOUND,
);
}
const updatedSubspaces: SubspaceEntity[] = await queryRunner.manager.save(
SubspaceEntity,
newSubspaces,
[
...newSubspaces,
...subspaceDtos
.filter((dto) => dto.uuid)
.map((dto) => ({
subspaceName: dto.subspaceName,
space,
})),
],
);
const allSubspaces = [...updatedSubspaces, ...existingSubspaces];
// create or update allocations for the subspaces
if (allSubspaces.length > 0) {
if (updatedSubspaces.length > 0) {
await this.subspaceProductAllocationService.updateSubspaceProductAllocationsV2(
subspaceDtos.map((dto) => ({
...dto,
uuid:
dto.uuid ||
allSubspaces.find((s) => s.subspaceName === dto.subspaceName)
updatedSubspaces.find((s) => s.subspaceName === dto.subspaceName)
?.uuid,
})),
projectUuid,

View File

@ -0,0 +1 @@
export * from './tag.service';

View File

@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
// todo: find out why we need to import this
// in community module in order for the whole system to work
@Injectable()
export class TagService {
constructor() {}
}

View File

@ -37,6 +37,7 @@ import {
} from '@app/common/modules/space-model';
import {
InviteSpaceRepository,
SpaceLinkRepository,
SpaceProductAllocationRepository,
SpaceRepository,
} from '@app/common/modules/space/repositories';
@ -115,6 +116,7 @@ export const CommandHandlers = [DisableSpaceHandler];
SubspaceRepository,
DeviceRepository,
CommunityRepository,
SpaceLinkRepository,
UserSpaceRepository,
UserRepository,
SpaceUserService,

View File

@ -1,7 +1,3 @@
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 {
Body,
Controller,
@ -11,12 +7,10 @@ import {
Param,
Patch,
Put,
Req,
UseGuards,
} from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { CheckProfilePictureGuard } from 'src/guards/profile.picture.guard';
import { SuperAdminRoleGuard } from 'src/guards/super.admin.role.guard';
import { UserService } from '../services/user.service';
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard';
import {
UpdateNameDto,
@ -24,7 +18,11 @@ import {
UpdateRegionDataDto,
UpdateTimezoneDataDto,
} from '../dtos';
import { UserService } from '../services/user.service';
import { CheckProfilePictureGuard } from 'src/guards/profile.picture.guard';
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')
@Controller({
@ -156,32 +154,6 @@ 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()
@UseGuards(JwtAuthGuard)
@Patch('agreements/web/:userUuid')

View File

@ -1,28 +1,28 @@
import { CommonErrorCodes } from '@app/common/constants/error-codes.enum';
import { PermissionType } from '@app/common/constants/permission-type.enum';
import { RoleType } from '@app/common/constants/role.type.enum';
import { UserStatusEnum } from '@app/common/constants/user-status.enum';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import {
InviteUserRepository,
InviteUserSpaceRepository,
} from '@app/common/modules/Invite-user/repositiories';
import { InviteSpaceEntity } from '@app/common/modules/space/entities/invite-space.entity';
import {
InviteSpaceRepository,
SpaceRepository,
} from '@app/common/modules/space/repositories';
import { UserSpaceRepository } from '@app/common/modules/user/repositories';
import {
BadRequestException,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';
import { UserDevicePermissionService } from 'src/user-device-permission/services';
import { UserSpaceRepository } from '@app/common/modules/user/repositories';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
import { AddUserSpaceDto, AddUserSpaceUsingCodeDto } from '../dtos';
import {
InviteSpaceRepository,
SpaceRepository,
} from '@app/common/modules/space/repositories';
import { CommonErrorCodes } from '@app/common/constants/error-codes.enum';
import { UserDevicePermissionService } from 'src/user-device-permission/services';
import { PermissionType } from '@app/common/constants/permission-type.enum';
import { InviteSpaceEntity } from '@app/common/modules/space/entities/invite-space.entity';
import { UserService } from './user.service';
import { RoleType } from '@app/common/constants/role.type.enum';
import {
InviteUserRepository,
InviteUserSpaceRepository,
} from '@app/common/modules/Invite-user/repositiories';
import { UserStatusEnum } from '@app/common/constants/user-status.enum';
@Injectable()
export class UserSpaceService {
@ -154,7 +154,6 @@ export class UserSpaceService {
lastName: user.lastName,
email: user.email,
jobTitle: null,
companyName: null,
phoneNumber: null,
roleType: { uuid: user.role.uuid },
status: UserStatusEnum.ACTIVE,

View File

@ -1,21 +1,21 @@
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { removeBase64Prefix } from '@app/common/helper/removeBase64Prefix';
import { RegionRepository } from '@app/common/modules/region/repositories';
import { TimeZoneRepository } from '@app/common/modules/timezone/repositories';
import { UserEntity } from '@app/common/modules/user/entities';
import { UserRepository } from '@app/common/modules/user/repositories';
import {
BadRequestException,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';
import {
UpdateNameDto,
UpdateProfilePictureDataDto,
UpdateRegionDataDto,
UpdateTimezoneDataDto,
} from './../dtos/update.user.dto';
import {
BadRequestException,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';
import { UserRepository } from '@app/common/modules/user/repositories';
import { RegionRepository } from '@app/common/modules/region/repositories';
import { TimeZoneRepository } from '@app/common/modules/timezone/repositories';
import { removeBase64Prefix } from '@app/common/helper/removeBase64Prefix';
import { UserEntity } from '@app/common/modules/user/entities';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
@Injectable()
export class UserService {
@ -269,12 +269,4 @@ export class UserService {
}
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 });
}
}

View File

@ -28,7 +28,7 @@ import { PasswordType } from '@app/common/constants/password-type.enum';
import { VisitorPasswordEnum } from '@app/common/constants/visitor-password.enum';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
import { ProjectRepository } from '@app/common/modules/project/repositiories';
import { EmailService } from '@app/common/util/email/email.service';
import { EmailService } from '@app/common/util/email.service';
import { DeviceService } from 'src/device/services';
import { DoorLockService } from 'src/door-lock/services';
import { PasswordEncryptionService } from 'src/door-lock/services/encryption.services';

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