diff --git a/.github/workflows/deploy-golang-develop.yml b/.github/workflows/deploy-golang-develop.yml
index d9d628b7..94cc9459 100644
--- a/.github/workflows/deploy-golang-develop.yml
+++ b/.github/workflows/deploy-golang-develop.yml
@@ -20,64 +20,7 @@ defaults:
working-directory: occupi-backend
jobs:
- lint:
- name: Lint
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
-
- - name: Set up Go
- uses: actions/setup-go@v5
- with:
- go-version: '1.21' # Specify the Go version you are using
-
- - name: Install golangci-lint
- run: |
- go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
-
- - name: Run golangci-lint
- run: |
- golangci-lint run
-
- test:
- needs: lint
- name: Test
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
-
- - name: Set up Go
- uses: actions/setup-go@v5
- with:
- go-version: '1.21' # Specify the Go version you are using
-
- - name: Decrypt env variables
- run: |
- echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 .env.gpg > .env
-
- - name: Decrypt key file
- run: |
- echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 key.pem.gpg > key.pem
-
- - name: Decrypt cert file
- run: |
- echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 cert.pem.gpg > cert.pem
-
- - name: Run tests
- run: |
- go test -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware ./tests/... -coverprofile=coverage.out
-
- - name: Upload coverage reports to Codecov
- uses: codecov/codecov-action@v4.0.1
- with:
- token: ${{ secrets.CODECOV_TOKEN }}
-
build-test:
- needs: test
name: Build
runs-on: ubuntu-latest
@@ -115,10 +58,14 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- - name: Decrypt env variables
+ - name: Decrypt default variables
+ run: |
+ echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 configs/config.yaml.gpg > configs/config.yaml
+
+ - name: Decrypt test variables
run: |
- echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 .dev.env.gpg > .dev.env
-
+ echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 configs/dev.deployed.yaml.gpg > configs/dev.deployed.yaml
+
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
diff --git a/.github/workflows/deploy-golang-prod.yml b/.github/workflows/deploy-golang-prod.yml
index 9cfa7e78..f3a5c685 100644
--- a/.github/workflows/deploy-golang-prod.yml
+++ b/.github/workflows/deploy-golang-prod.yml
@@ -12,64 +12,7 @@ defaults:
working-directory: occupi-backend
jobs:
- lint:
- name: Lint
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
-
- - name: Set up Go
- uses: actions/setup-go@v5
- with:
- go-version: '1.21' # Specify the Go version you are using
-
- - name: Install golangci-lint
- run: |
- go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
-
- - name: Run golangci-lint
- run: |
- golangci-lint run
-
- test:
- needs: lint
- name: Test
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
-
- - name: Set up Go
- uses: actions/setup-go@v5
- with:
- go-version: '1.21' # Specify the Go version you are using
-
- - name: Decrypt env variables
- run: |
- echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 .env.gpg > .env
-
- - name: Decrypt key file
- run: |
- echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 key.pem.gpg > key.pem
-
- - name: Decrypt cert file
- run: |
- echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 cert.pem.gpg > cert.pem
-
- - name: Run tests
- run: |
- go test -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware ./tests/... -coverprofile=coverage.out
-
- - name: Upload coverage reports to Codecov
- uses: codecov/codecov-action@v4.0.1
- with:
- token: ${{ secrets.CODECOV_TOKEN }}
-
build:
- needs: test
name: Build
runs-on: ubuntu-latest
@@ -107,10 +50,14 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- - name: Decrypt env variables
+ - name: Decrypt default variables
+ run: |
+ echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 configs/config.yaml.gpg > configs/config.yaml
+
+ - name: Decrypt test variables
run: |
- echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 .prod.env.gpg > .prod.env
-
+ echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 configs/prod.yaml.gpg > configs/prod.yaml
+
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
diff --git a/.github/workflows/lint-test-build-golang.yml b/.github/workflows/lint-test-build-golang.yml
index ec411998..bb04c963 100644
--- a/.github/workflows/lint-test-build-golang.yml
+++ b/.github/workflows/lint-test-build-golang.yml
@@ -44,27 +44,69 @@ jobs:
name: Test
runs-on: ubuntu-latest
+ services:
+ mongo:
+ image: mongo:latest
+ ports:
+ - 27017:27017
+ options: >-
+ --health-cmd "mongosh --eval 'db.adminCommand({ping: 1})'"
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+
steps:
- name: Checkout code
uses: actions/checkout@v4
+ - name: Install mongosh
+ run: |
+ wget -qO - https://www.mongodb.org/static/pgp/server-5.0.asc | sudo apt-key add -
+ echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/5.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-5.0.list
+ sudo apt-get update
+ sudo apt-get install -y mongodb-mongosh
+
+
+ - name: Wait for MongoDB to be ready
+ run: |
+ for i in {1..30}; do
+ if mongosh --eval 'db.adminCommand({ping: 1})' ${{ secrets.MONGO_DB_TEST_URL }}; then
+ echo "MongoDB is up"
+ break
+ fi
+ echo "Waiting for MongoDB to be ready..."
+ sleep 2
+ done
+
+ - name: Create MongoDB User
+ env:
+ MONGO_INITDB_ROOT_USERNAME: ${{ secrets.MONGO_DB_TEST_USERNAME }}
+ MONGO_INITDB_ROOT_PASSWORD: ${{ secrets.MONGO_DB_TEST_PASSWORD }}
+ MONGO_INITDB_DATABASE: ${{ secrets.MONGO_DB_TEST_DB }}
+ run: |
+ mongosh ${{ secrets.MONGO_DB_TEST_URL }}/admin --eval "
+ db.createUser({
+ user: '${MONGO_INITDB_ROOT_USERNAME}',
+ pwd: '${MONGO_INITDB_ROOT_PASSWORD}',
+ roles: [
+ { role: 'readWrite', db: '${MONGO_INITDB_DATABASE}' }
+ ]
+ });
+ "
+
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.21' # Specify the Go version you are using
- - name: Decrypt env variables
- run: |
- echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 .env.gpg > .env
-
- - name: Decrypt key file
+ - name: Decrypt default variables
run: |
- echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 key.pem.gpg > key.pem
+ echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 configs/config.yaml.gpg > configs/config.yaml
- - name: Decrypt cert file
+ - name: Decrypt test variables
run: |
- echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 cert.pem.gpg > cert.pem
-
+ echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --quiet --batch --yes --decrypt --passphrase-fd 0 configs/test.yaml.gpg > configs/test.yaml
+
- name: Run tests
run: |
go test -v -coverpkg=github.com/COS301-SE-2024/occupi/occupi-backend/pkg/utils,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/authenticator,github.com/COS301-SE-2024/occupi/occupi-backend/pkg/middleware ./tests/... -coverprofile=coverage.out
diff --git a/.gitignore b/.gitignore
index b512c09d..8f7e2474 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1 @@
-node_modules
\ No newline at end of file
+/benchmarks
\ No newline at end of file
diff --git a/README.md b/README.md
index d7271613..b534f272 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,9 @@
![Codecov](https://img.shields.io/codecov/c/github/COS301-SE-2024/occupi?style=flat-square)
![GitHub License](https://img.shields.io/github/license/COS301-SE-2024/occupi?style=flat-square)
![GitHub repo size](https://img.shields.io/github/repo-size/COS301-SE-2024/occupi?style=flat-square)
-![Docs Website](https://img.shields.io/website?url=https%3A%2F%2Fdocs.occupi.tech&style=flat-square)
+![Uptime Robot ratio (7 days)](https://img.shields.io/uptimerobot/ratio/7/m797164574-0d7a208e3fca09e737a9619f?style=flat-square)
+![Uptime Robot ratio (7 days)](https://img.shields.io/uptimerobot/ratio/7/m797164576-1e9cec32af1ac1e298136819?style=flat-square)
+![Uptime Robot ratio (7 days)](https://img.shields.io/uptimerobot/ratio/7/m797164567-64429e97f6d650162e9354dd?style=flat-square)
![GitHub Repo stars](https://img.shields.io/github/stars/COS301-SE-2024/occupi?style=flat-square)
[![codecov](https://codecov.io/gh/COS301-SE-2024/occupi/graph/badge.svg?token=71QPCD9NNP)](https://codecov.io/gh/COS301-SE-2024/occupi)
![GitHub Issues or Pull Requests](https://img.shields.io/github/issues/COS301-SE-2024/occupi?style=flat-square)
diff --git a/documentation/occupi-docs/pages/index.mdx b/documentation/occupi-docs/pages/index.mdx
index 152aeef5..a758e3ca 100644
--- a/documentation/occupi-docs/pages/index.mdx
+++ b/documentation/occupi-docs/pages/index.mdx
@@ -21,21 +21,131 @@ To get started, please feel free to explore the top navbar or the sidebar and na
![GitHub repo size](https://img.shields.io/github/repo-size/COS301-SE-2024/occupi?style=flat-square)
- ![Docs website](https://img.shields.io/website?url=https%3A%2F%2Fcos301-se-2024.github.io%2Foccupi%2F&style=flat-square)
+ ![Uptime Robot ratio (7 days)](https://img.shields.io/uptimerobot/ratio/7/m797164574-0d7a208e3fca09e737a9619f?style=flat-square)
+
+ ![Uptime Robot ratio (7 days)](https://img.shields.io/uptimerobot/ratio/7/m797164576-1e9cec32af1ac1e298136819?style=flat-square)
+
+ ![Uptime Robot ratio (7 days)](https://img.shields.io/uptimerobot/ratio/7/m797164567-64429e97f6d650162e9354dd?style=flat-square)
![GitHub Repo stars](https://img.shields.io/github/stars/COS301-SE-2024/occupi?style=flat-square)
- ![Deploy Landing page](https://github.com/COS301-SE-2024/occupi/actions/workflows/deploy-landing-page.yml/badge.svg)
+ [![codecov](https://codecov.io/gh/COS301-SE-2024/occupi/graph/badge.svg?token=71QPCD9NNP)](https://codecov.io/gh/COS301-SE-2024/occupi)
- ![Deploy Docs site to Pages](https://github.com/COS301-SE-2024/occupi/actions/workflows/deploy-docs.yml/badge.svg)
+ ![GitHub Issues or Pull Requests](https://img.shields.io/github/issues/COS301-SE-2024/occupi?style=flat-square)
-## Additional info
+
+
+
+ ![Deploy Docs](https://github.com/COS301-SE-2024/occupi/actions/workflows/deploy-docs.yml/badge.svg)
+
+ ![Deploy Landing page](https://github.com/COS301-SE-2024/occupi/actions/workflows/deploy-landing-page.yml/badge.svg)
+
+ ![Lint Test Occupi-mobile](https://github.com/COS301-SE-2024/occupi/actions/workflows/lint-test-mobile.yml/badge.svg)
-//stub
+ ![Lint Test and Build Golang](https://github.com/COS301-SE-2024/occupi/actions/workflows/lint-test-build-golang.yml/badge.svg)
+
+ ![Deploy api](https://github.com/COS301-SE-2024/occupi/actions/workflows/deploy-golang-develop.yml/badge.svg)
+
+ ![Lint Test and Build Occupi-Web](https://github.com/COS301-SE-2024/occupi/actions/workflows/lint-test-build-web.yml/badge.svg)
+
+
+
+
+
+# Meet the team behind occupi
+
+
+
+
+
+
+
+ Kamo
+ Project Manager, Fullstack Engineer
+ Kamo is a skilled software developer with a strong focus on database engineering and architecture. He is proficient in both front-end and back-end development, making him a well-rounded contributor to any team.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Carey
+ Frontend Engineer, UI Engineer, Business Analyst, Testing Engineer
+ Carey has a solid understanding of business principles, business analysis and systems analysis with a strong support for technology-centered strategies that promote interactive learning and easier access to resources for independent study.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Michael
+ Infrastructure management, Designer, Backend Engineer, DevOps, Frontend Engineer
+ Michael is a computer science enthusiast with a keen interest in creating engaging and aesthetically pleasing CS-related projects. His strengths lie in UI design, FullStack development, DevOps, and Systems Design, making him a versatile contributor to any team.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Tinashe
+ Frontend Engineer, UI engineer, Business Analyst, Testing Engineer
+ Tinashe is a BSc Computer Science Major and Multimedia Minor student with a keen interest in UI/UX design and Human Computer Interaction. She loves learning and tackling new challenges. Her resilience makes her an asset in any team.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Reta
+ Fullstack Engineer
+ Rethakgetse has a comprehensive skill set that spans both frontend and backend development, making him a well-rounded software developer. He is driven by a passion for problem-solving and a desire to make a tangible impact. He embraces the limitless possibilities offered by technology and is committed to leveraging his skills to develop transformative applications that address pressing industry needs, enhance operational efficiency, and improve customer satisfaction.
+
+
+
+
+
+
+
+
+
+
+
## Sponsors and Stakeholders
@@ -64,4 +174,4 @@ To get started, please feel free to explore the top navbar or the sidebar and na
-
\ No newline at end of file
+
diff --git a/documentation/occupi-docs/theme.config.jsx b/documentation/occupi-docs/theme.config.jsx
index 6d377946..2d5b736f 100644
--- a/documentation/occupi-docs/theme.config.jsx
+++ b/documentation/occupi-docs/theme.config.jsx
@@ -6,6 +6,7 @@ export default {
+ Occupi
>
),
logo: (
@@ -26,5 +27,15 @@ export default {
link: 'https://discord.com',
},
docsRepositoryBase: 'https://github.com/COS301-SE-2024/occupi/documentation/occupi-docs',
- footerText: `MIT ${new Date().getFullYear()} © Occupi.`,
+ footer: {
+ text: (
+
+ MIT {new Date().getFullYear()} ©{' '}
+
+ occupi
+
+ .
+
+ )
+ }
}
\ No newline at end of file
diff --git a/frontend/occupi-mobile4/.gitignore b/frontend/occupi-mobile4/.gitignore
index 6623142a..d3e535a6 100644
--- a/frontend/occupi-mobile4/.gitignore
+++ b/frontend/occupi-mobile4/.gitignore
@@ -10,6 +10,9 @@ npm-debug.*
*.orig.*
web-build/
+eas.json
+app.json
+
# macOS
.DS_Store
diff --git a/frontend/occupi-mobile4/app.json b/frontend/occupi-mobile4/app.json
index 891d2e0d..2b11cd34 100644
--- a/frontend/occupi-mobile4/app.json
+++ b/frontend/occupi-mobile4/app.json
@@ -19,11 +19,15 @@
}
},
"android": {
- "permissions": ["USE_BIOMETRIC", "USE_FINGERPRINT"],
+ "permissions": [
+ "USE_BIOMETRIC",
+ "USE_FINGERPRINT"
+ ],
"adaptiveIcon": {
"foregroundImage": "./assets/images/adaptive-icon.png",
"backgroundColor": "#ffffff"
- }
+ },
+ "package": "com.y2kodedev.occupi"
},
"web": {
"bundler": "metro",
@@ -35,6 +39,14 @@
],
"experiments": {
"typedRoutes": true
+ },
+ "extra": {
+ "router": {
+ "origin": false
+ },
+ "eas": {
+ "projectId": "0127a6f2-e29a-4a67-a134-be8c4daed566"
+ }
}
}
}
diff --git a/frontend/occupi-mobile4/eas.json b/frontend/occupi-mobile4/eas.json
new file mode 100644
index 00000000..bea8c5b7
--- /dev/null
+++ b/frontend/occupi-mobile4/eas.json
@@ -0,0 +1,18 @@
+{
+ "cli": {
+ "version": ">= 10.0.2"
+ },
+ "build": {
+ "development": {
+ "developmentClient": true,
+ "distribution": "internal"
+ },
+ "preview": {
+ "distribution": "internal"
+ },
+ "production": {}
+ },
+ "submit": {
+ "production": {}
+ }
+}
diff --git a/frontend/occupi-mobile4/package-lock.json b/frontend/occupi-mobile4/package-lock.json
index 04a45e25..a40cd5e0 100644
--- a/frontend/occupi-mobile4/package-lock.json
+++ b/frontend/occupi-mobile4/package-lock.json
@@ -55,6 +55,7 @@
"react-native-responsive-screen": "^1.4.2",
"react-native-safe-area-context": "^4.10.1",
"react-native-screens": "3.31.1",
+ "react-native-skeleton-content": "^1.0.13",
"react-native-svg": "^15.3.0",
"react-native-web": "~0.19.10",
"zod": "^3.23.8"
@@ -19650,6 +19651,24 @@
"react-native": "*"
}
},
+ "node_modules/react-native-skeleton-content": {
+ "version": "1.0.13",
+ "resolved": "https://registry.npmjs.org/react-native-skeleton-content/-/react-native-skeleton-content-1.0.13.tgz",
+ "integrity": "sha512-WgwDnBEdgFNh63BkscV3FRvDKHbIFYxu0MiJkuySHYKIiexYu75tnNBe8ONkM0hQEJ9pO6pg5+c8uuUmY/uoXA==",
+ "dependencies": {
+ "expo-linear-gradient": "^8.0.0",
+ "lodash": "^4.17.14"
+ }
+ },
+ "node_modules/react-native-skeleton-content/node_modules/expo-linear-gradient": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-8.4.0.tgz",
+ "integrity": "sha512-f9JOXaIl0MR8RBYRIud5UAsEi52oz81XhQs73VUpujemHjOyHmrZa6dqwf399YOwI/WBwbpcINcUIw/mCYE1mA==",
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/react-native-svg": {
"version": "15.3.0",
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.3.0.tgz",
diff --git a/frontend/occupi-mobile4/package.json b/frontend/occupi-mobile4/package.json
index 277e1696..9a6ca019 100644
--- a/frontend/occupi-mobile4/package.json
+++ b/frontend/occupi-mobile4/package.json
@@ -62,6 +62,7 @@
"react-native-responsive-screen": "^1.4.2",
"react-native-safe-area-context": "^4.10.1",
"react-native-screens": "3.31.1",
+ "react-native-skeleton-content": "^1.0.13",
"react-native-svg": "^15.3.0",
"react-native-web": "~0.19.10",
"zod": "^3.23.8"
diff --git a/frontend/occupi-mobile4/screens/Booking/BookRoom.tsx b/frontend/occupi-mobile4/screens/Booking/BookRoom.tsx
index 4d11382f..4deef071 100644
--- a/frontend/occupi-mobile4/screens/Booking/BookRoom.tsx
+++ b/frontend/occupi-mobile4/screens/Booking/BookRoom.tsx
@@ -102,90 +102,93 @@ const BookRoom = () => {
const roomPairs = groupDataInPairs(roomData);
return (
-
-
-
- Book
-
-
- Quick search for an office
-
-
- Rooms
-
- {layout === "row" ? (
-
-
-
- ) : (
-
-
-
- )}
-
+ <>
+
+
+
+ Book
+
+
+ Quick search for an office
+
+
+ Rooms
+
+ {layout === "row" ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
-
- {layout === "grid" ? (
-
- {roomPairs.map((pair, index) => (
-
- {pair.map((room, idx) => (
- router.push({ pathname: '/office-details', params: { roomData: JSON.stringify(room) } })}>
-
-
- {room.roomName}
-
-
- {room.description.length > 20 ? `${room.description.substring(0, 40)}...` : room.description}
-
- Floor: {room.floorNo === 0 ? 'G' : room.floorNo}
-
-
- {room.minOccupancy} - {room.maxOccupancy}
+ {layout === "grid" ? (
+
+ {roomPairs.map((pair, index) => (
+
+ {pair.map((room, idx) => (
+ router.push({ pathname: '/office-details', params: { roomData: JSON.stringify(room) } })}>
+
+
+ {room.roomName}
+
+
+ {room.description.length > 20 ? `${room.description.substring(0, 40)}...` : room.description}
+
+ Floor: {room.floorNo === 0 ? 'G' : room.floorNo}
+
+
+ {room.minOccupancy} - {room.maxOccupancy}
+
+
+
+
+ Available: now
+
+
-
-
- Available: now
-
-
+
+ ))}
+
+ ))}
+
+ ) : (
+
+ {roomData.map((room, idx) => (
+ router.push({ pathname: '/office-details', params: { roomData: JSON.stringify(room) } })}>
+
+
+ {room.roomName}
+
+
+ {room.description.length > 20 ? `${room.description.substring(0, 68)}...` : room.description}
+
+ Floor: {room.floorNo === 0 ? "G" : room.floorNo}
+
+
+ {room.minOccupancy} - {room.maxOccupancy}
-
- ))}
-
- ))}
-
- ) : (
-
- {roomData.map((room, idx) => (
- router.push({ pathname: '/office-details', params: { roomData: JSON.stringify(room) } })}>
-
-
- {room.roomName}
-
-
- {room.description.length > 20 ? `${room.description.substring(0, 68)}...` : room.description}
-
- Floor: {room.floorNo === 0 ? "G" : room.floorNo}
-
-
- {room.minOccupancy} - {room.maxOccupancy}
+
+
+ Available: now
+
+
-
-
- Available: now
-
-
-
-
-
- ))}
-
- )}
-
-
+
+ ))}
+
+ )}
+
+
+
+ >
);
};
diff --git a/frontend/occupi-mobile4/screens/Booking/ViewBookingDetails.tsx b/frontend/occupi-mobile4/screens/Booking/ViewBookingDetails.tsx
index e013321d..f918f6b4 100644
--- a/frontend/occupi-mobile4/screens/Booking/ViewBookingDetails.tsx
+++ b/frontend/occupi-mobile4/screens/Booking/ViewBookingDetails.tsx
@@ -17,7 +17,7 @@ import {
EvilIcons,
MaterialIcons
} from '@expo/vector-icons';
-import { useColorScheme, StyleSheet, TouchableOpacity } from 'react-native';
+import { useColorScheme, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native';
import {
widthPercentageToDP as wp
} from 'react-native-responsive-screen';
@@ -33,6 +33,7 @@ const ViewBookingDetails = (bookingId, roomName) => {
const room = JSON.parse(roomData);
const router = useRouter();
const [checkedIn, setCheckedIn] = useState(room.checkedIn);
+ const [isLoading, setIsLoading] = useState(false);
const toast = useToast();
console.log("HERE:" + roomData);
console.log(checkedIn);
@@ -43,6 +44,7 @@ const ViewBookingDetails = (bookingId, roomName) => {
"creator": room.creator,
"roomId": room.roomId
};
+ setIsLoading(true);
console.log(body);
try {
const response = await fetch('https://dev.occupi.tech/api/check-in', {
@@ -64,19 +66,21 @@ const ViewBookingDetails = (bookingId, roomName) => {
placement: 'top',
render: ({ id }) => {
return (
-
+
{data.message}
);
},
});
+ setIsLoading(false);
} else {
+ setIsLoading(false);
console.log(data);
toast.show({
placement: 'top',
render: ({ id }) => {
return (
-
+
{data.message}
);
@@ -93,6 +97,7 @@ const ViewBookingDetails = (bookingId, roomName) => {
"_id": room._id,
"creator": room.creator,
};
+ setIsLoading(true);
console.log(body);
try {
const response = await fetch('https://dev.occupi.tech/api/cancel-booking', {
@@ -113,20 +118,22 @@ const ViewBookingDetails = (bookingId, roomName) => {
placement: 'top',
render: ({ id }) => {
return (
-
+
{data.message}
);
},
});
+ setIsLoading(false);
router.push("/home");
} else {
+ setIsLoading(false);
console.log(data);
toast.show({
placement: 'top',
render: ({ id }) => {
return (
-
+
{data.message}
);
@@ -184,25 +191,46 @@ const ViewBookingDetails = (bookingId, roomName) => {
ViewBooking
- {!checkedIn ? (
- checkin()}>
+ {isLoading ? (
+
- Check in
+
) : (
- checkin()}>
+ !checkedIn ? (
+ checkin()}>
+
+
+ Check in
+
+
+ ) : (
+ checkin()}>
+
+
+ Check out
+
+
+ )
+ )}
+
+
+ {!isLoading ? (
+ cancelBooking()}>
- Check out
+ Delete Booking
+
+
+ ) : (
+
+
+
)}
- cancelBooking()}>
-
- Delete Booking
-
-
+
)
diff --git a/frontend/occupi-mobile4/screens/Booking/ViewBookings.tsx b/frontend/occupi-mobile4/screens/Booking/ViewBookings.tsx
index bc3a8a4c..7e28223a 100644
--- a/frontend/occupi-mobile4/screens/Booking/ViewBookings.tsx
+++ b/frontend/occupi-mobile4/screens/Booking/ViewBookings.tsx
@@ -1,5 +1,5 @@
-import { React, useEffect, useState } from 'react';
-import { ScrollView, useColorScheme, TouchableOpacity } from 'react-native';
+import React, { useEffect, useState, useCallback } from 'react';
+import { ScrollView, useColorScheme, TouchableOpacity, RefreshControl, StyleSheet } from 'react-native';
import {
Icon, View, Text, Input, InputField, Image, Box, ChevronDownIcon, Toast,
ToastTitle,
@@ -30,6 +30,7 @@ interface Room {
minOccupancy: number;
maxOccupancy: number;
description: string;
+ emails: string[];
}
const getTimeForSlot = (slot) => {
@@ -82,7 +83,7 @@ const getTimeForSlot = (slot) => {
return { startTime, endTime };
};
-const slotToTime = (slot : number) => {
+const slotToTime = (slot: number) => {
const { startTime, endTime } = getTimeForSlot(slot);
return `${startTime} - ${endTime}`
}
@@ -96,7 +97,67 @@ const ViewBookings = () => {
// const [selectedSort, setSelectedSort] = useState("newest");
// const [email, setEmail] = useState('kamogelomoeketse@gmail.com');
const router = useRouter();
-
+ const [refreshing, setRefreshing] = useState(false);
+
+
+ const onRefresh = React.useCallback(() => {
+ const fetchAllRooms = async () => {
+ console.log("heree");
+ try {
+ const response = await fetch(`https://dev.occupi.tech/api/view-bookings?email=kamogelomoeketse@gmail.com`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ const data = await response.json();
+ if (response.ok) {
+ setRoomData(data.data || []); // Ensure data is an array
+ console.log(data);
+ // toast.show({
+ // placement: 'top',
+ // render: ({ id }) => {
+ // return (
+ //
+ // {data.message}
+ //
+ // );
+ // },
+ // });
+ } else {
+ console.log(data);
+ toast.show({
+ placement: 'top',
+ render: ({ id }) => {
+ return (
+
+ {data.error.message}
+
+ );
+ },
+ });
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ toast.show({
+ placement: 'top',
+ render: ({ id }) => {
+ return (
+
+ Network Error: {error.message}
+
+ );
+ },
+ });
+ }
+ };
+ setRefreshing(true);
+ setTimeout(() => {
+ setRefreshing(false);
+ fetchAllRooms();
+ }, 2000);
+ }, [toast]);
+
const toggleLayout = () => {
setLayout((prevLayout) => (prevLayout === "row" ? "grid" : "row"));
};
@@ -180,12 +241,12 @@ const ViewBookings = () => {
const roomPairs = groupDataInPairs(roomData);
return (
-
+
- My bookings
+ My bookings
-
+
{
color={textColor}
/>
-
-
- Sort by:
-
+
+
+ Sort by:
+
setSelectedSort(value)}
items={[
@@ -205,10 +266,9 @@ const ViewBookings = () => {
{ label: 'Newest', value: 'Newest' },
]}
placeholder={{ label: 'Latest', value: null }}
- backgroundColor={cardBackgroundColor}
+ // backgroundColor={cardBackgroundColor}
style={{
inputIOS: {
- placeholder: "Latest",
fontSize: 16,
paddingVertical: 8,
borderWidth: 1,
@@ -218,26 +278,29 @@ const ViewBookings = () => {
color: textColor
},
inputAndroid: {
+ alignItems: "center",
+ width: 130,
+ height: 60,
+ fontSize: 10,
+ // paddingVertical: 4,
borderWidth: 1,
borderRadius: 10,
borderColor: cardBackgroundColor,
- paddingRight: 30, // to ensure the text is never behind the icon
+ padding: 0, // to ensure the text is never behind the icon
color: textColor
},
}}
- Icon={() => {
- return ;
- }}
+
/>
{layout === "row" ? (
-
+
) : (
-
+
)}
@@ -245,10 +308,16 @@ const ViewBookings = () => {
{layout === "grid" ? (
-
+
+ }
+ >
{roomPairs.map((pair, index) => (
{
}}
>
{pair.map((room) => (
- router.push({ pathname: '/viewbookingdetails', params: { roomData: JSON.stringify(room) } })}
- style={{
- flex: 1,
- borderWidth: 1,
- borderColor: cardBackgroundColor,
- borderRadius: 12,
- backgroundColor: cardBackgroundColor,
- marginHorizontal: 4,
- width: '45%'
- }}>
+ router.push({ pathname: '/viewbookingdetails', params: { roomData: JSON.stringify(room) } })}
+ style={{
+ flex: 1,
+ borderWidth: 1,
+ borderColor: cardBackgroundColor,
+ borderRadius: 12,
+ backgroundColor: cardBackgroundColor,
+ marginHorizontal: 4,
+ width: '45%'
+ }}>
{
>
{room.roomName}
-
- Attendees: {room.emails.length}
+
+ Attendees: {room.emails.length}
- Your booking time:
+ Your booking time:
-
+
- {new Date().toDateString()}
+ {new Date().toDateString()}
{slotToTime(room.slot)}
@@ -302,26 +371,32 @@ const ViewBookings = () => {
))}
) : (
-
+
+ }
+ >
{roomData.map((room) => (
- router.push({ pathname: '/viewbookingdetails', params: { roomData: JSON.stringify(room) } })}
- style={{
- flex: 1,
- borderWidth: 1,
- borderColor: cardBackgroundColor,
- borderRadius: 12,
- height: 160,
- backgroundColor: cardBackgroundColor,
- marginVertical: 4,
- flexDirection: "row"
-
- }}>
+ router.push({ pathname: '/viewbookingdetails', params: { roomData: JSON.stringify(room) } })}
+ style={{
+ flex: 1,
+ borderWidth: 1,
+ borderColor: cardBackgroundColor,
+ borderRadius: 12,
+ height: 160,
+ backgroundColor: cardBackgroundColor,
+ marginVertical: 4,
+ flexDirection: "row"
+
+ }}>
{
}}
>
{room.roomName}
-
- Attendees: {room.emails.length}
+
+ Attendees: {room.emails.length}
-
- Your booking time:
-
+
+ Your booking time:
+
- {new Date().toDateString()}
+ {new Date().toDateString()}
{slotToTime(room.slot)}
@@ -358,4 +433,16 @@ const ViewBookings = () => {
);
};
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ scrollView: {
+ flex: 1,
+ backgroundColor: 'pink',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+});
+
export default ViewBookings;
diff --git a/frontend/occupi-mobile4/screens/Dashboard/Dashboard.tsx b/frontend/occupi-mobile4/screens/Dashboard/Dashboard.tsx
index 785f2c47..5a232518 100644
--- a/frontend/occupi-mobile4/screens/Dashboard/Dashboard.tsx
+++ b/frontend/occupi-mobile4/screens/Dashboard/Dashboard.tsx
@@ -51,7 +51,7 @@ const Dashboard = () => {
toast.show({
placement: 'top',
render: ({ id }) => (
-
+
Check in successful. Have a productive day!
),
@@ -61,7 +61,7 @@ const Dashboard = () => {
toast.show({
placement: 'top',
render: ({ id }) => (
-
+
Travel safe. Have a lovely day further!
),
@@ -74,7 +74,7 @@ const Dashboard = () => {
const cardBackgroundColor = isDarkMode ? '#2C2C2E' : '#F3F3F3';
return (
-
+
@@ -92,23 +92,23 @@ const Dashboard = () => {
style={{ width: wp('8%'), height: wp('8%'), flexDirection: 'column', tintColor: isDarkMode ? 'white' : 'black' }}
/>
-
+
-
+
{numbers[0]}
-
- {numbers[0]/10+5}%
+
+ {numbers[0]/10+5}%
-
+
{checkedIn ? (
-
+
Check out
) : (
-
+
Check in
)}
diff --git a/frontend/occupi-mobile4/screens/Login/OtpVerification.tsx b/frontend/occupi-mobile4/screens/Login/OtpVerification.tsx
index 4c9dbe1a..6011eb01 100644
--- a/frontend/occupi-mobile4/screens/Login/OtpVerification.tsx
+++ b/frontend/occupi-mobile4/screens/Login/OtpVerification.tsx
@@ -58,12 +58,12 @@ const OTPVerification = () => {
const onSubmit = async () => {
console.log(email);
const pin = otp.join('');
- const Count = otp.filter((value) => value !== '').length;
- if (Count < 6) {
- setValidationError('OTP must be at least 6 characters in length');
- return;
- }
- setValidationError(null);
+ // const Count = otp.filter((value) => value !== '').length;
+ // if (Count < 6) {
+ // setValidationError('OTP must be at least 6 characters in length');
+ // return;
+ // }
+ // setValidationError(null);
console.log(pin);
setLoading(true);
try {
@@ -86,13 +86,13 @@ const OTPVerification = () => {
placement: 'top',
render: ({ id }) => {
return (
-
+
{data.message}
);
},
});
- router.push('/home');
+ router.push('/login');
} else {
setLoading(false);
// console.log(data);
@@ -100,7 +100,7 @@ const OTPVerification = () => {
placement: 'top',
render: ({ id }) => {
return (
-
+
{data.message}
);
@@ -211,7 +211,7 @@ const MainText = (email : string) => {
fontSize={wp('5%')}
fontWeight="$light"
>
- {' '+email}
+ {' '+String(email)}
diff --git a/frontend/occupi-mobile4/screens/Login/SignIn.tsx b/frontend/occupi-mobile4/screens/Login/SignIn.tsx
index eeab7397..4422e20f 100644
--- a/frontend/occupi-mobile4/screens/Login/SignIn.tsx
+++ b/frontend/occupi-mobile4/screens/Login/SignIn.tsx
@@ -114,7 +114,7 @@ const SignInForm = () => {
placement: 'top',
render: ({ id }) => {
return (
-
+
Biometric authentication failed
{result.error && {result.error.message} }
@@ -128,7 +128,7 @@ const SignInForm = () => {
placement: 'top',
render: ({ id }) => {
return (
-
+
Biometric authentication error
{error.message}
@@ -142,7 +142,7 @@ const SignInForm = () => {
placement: 'top',
render: ({ id }) => {
return (
-
+
Biometric authentication not available
);
@@ -180,7 +180,7 @@ const SignInForm = () => {
placement: 'top',
render: ({ id }) => {
return (
-
+
{data.message}
);
@@ -194,7 +194,7 @@ const SignInForm = () => {
placement: 'top',
render: ({ id }) => {
return (
-
+
{data.message}
);
@@ -283,7 +283,7 @@ const SignInForm = () => {
},
}}
render={({ field: { onChange, onBlur, value } }) => (
-
+
{
},
}}
render={({ field: { onChange, onBlur, value } }) => (
-
+
{
placement: 'top',
render: ({ id }) => {
return (
-
+
{data.message}
);
@@ -120,7 +120,7 @@ const SignUpForm = () => {
placement: 'top',
render: ({ id }) => {
return (
-
+
{data.error.message}
);
@@ -136,7 +136,7 @@ const SignUpForm = () => {
placement: 'bottom right',
render: ({ id }) => {
return (
-
+
Passwords do not match
);
@@ -217,7 +217,7 @@ const SignUpForm = () => {
},
}}
render={({ field: { onChange, onBlur, value } }) => (
-
+
{
},
}}
render={({ field: { onChange, onBlur, value } }) => (
-
+
{
},
}}
render={({ field: { onChange, onBlur, value } }) => (
-
+
{
},
}}
render={({ field: { onChange, onBlur, value } }) => (
-
+
{
placement: 'top',
render: ({ id }) => {
return (
-
+
{data.message}
);
@@ -163,7 +163,7 @@ const BookingDetails = () => {
placement: 'top',
render: ({ id }) => {
return (
-
+
{data.message}
);
@@ -295,6 +295,7 @@ const BookingDetails = () => {
style={{
flex: 1,
backgroundColor: isDark ? "#000" : "#fff",
+ paddingTop: 50
}}
>
{
}}
>
navigation.goBack()}>
- router.back()} />
+ router.back()} />
{/* */}
{
{currentStep === 1 && (
-
+
{
> */}
- HDMI Room
+ HDMI Room
Fast
OLED
{roomData.minOccupancy} - {roomData.maxOccupancy}
Floor {roomData.floorNo === 0 ? 'G' : roomData.floorNo}
-
-
+
+
Check in: {startTime}
-
+
Check out: {endTime}
-
-
+
+
----------------------------------------------
-
+
-
+
- Attendees:
+ Attendees:
{attendees.map((email, idx) => (
@@ -612,7 +613,7 @@ const BookingDetails = () => {
{/* */}
router.push('/home')}>
-
+
Home
diff --git a/frontend/occupi-mobile4/screens/Office/OfficeDetails.tsx b/frontend/occupi-mobile4/screens/Office/OfficeDetails.tsx
index cc7c231b..d9b34feb 100644
--- a/frontend/occupi-mobile4/screens/Office/OfficeDetails.tsx
+++ b/frontend/occupi-mobile4/screens/Office/OfficeDetails.tsx
@@ -139,11 +139,10 @@ const OfficeDetails = () => {
return (
<>
{/* Top Section */}
-
- navigation.goBack()} />
-
- {roomData2.roomName}
-
+
+ navigation.goBack()} />
+ {roomData2.roomName}
+
@@ -165,7 +164,7 @@ const OfficeDetails = () => {
*/}
-
+
{
})}
>
{pages.map((page, index) => (
-
+
@@ -194,7 +193,7 @@ const OfficeDetails = () => {
- {roomData2.roomName}
+ {roomData2.roomName}
Fast
OLED
@@ -230,8 +229,8 @@ const OfficeDetails = () => {
{/* Description */}
- Description
-
+ Description
+
{roomData2.description}
@@ -280,7 +279,7 @@ const OfficeDetails = () => {
/>
{/* Check Availability Button */}
- router.push({ pathname: '/booking-details', params: { email: userEmail, slot: slot, roomId: roomData2.roomId, floorNo: roomData2.floorNo, roomData: roomData } })}>
+ router.push({ pathname: '/booking-details', params: { email: userEmail, slot: slot, roomId: roomData2.roomId, floorNo: roomData2.floorNo, roomData: roomData } })}>
{
onConfirm={handleConfirm}
onCancel={hideDatePicker}
/>
- Check availability
+ Check availability
diff --git a/frontend/occupi-mobile4/screens/Profile/Profile.tsx b/frontend/occupi-mobile4/screens/Profile/Profile.tsx
index 36f7ec75..2916da55 100644
--- a/frontend/occupi-mobile4/screens/Profile/Profile.tsx
+++ b/frontend/occupi-mobile4/screens/Profile/Profile.tsx
@@ -86,7 +86,7 @@ const Profile = () => {
router.push('/settings')}
/>
@@ -131,7 +131,7 @@ const Profile = () => {
{
{
{
placement: 'top',
render: ({ id }) => {
return (
-
+
{data.message}
);
@@ -63,7 +63,7 @@ const Settings = () => {
placement: 'top',
render: ({ id }) => {
return (
-
+
{data.message}
);
@@ -125,7 +125,7 @@ const Settings = () => {
{item.description}
- {item.accessoryRight ? item.accessoryRight() : }
+ {item.accessoryRight ? item.accessoryRight() : }
);
@@ -142,7 +142,7 @@ const Settings = () => {
-
+
{name}
{/* handleNavigate('EditProfileScreen')} /> */}
@@ -150,7 +150,7 @@ const Settings = () => {
-
+
{data.map((item, index) => (
{renderListItem({ item })}
diff --git a/occupi-backend/.dev.env.gpg b/occupi-backend/.dev.env.gpg
deleted file mode 100644
index 2ceb8bc6..00000000
--- a/occupi-backend/.dev.env.gpg
+++ /dev/null
@@ -1,2 +0,0 @@
-
Uv:Ӡе]YEq~LE>Z&P_$F/b?~ւE* +QNq@M1hZp"_u8p4
pAt H $ 0 {
+ viper.AddConfigPath(configpath[0])
+ } else {
+ viper.AddConfigPath("./configs")
+ }
+ if err := viper.ReadInConfig(); err != nil {
+ log.Fatalf("Error reading config file: %s", err)
+ }
-// gets the port to start the server on as defined in the .env file
+ // Merge environment-specific config
+ viper.SetConfigName(*envtype)
+ if err := viper.MergeInConfig(); err != nil {
+ log.Fatalf("Error merging config file: %s", err)
+ }
+}
+
+// gets the port to start the server on as defined in the config.yaml file
func GetPort() string {
- port := os.Getenv("PORT")
+ port := viper.GetString(Port)
if port == "" {
port = "PORT"
}
return port
}
-// gets the mongodb username as defined in the .env file
+// gets the mongodb username as defined in the config.yaml file
func GetMongoDBUsername() string {
- username := os.Getenv("MONGODB_USERNAME")
+ username := viper.GetString(MongodbUsername)
if username == "" {
username = "MONGODB_USERNAME"
}
return username
}
-// gets the mongodb password as defined in the .env file
+// gets the mongodb password as defined in the config.yaml file
func GetMongoDBPassword() string {
- password := os.Getenv("MONGODB_PASSWORD")
+ password := viper.GetString(MongodbPassword)
if password == "" {
password = "MONGODB_PASSWORD"
}
return password
}
-// gets the mongodb cluster uri as defined in the .env file
+// gets the mongodb cluster uri as defined in the config.yaml file
func GetMongoDBCLUSTERURI() string {
- uri := os.Getenv("MONGODB_CLUSTERURI")
+ uri := viper.GetString(MongodbClusteruri)
if uri == "" {
uri = "MONGODB_CLUSTERURI"
}
return uri
}
-// gets the mongodb name as defined in the .env file
+// gets the mongodb name as defined in the config.yaml file
func GetMongoDBName() string {
- name := os.Getenv("MONGODB_DBNAME")
+ name := viper.GetString(MongodbDbname)
if name == "" {
name = "MONGODB_DBNAME"
}
return name
}
-// gets the mongodb start uri as defined in the .env file
+// gets the mongodb start uri as defined in the config.yaml file
func GetMongoDBStartURI() string {
- startURI := os.Getenv("MONGODB_START_URI")
+ startURI := viper.GetString(MongodbStartURI)
if startURI == "" {
startURI = "MONGODB_START_URI"
}
return startURI
}
+// gets the log file name as defined in the config.yaml file
func GetLogFileName() string {
- logFileName := os.Getenv("LOG_FILE_NAME")
+ logFileName := viper.GetString(LogFileName)
if logFileName == "" {
logFileName = "LOG_FILE_NAME"
}
return logFileName
}
-// gets the system email as defined in the .env file
+// gets the system email as defined in the config.yaml file
func GetSystemEmail() string {
- email := os.Getenv("SYSTEM_EMAIL")
+ email := viper.GetString(SystemEmail)
if email == "" {
email = ""
}
@@ -82,7 +125,7 @@ func GetSystemEmail() string {
// GetSMTPPort retrieves the SMTP port from the environment and converts it to an integer.
// If the environment variable is not set, it returns the default port 587.
func GetSMTPPort() int {
- port := os.Getenv("SMTP_PORT")
+ port := viper.GetString(SMTPPort)
if port == "" {
return 587
}
@@ -95,54 +138,54 @@ func GetSMTPPort() int {
return portInt
}
-// gets the smtp password as defined in the .env file
+// gets the smtp password as defined in the config.yaml file
func GetSMTPPassword() string {
- password := os.Getenv("SMTP_PASSWORD")
+ password := viper.GetString(SMTPPassword)
if password == "" {
password = ""
}
return password
}
-// gets the smtp host as defined in the .env file
+// gets the smtp host as defined in the config.yaml file
func GetSMTPHost() string {
- host := os.Getenv("SMTP_HOST")
+ host := viper.GetString(SMTPHost)
if host == "" {
host = "smtp.gmail.com"
}
return host
}
-// gets the certificate file name as defined in the .env file
+// gets the certificate file name as defined in the config.yaml file
func GetCertFileName() string {
- certFileName := os.Getenv("CERTIFICATE_FILE_PATH")
+ certFileName := viper.GetString(CertificateFilePath)
if certFileName == "" {
certFileName = "CERTIFICATE_FILE_PATH"
}
return certFileName
}
-// gets the key file name as defined in the .env file
+// gets the key file name as defined in the config.yaml file
func GetKeyFileName() string {
- keyFileName := os.Getenv("KEY_FILE_PATH")
+ keyFileName := viper.GetString(KeyFilePath)
if keyFileName == "" {
keyFileName = "KEY_FILE_PATH"
}
return keyFileName
}
-// gets gins run mode as defined in the .env file
+// gets gins run mode as defined in the config.yaml file
func GetGinRunMode() string {
- ginRunMode := os.Getenv("GIN_RUN_MODE")
+ ginRunMode := viper.GetString(GinRunMode)
if ginRunMode == "" {
ginRunMode = "debug"
}
return ginRunMode
}
-// gets list of trusted proxies as defined in the .env file
+// gets list of trusted proxies as defined in the config.yaml file
func GetTrustedProxies() []string {
- trustedProxies := os.Getenv("TRUSTED_PROXIES")
+ trustedProxies := viper.GetString(TrustedProxies)
if trustedProxies != "" {
proxyList := strings.Split(trustedProxies, ",")
return proxyList
@@ -150,27 +193,39 @@ func GetTrustedProxies() []string {
return []string{""}
}
+// gets the JWT secret as defined in the config.yaml file
func GetJWTSecret() string {
- secret := os.Getenv("JWT_SECRET")
+ secret := viper.GetString(JwtSecret)
if secret == "" {
secret = "JWT_SECRET"
}
return secret
}
+// gets the session secret as defined in the config.yaml file
func GetSessionSecret() string {
- secret := os.Getenv("SESSION_SECRET")
+ secret := viper.GetString(SessionSecret)
if secret == "" {
secret = "SESSION_SECRET"
}
return secret
}
+// gets the list of occupi domains as defined in the config.yaml file
func GetOccupiDomains() []string {
- domains := os.Getenv("OCCUPI_DOMAINS")
+ domains := viper.GetString(OccupiDomains)
if domains != "" {
domainList := strings.Split(domains, ",")
return domainList
}
return []string{""}
}
+
+// gets the environment type as defined in the config.yaml file
+func GetEnv() string {
+ env := viper.GetString(Env)
+ if env == "" {
+ env = "ENV"
+ }
+ return env
+}
diff --git a/occupi-backend/configs/config.yaml.gpg b/occupi-backend/configs/config.yaml.gpg
new file mode 100644
index 00000000..92257cc4
--- /dev/null
+++ b/occupi-backend/configs/config.yaml.gpg
@@ -0,0 +1 @@
+
G/#=j\>O%"ƫT
p[!\ZB>cUkukoP)Af{7zO]7.%"fAg^RF-iA*` go run -v cmd/occupi-backend/main.go"
- echo " run prod -> go run cmd/occupi-backend/main.go"
- echo " build dev -> go build -v cmd/occupi-backend/main.go"
- echo " build prod -> go build cmd/occupi-backend/main.go"
- echo " docker build -> docker-compose build"
- echo " docker up -> docker-compose up"
+ echo " run dev -> go run -v cmd/occupi-backend/main.go -env=dev.localhost"
+ echo " run prod -> go run cmd/occupi-backend/main.go -env=dev.localhost"
+ echo " build dev -> go build -v cmd/occupi-backend/main.go -env=dev.localhost"
+ echo " build prod -> go build cmd/occupi-backend/main.go -env=dev.localhost"
echo " test -> go test ./tests/..."
echo " test codecov -> go test ./tests/... -race -coverprofile=coverage.out -covermode=atomic"
echo " lint -> golangci-lint run"
@@ -17,17 +15,13 @@ print_help() {
}
if [ "$1" = "run" ] && [ "$2" = "dev" ]; then
- go run -v cmd/occupi-backend/main.go
+ go run -v cmd/occupi-backend/main.go -env=dev.localhost
elif [ "$1" = "run" ] && [ "$2" = "prod" ]; then
- go run cmd/occupi-backend/main.go
+ go run cmd/occupi-backend/main.go -env=dev.localhost
elif [ "$1" = "build" ] && [ "$2" = "dev" ]; then
- go build -v cmd/occupi-backend/main.go
+ go build -v cmd/occupi-backend/main.go -env=dev.localhost
elif [ "$1" = "build" ] && [ "$2" = "prod" ]; then
- go build cmd/occupi-backend/main.go
-elif [ "$1" = "docker" ] && [ "$2" = "build" ]; then
- docker-compose build
-elif [ "$1" = "docker" ] && [ "$2" = "up" ]; then
- docker-compose up
+ go build cmd/occupi-backend/main.go -env=dev.localhost
elif [ "$1" = "test" ]; then
go test -v ./tests/...
elif [ "$1" = "test" ] && [ "$2" = "codecov" ]; then
diff --git a/occupi-backend/pkg/constants/constants.go b/occupi-backend/pkg/constants/constants.go
index 3f2d0a6a..a3241054 100644
--- a/occupi-backend/pkg/constants/constants.go
+++ b/occupi-backend/pkg/constants/constants.go
@@ -1,10 +1,13 @@
package constants
-const InvalidRequestPayloadCode = "INVALID_REQUEST_PAYLOAD"
-const BadRequestCode = "BAD_REQUEST"
-const InvalidAuthCode = "INVALID_AUTH"
-const IncompleteAuthCode = "INCOMPLETE_AUTH"
-const InternalServerErrorCode = "INTERNAL_SERVER_ERROR"
-const UnAuthorizedCode = "UNAUTHORIZED"
-const Admin = "admin"
-const Basic = "basic"
+const (
+ InvalidRequestPayloadCode = "INVALID_REQUEST_PAYLOAD"
+ BadRequestCode = "BAD_REQUEST"
+ InvalidAuthCode = "INVALID_AUTH"
+ IncompleteAuthCode = "INCOMPLETE_AUTH"
+ InternalServerErrorCode = "INTERNAL_SERVER_ERROR"
+ UnAuthorizedCode = "UNAUTHORIZED"
+ Admin = "admin"
+ Basic = "basic"
+ AdminDBAccessOption = "authSource=admin"
+)
diff --git a/occupi-backend/pkg/database/database.go b/occupi-backend/pkg/database/database.go
index 24b38db3..3c4b826e 100644
--- a/occupi-backend/pkg/database/database.go
+++ b/occupi-backend/pkg/database/database.go
@@ -20,7 +20,7 @@ import (
)
// attempts to and establishes a connection with the remote mongodb database
-func ConnectToDatabase() *mongo.Client {
+func ConnectToDatabase(args ...string) *mongo.Client {
// MongoDB connection parameters
username := configs.GetMongoDBUsername()
password := configs.GetMongoDBPassword()
@@ -32,7 +32,12 @@ func ConnectToDatabase() *mongo.Client {
escapedPassword := url.QueryEscape(password)
// Construct the connection URI
- uri := fmt.Sprintf("%s://%s:%s@%s/%s", mongoDBStartURI, username, escapedPassword, clusterURI, dbName)
+ var uri string
+ if len(args) > 0 {
+ uri = fmt.Sprintf("%s://%s:%s@%s/%s?%s", mongoDBStartURI, username, escapedPassword, clusterURI, dbName, args[0])
+ } else {
+ uri = fmt.Sprintf("%s://%s:%s@%s/%s", mongoDBStartURI, username, escapedPassword, clusterURI, dbName)
+ }
// Set client options
clientOptions := options.Client().ApplyURI(uri)
@@ -40,13 +45,13 @@ func ConnectToDatabase() *mongo.Client {
// Connect to MongoDB
client, err := mongo.Connect(context.TODO(), clientOptions)
if err != nil {
- logrus.Error(err)
+ logrus.Fatal(err)
}
// Check the connection
err = client.Ping(context.TODO(), nil)
if err != nil {
- logrus.Error(err)
+ logrus.Fatal(err)
}
logrus.Info("Connected to MongoDB!")
@@ -130,7 +135,6 @@ func ConfirmCheckIn(ctx *gin.Context, db *mongo.Client, checkIn models.CheckIn)
// Find the booking by bookingId, roomId, and creator
filter := bson.M{
"_id": checkIn.BookingID,
- "roomId": checkIn.RoomID,
"creator": checkIn.Creator,
}
@@ -365,8 +369,8 @@ func ConfirmCancellation(ctx *gin.Context, db *mongo.Client, id string, email st
}
// Gets all rooms available for booking
-func GetAllRooms(ctx *gin.Context, db *mongo.Client, floorNo int) ([]models.Room, error) {
- collection := db.Database("Occupi").Collection("Rooms")
+func GetAllRooms(ctx *gin.Context, db *mongo.Client, floorNo string) ([]models.Room, error) {
+ collection := db.Database("Occupi").Collection("RoomsV2")
var cursor *mongo.Cursor
var err error
@@ -375,17 +379,10 @@ func GetAllRooms(ctx *gin.Context, db *mongo.Client, floorNo int) ([]models.Room
// findOptions.SetLimit(10) // Limit the results to 10
// findOptions.SetSkip(int64(10)) // Skip the specified number of documents for pagination
- if floorNo == 0 {
- // Find all rooms
- filter := bson.M{"floorNo": 0}
- // cursor, err = collection.Find(context.TODO(), filter, findOptions)
- cursor, err = collection.Find(context.TODO(), filter)
- } else {
- // Find all rooms on the specified floor
- filter := bson.M{"floorNo": floorNo}
- // cursor, err = collection.Find(context.TODO(), filter, findOptions)
- cursor, err = collection.Find(context.TODO(), filter)
- }
+ // Find all rooms on the specified floor
+ filter := bson.M{"floorNo": floorNo}
+ // cursor, err = collection.Find(context.TODO(), filter, findOptions)
+ cursor, err = collection.Find(context.TODO(), filter)
if err != nil {
logrus.Error(err)
diff --git a/occupi-backend/pkg/database/seed.go b/occupi-backend/pkg/database/seed.go
new file mode 100644
index 00000000..910fa8e9
--- /dev/null
+++ b/occupi-backend/pkg/database/seed.go
@@ -0,0 +1,86 @@
+package database
+
+import (
+ "context"
+ "log"
+ "os"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+
+ "github.com/COS301-SE-2024/occupi/occupi-backend/configs"
+ "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/constants"
+ "github.com/COS301-SE-2024/occupi/occupi-backend/pkg/models"
+)
+
+type MockDatabase struct {
+ OTPS []models.OTP `json:"otps"`
+ Bookings []models.Booking `json:"bookings"`
+ Rooms []models.Room `json:"rooms"`
+ Users []models.User `json:"users"`
+}
+
+func SeedMockDatabase(mockdatafilepath string) {
+ // connect to the database
+ db := ConnectToDatabase(constants.AdminDBAccessOption)
+
+ // Read the JSON file
+ data, err := os.ReadFile(mockdatafilepath)
+ if err != nil {
+ log.Fatalf("Failed to read JSON file: %v", err)
+ }
+
+ // Parse the JSON file
+ var mockDatabase MockDatabase
+ if err := bson.UnmarshalExtJSON(data, true, &mockDatabase); err != nil {
+ log.Fatalf("Failed to unmarshal JSON data: %v", err)
+ }
+
+ // Insert data into each collection
+ otpsDocuments := make([]interface{}, 0, len(mockDatabase.OTPS))
+ for _, otps := range mockDatabase.OTPS {
+ otpsDocuments = append(otpsDocuments, otps)
+ }
+ insertData(db.Database(configs.GetMongoDBName()).Collection("OTPS"), otpsDocuments)
+
+ BookingsDocuments := make([]interface{}, 0, len(mockDatabase.Bookings))
+ for _, roomBooking := range mockDatabase.Bookings {
+ BookingsDocuments = append(BookingsDocuments, roomBooking)
+ }
+ insertData(db.Database(configs.GetMongoDBName()).Collection("RoomBooking"), BookingsDocuments)
+
+ roomsDocuments := make([]interface{}, 0, len(mockDatabase.Rooms))
+ for _, rooms := range mockDatabase.Rooms {
+ roomsDocuments = append(roomsDocuments, rooms)
+ }
+ insertData(db.Database(configs.GetMongoDBName()).Collection("Rooms"), roomsDocuments)
+
+ usersDocuments := make([]interface{}, 0, len(mockDatabase.Users))
+ for _, users := range mockDatabase.Users {
+ usersDocuments = append(usersDocuments, users)
+ }
+ insertData(db.Database(configs.GetMongoDBName()).Collection("Users"), usersDocuments)
+
+ log.Println("Successfully seeded test data into MongoDB")
+}
+
+// Function to insert data if the collection is empty
+func insertData(collection *mongo.Collection, documents []interface{}) {
+ count, err := collection.CountDocuments(context.Background(), bson.D{})
+ if err != nil {
+ log.Fatalf("Failed to count documents: %v", err)
+ }
+
+ switch {
+ case count == 0 && len(documents) > 0:
+ _, err := collection.InsertMany(context.Background(), documents)
+ if err != nil {
+ log.Fatalf("Failed to insert documents into %s collection: %v", collection.Name(), err)
+ }
+ log.Printf("Successfully seeded data into %s collection\n", collection.Name())
+ case len(documents) == 0:
+ log.Printf("No documents to insert into %s skipping seeding\n", collection.Name())
+ default:
+ log.Printf("Collection %s already has %d documents, skipping seeding\n", collection.Name(), count)
+ }
+}
diff --git a/occupi-backend/pkg/handlers/api_handlers.go b/occupi-backend/pkg/handlers/api_handlers.go
index 1c4c9214..0903f0a4 100644
--- a/occupi-backend/pkg/handlers/api_handlers.go
+++ b/occupi-backend/pkg/handlers/api_handlers.go
@@ -1,13 +1,16 @@
package handlers
import (
+ "encoding/json"
"errors"
"io"
"net/http"
+ "reflect"
"github.com/COS301-SE-2024/occupi/occupi-backend/pkg/constants"
"github.com/COS301-SE-2024/occupi/occupi-backend/pkg/database"
"github.com/COS301-SE-2024/occupi/occupi-backend/pkg/models"
+ "github.com/go-playground/validator/v10"
"go.mongodb.org/mongo-driver/bson/primitive"
"github.com/COS301-SE-2024/occupi/occupi-backend/pkg/mail"
@@ -51,47 +54,41 @@ func FetchResourceAuth(ctx *gin.Context, appsession *models.AppSession) {
// BookRoom handles booking a room and sends a confirmation email
func BookRoom(ctx *gin.Context, appsession *models.AppSession) {
- // consider structuring api responses to match that as outlined in our coding standards documentation
- //link: https://cos301-se-2024.github.io/occupi/coding-standards/go-coding-standards#response-and-error-handling
+ var bookingRequest map[string]interface{}
+ if err := ctx.ShouldBindJSON(&bookingRequest); err != nil {
+ HandleValidationErrors(ctx, err)
+ return
+ }
+
+ // Validate JSON
+ validatedData, err := utils.ValidateJSON(bookingRequest, reflect.TypeOf(models.Booking{}))
+ if err != nil {
+ ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.BadRequestCode, err.Error(), nil))
+ return
+ }
+
+ // Convert validated data to Booking struct
var booking models.Booking
- if err := ctx.ShouldBindJSON(&booking); err != nil {
- ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Expected RoomID,Slot,Emails[],Creator,FloorNo ", nil))
- return
- }
- // // Initialize the validator
- // validate := validator.New()
-
- // // Validate the booking struct
- // if err := validate.Struct(booking); err != nil {
- // validationErrors := err.(validator.ValidationErrors)
- // errorDetails := make(gin.H)
- // for _, validationError := range validationErrors {
- // errorDetails[validationError.Field()] = validationError.Tag()
- // }
- // ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Validation failed", constants.InvalidRequestPayloadCode, "Validation errors occurred", errorDetails))
- // return
- // }
+ bookingBytes, _ := json.Marshal(validatedData)
+ if err := json.Unmarshal(bookingBytes, &booking); err != nil {
+ ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to book", constants.InternalServerErrorCode, "Failed to check in", nil))
+ return
+ }
+
// Generate a unique ID for the booking
booking.ID = primitive.NewObjectID().Hex()
booking.OccupiID = utils.GenerateBookingID()
booking.CheckedIn = false
// Save the booking to the database
- _, err := database.SaveBooking(ctx, appsession.DB, booking)
+ _, err = database.SaveBooking(ctx, appsession.DB, booking)
if err != nil {
ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to save booking", constants.InternalServerErrorCode, "Failed to save booking", nil))
return
}
- // Prepare the email content
- subject := "Booking Confirmation - Occupi"
- body := mail.FormatBookingEmailBodyForBooker(booking.ID, booking.RoomID, booking.Slot, booking.Emails)
-
- // Send the confirmation email concurrently to all recipients
- emailErrors := mail.SendMultipleEmailsConcurrently(booking.Emails, subject, body)
-
- if len(emailErrors) > 0 {
- ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to send confirmation email", constants.InternalServerErrorCode, "Failed to send confirmation email", nil))
+ if err := mail.SendBookingEmails(booking); err != nil {
+ ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to send booking email", constants.InternalServerErrorCode, "Failed to send booking email", nil))
return
}
@@ -102,7 +99,7 @@ func BookRoom(ctx *gin.Context, appsession *models.AppSession) {
func ViewBookings(ctx *gin.Context, appsession *models.AppSession) {
// Extract the email query parameter
email := ctx.Query("email")
- if email == "" {
+ if email == "" || !utils.ValidateEmail(email) {
ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Expected Email Address", nil))
return
}
@@ -110,44 +107,51 @@ func ViewBookings(ctx *gin.Context, appsession *models.AppSession) {
// Get all bookings for the userBooking
bookings, err := database.GetUserBookings(ctx, appsession.DB, email)
if err != nil {
- ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to get bookings", constants.InternalServerErrorCode, "Failed to get bookings", nil))
+ ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusNotFound, "Failed to get bookings", constants.InternalServerErrorCode, "Failed to get bookings", nil))
return
}
ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully fetched bookings!", bookings))
}
-// Cancel booking handles the cancellation of a booking
func CancelBooking(ctx *gin.Context, appsession *models.AppSession) {
- var booking models.Booking
- if err := ctx.ShouldBindJSON(&booking); err != nil {
- ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Expected Booking ID, Room ID, and Email Address", nil))
+ var cancelRequest map[string]interface{}
+ if err := ctx.ShouldBindJSON(&cancelRequest); err != nil {
+ ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Invalid JSON payload", nil))
+ return
+ }
+
+ // Validate JSON
+ validatedData, err := utils.ValidateJSON(cancelRequest, reflect.TypeOf(models.Cancel{}))
+ if err != nil {
+ ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.BadRequestCode, err.Error(), nil))
+ return
+ }
+
+ // Convert validated JSON to Cancel struct
+ var cancel models.Cancel
+ cancelBytes, _ := json.Marshal(validatedData)
+ if err := json.Unmarshal(cancelBytes, &cancel); err != nil {
+ ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to cancel", constants.InternalServerErrorCode, "Failed to check in", nil))
return
}
// Check if the booking exists
- exists := database.BookingExists(ctx, appsession.DB, booking.ID)
+ exists := database.BookingExists(ctx, appsession.DB, cancel.BookingID)
if !exists {
- ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(404, "Booking not found", constants.InternalServerErrorCode, "Booking not found", nil))
+ ctx.JSON(http.StatusNotFound, utils.ErrorResponse(http.StatusNotFound, "Booking not found", constants.InternalServerErrorCode, "Booking not found", nil))
return
}
// Confirm the cancellation to the database
- _, err := database.ConfirmCancellation(ctx, appsession.DB, booking.ID, booking.Creator)
+ _, err = database.ConfirmCancellation(ctx, appsession.DB, cancel.BookingID, cancel.Creator)
if err != nil {
ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to cancel booking", constants.InternalServerErrorCode, "Failed to cancel booking", nil))
return
}
- // Prepare the email content
- subject := "Booking Cancelled - Occupi"
- body := mail.FormatBookingEmailBody(booking.ID, booking.RoomID, booking.Slot)
-
- // Send the confirmation email concurrently to all recipients
- emailErrors := mail.SendMultipleEmailsConcurrently(booking.Emails, subject, body)
-
- if len(emailErrors) > 0 {
- ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to send cancellation email", constants.InternalServerErrorCode, "Failed to send cancellation email", nil))
+ if err := mail.SendCancellationEmails(cancel); err != nil {
+ ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "An error occurred", constants.InternalServerErrorCode, "Failed to send booking email", nil))
return
}
@@ -156,21 +160,36 @@ func CancelBooking(ctx *gin.Context, appsession *models.AppSession) {
// CheckIn handles the check-in process for a booking
func CheckIn(ctx *gin.Context, appsession *models.AppSession) {
- var checkIn models.CheckIn
+ var checkInRequest map[string]interface{}
+ if err := ctx.ShouldBindJSON(&checkInRequest); err != nil {
+ HandleValidationErrors(ctx, err)
+ return
+ }
- if err := ctx.ShouldBindJSON(&checkIn); err != nil {
- ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Expected Booking ID, Room ID, and Creator Email Address", nil))
+ // Validate JSON
+ validatedData, err := utils.ValidateJSON(checkInRequest, reflect.TypeOf(models.CheckIn{}))
+ if err != nil {
+ ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.BadRequestCode, err.Error(), nil))
+ return
+ }
+
+ // Convert validated JSON to CheckIn struct
+ var checkIn models.CheckIn
+ checkInBytes, _ := json.Marshal(validatedData)
+ if err := json.Unmarshal(checkInBytes, &checkIn); err != nil {
+ ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to check in", constants.InternalServerErrorCode, "Failed to check in", nil))
return
}
// Check if the booking exists
exists := database.BookingExists(ctx, appsession.DB, checkIn.BookingID)
if !exists {
- ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to find booking", constants.InternalServerErrorCode, "Failed to find booking", nil))
+ ctx.JSON(http.StatusNotFound, utils.ErrorResponse(http.StatusNotFound, "Booking not found", constants.InternalServerErrorCode, "Booking not found", nil))
return
}
+
// Confirm the check-in to the database
- _, err := database.ConfirmCheckIn(ctx, appsession.DB, checkIn)
+ _, err = database.ConfirmCheckIn(ctx, appsession.DB, checkIn)
if err != nil {
ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to check in", constants.InternalServerErrorCode, "Failed to check in. Email not associated with booking", nil))
return
@@ -179,23 +198,37 @@ func CheckIn(ctx *gin.Context, appsession *models.AppSession) {
ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully checked in!", nil))
}
-// View all available rooms
func ViewRooms(ctx *gin.Context, appsession *models.AppSession) {
- var room models.Room
- if err := ctx.ShouldBindJSON(&room); err != nil && !errors.Is(err, io.EOF) {
- ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Expected Floor No", nil))
+ var roomRequest map[string]interface{}
+ var room models.RoomRequest
+ if err := ctx.ShouldBindJSON(&roomRequest); err != nil && !errors.Is(err, io.EOF) {
+ HandleValidationErrors(ctx, err)
return
}
- var rooms []models.Room
- var err error
- if room.FloorNo != -1 {
- // If FloorNo is provided, filter by FloorNo
- rooms, err = database.GetAllRooms(ctx, appsession.DB, room.FloorNo)
+
+ // Validate JSON
+ validatedData, err := utils.ValidateJSON(roomRequest, reflect.TypeOf(models.RoomRequest{}))
+ if err != nil {
+ ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, err.Error(), constants.BadRequestCode, err.Error(), nil))
+ return
+ }
+
+ // Convert validated JSON to RoomRequest struct
+ roomBytes, _ := json.Marshal(validatedData)
+ if err := json.Unmarshal(roomBytes, &room); err != nil {
+ ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to get room", constants.InternalServerErrorCode, "Failed to check in", nil))
+ return
+ }
+
+ var floorNo string
+ if room.FloorNo == "" {
+ floorNo = "0"
} else {
- // If FloorNo is not provided, fetch all rooms on the ground floor
- rooms, err = database.GetAllRooms(ctx, appsession.DB, 0)
+ floorNo = room.FloorNo
}
+ var rooms []models.Room
+ rooms, err = database.GetAllRooms(ctx, appsession.DB, floorNo)
if err != nil {
ctx.JSON(http.StatusInternalServerError, utils.ErrorResponse(http.StatusInternalServerError, "Failed to get rooms", constants.InternalServerErrorCode, "Failed to get rooms", nil))
return
@@ -203,3 +236,18 @@ func ViewRooms(ctx *gin.Context, appsession *models.AppSession) {
ctx.JSON(http.StatusOK, utils.SuccessResponse(http.StatusOK, "Successfully fetched rooms!", rooms))
}
+
+// Helper function to handle validation of requests
+func HandleValidationErrors(ctx *gin.Context, err error) {
+ var ve validator.ValidationErrors
+ if errors.As(err, &ve) {
+ out := make([]models.ErrorMsg, len(ve))
+ for i, err := range ve {
+ out[i] = models.ErrorMsg{
+ Field: utils.LowercaseFirstLetter(err.Field()),
+ Message: models.GetErrorMsg(err),
+ }
+ }
+ ctx.JSON(http.StatusBadRequest, utils.ErrorResponse(http.StatusBadRequest, "Invalid request payload", constants.InvalidRequestPayloadCode, "Invalid request payload", gin.H{"errors": out}))
+ }
+}
diff --git a/occupi-backend/pkg/handlers/auth_handlers.go b/occupi-backend/pkg/handlers/auth_handlers.go
index 766785d8..e4c33dcd 100644
--- a/occupi-backend/pkg/handlers/auth_handlers.go
+++ b/occupi-backend/pkg/handlers/auth_handlers.go
@@ -172,7 +172,7 @@ func Login(ctx *gin.Context, appsession *models.AppSession, role string) {
logrus.Error(err)
return
}
- ctx.SetCookie("token", token, int(expirationTime.Unix()), "/", "", false, true)
+ ctx.SetCookie("token", token, int(time.Until(expirationTime).Seconds()), "/", "", false, true)
ctx.JSON(http.StatusOK, utils.SuccessResponse(
http.StatusOK,
diff --git a/occupi-backend/pkg/mail/email_format.go b/occupi-backend/pkg/mail/email_format.go
index 9ea9dc1d..846abae0 100644
--- a/occupi-backend/pkg/mail/email_format.go
+++ b/occupi-backend/pkg/mail/email_format.go
@@ -21,7 +21,7 @@ func FormatBookingEmailBody(bookingID string, roomID string, slot int) string {
}
// formats booking email body to send person who booked
-func FormatBookingEmailBodyForBooker(bookingID string, roomID string, slot int, attendees []string) string {
+func FormatBookingEmailBodyForBooker(bookingID string, roomID string, slot int, attendees []string, email string) string {
listOfAttendees := ""
for _, email := range attendees {
listOfAttendees += "" + email + " "
@@ -44,6 +44,23 @@ func FormatBookingEmailBodyForBooker(bookingID string, roomID string, slot int,
` + appendFooter()
}
+// formats cancellation email body to send person who booked
+func FormatCancellationEmailBodyForBooker(bookingID string, roomID string, slot int, email string) string {
+
+ return appendHeader("Cancellation") + `
+
+
Dear booker,
+
+ You have successfully cancelled your booked office space. Here are the booking details:
+ Booking ID: ` + bookingID + `
+ Room ID: ` + roomID + `
+ Slot: ` + strconv.Itoa(slot) + `
+ Thank you,
+ The Occupi Team
+
+
` + appendFooter()
+}
+
// formats booking email body to send attendees
func FormatBookingEmailBodyForAttendees(bookingID string, roomID string, slot int, email string) string {
return appendHeader("Booking") + `
@@ -61,6 +78,23 @@ func FormatBookingEmailBodyForAttendees(bookingID string, roomID string, slot in
` + appendFooter()
}
+// formats cancellation email body to send attendees
+func FormatCancellationEmailBodyForAttendees(bookingID string, roomID string, slot int, email string) string {
+ return appendHeader("Booking") + `
+
+
Dear attendees,
+
+ ` + email + ` has cancelled the booked office space with the following details:
+ Booking ID: ` + bookingID + `
+ Room ID: ` + roomID + `
+ Slot: ` + strconv.Itoa(slot) + `
+ If you have any questions, feel free to contact us.
+ Thank you,
+ The Occupi Team
+
+
` + appendFooter()
+}
+
// formats verification email body
func FormatEmailVerificationBody(otp string, email string) string {
return appendHeader("Registration") + `
@@ -115,10 +149,11 @@ func appendHeader(title string) string {
}
func appendFooter() string {
- return `
+ return `
+