diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b86b39a --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +VITE_PORT=8081 \ No newline at end of file diff --git a/.github/workflows/deploy-documentation.yml b/.github/workflows/deploy-documentation.yml index 02351bf..03c3081 100644 --- a/.github/workflows/deploy-documentation.yml +++ b/.github/workflows/deploy-documentation.yml @@ -18,10 +18,10 @@ jobs: run: | sphinx-build docs/source _build - name: Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-Pages@v3 if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} with: - publish_branch: gh-pages + publish_branch: gh-Pages github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: _build/ - force_orphan: true \ No newline at end of file + force_orphan: true diff --git a/.gitignore b/.gitignore index d2a6fa5..e4c9141 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ # Documentation build/ +!server/build/ # Logs logs diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..35c5e34 --- /dev/null +++ b/Makefile @@ -0,0 +1,59 @@ +# Colors +GREEN := \033[0;32m +YELLOW := \033[0;33m +WHITE := \033[0;37m +RESET := \033[0m + +# Target help text +TARGET_MAX_CHAR_NUM=20 + +.PHONY: start build stop restart reset logs clean help + +PROJECT_IMAGES = area-client-web area-client-mobile + +## Show help +help: + @printf '\n' + @printf 'Usage:\n' + @printf ' $(YELLOW)make$(RESET) $(GREEN)$(RESET)\n' + @printf '\n' + @printf 'Targets:\n' + @awk '/^[a-zA-Z\-\_0-9]+:/ { \ + helpMessage = match(lastLine, /^## (.*)/); \ + if (helpMessage) { \ + helpCommand = substr($$1, 0, index($$1, ":")-1); \ + helpMessage = substr(lastLine, RSTART + 3, RLENGTH); \ + printf " $(YELLOW)%-$(TARGET_MAX_CHAR_NUM)s$(RESET) $(GREEN)%s$(RESET)\n", helpCommand, helpMessage; \ + } \ + } \ + { lastLine = $$0 }' $(MAKEFILE_LIST) + @printf '\n' + +## Start containers in detached mode +start: + docker compose up -d + +## Build and start containers in detached mode +build: + docker compose up --build -d + +## Stop all containers +stop: + docker compose down + +## Restart all containers +restart: stop start + +## Reset containers, remove images and rebuild +reset: + docker compose down + docker rmi $(PROJECT_IMAGES) -f + docker compose up --build -d + +## Show container logs +logs: + docker compose logs -f + +## Clean up containers, images, volumes and orphans +clean: + docker compose down --rmi local -v --remove-orphans \ No newline at end of file diff --git a/README.md b/README.md index dfadc88..fc47963 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,16 @@ With AREA, you can create automated workflows that integrate various services an git clone git@github.com:ASM-Studios/AREA.git ``` -2. Install NPM packages +2. Create .env files +- Run the following command to create private env files +```sh +cp .env.example .env +cp client_web/.env.local.example .env.local +cp client_mobile/.env.mobile.example .env.mobile +``` +- Fill the .env, .env.web and .env.mobile files + +3. Install NPM packages ```sh cd AREA/client-web npm install diff --git a/client_mobile/.env.mobile.example b/client_mobile/.env.mobile.example new file mode 100644 index 0000000..965d1d9 --- /dev/null +++ b/client_mobile/.env.mobile.example @@ -0,0 +1,24 @@ +VITE_PORT=8081 +VITE_ENDPOINT=http://localhost:8080 + +VITE_GOOGLE_CLIENT_ID= +VITE_GOOGLE_CLIENT_SECRET= + +VITE_MICROSOFT_CLIENT_ID= + +VITE_LINKEDIN_CLIENT_ID= +VITE_LINKEDIN_CLIENT_SECRET= + +VITE_SPOTIFY_CLIENT_ID= +VITE_SPOTIFY_CLIENT_SECRET= + +# Server URLs +API_URL=http://localhost:8080 +WEB_CLIENT_URL=http://localhost:8081 +MOBILE_CLIENT_URL=http://localhost:8082 + +# OAuth credentials +GITHUB_CLIENT_ID=your_github_client_id +GITHUB_CLIENT_SECRET=your_github_client_secret + +# Add other environment variables as needed \ No newline at end of file diff --git a/client_mobile/.gitignore b/client_mobile/.gitignore index 29a3a50..5b8e2b0 100644 --- a/client_mobile/.gitignore +++ b/client_mobile/.gitignore @@ -41,3 +41,4 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release +.env.mobile \ No newline at end of file diff --git a/client_mobile/Dockerfile b/client_mobile/Dockerfile new file mode 100644 index 0000000..778bcff --- /dev/null +++ b/client_mobile/Dockerfile @@ -0,0 +1,26 @@ +FROM ghcr.io/cirruslabs/flutter:stable + +WORKDIR /app + +ARG VITE_PORT +ARG VITE_ENDPOINT +ARG VITE_GOOGLE_CLIENT_ID +ARG VITE_GOOGLE_CLIENT_SECRET +ARG VITE_MICROSOFT_CLIENT_ID +ARG VITE_LINKEDIN_CLIENT_ID +ARG VITE_LINKEDIN_CLIENT_SECRET +ARG VITE_SPOTIFY_CLIENT_ID +ARG VITE_SPOTIFY_CLIENT_SECRET +ARG API_URL +ARG WEB_CLIENT_URL +ARG MOBILE_CLIENT_URL +ARG GITHUB_CLIENT_ID +ARG GITHUB_CLIENT_SECRET + +COPY . . + +RUN flutter pub get +RUN flutter build apk --release + +RUN mv build/app/outputs/flutter-apk/app-release.apk build/app/outputs/flutter-apk/client.apk +RUN chmod -R 755 build/app/outputs/flutter-apk/ \ No newline at end of file diff --git a/client_mobile/pubspec.yaml b/client_mobile/pubspec.yaml index 0b264fd..a5640fa 100644 --- a/client_mobile/pubspec.yaml +++ b/client_mobile/pubspec.yaml @@ -69,7 +69,7 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - - .env + - .env.mobile - assets/images/microsoft_logo.png # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg diff --git a/client_web/.env.example b/client_web/.env.example deleted file mode 100644 index 4d4d03a..0000000 --- a/client_web/.env.example +++ /dev/null @@ -1,5 +0,0 @@ -VITE_PORT= -VITE_ENDPOINT= - -VITE_GOOGLE_CLIENT_ID= -VITE_GOOGLE_CLIENT_SECRET= diff --git a/client_web/.env.local.example b/client_web/.env.local.example new file mode 100644 index 0000000..965d1d9 --- /dev/null +++ b/client_web/.env.local.example @@ -0,0 +1,24 @@ +VITE_PORT=8081 +VITE_ENDPOINT=http://localhost:8080 + +VITE_GOOGLE_CLIENT_ID= +VITE_GOOGLE_CLIENT_SECRET= + +VITE_MICROSOFT_CLIENT_ID= + +VITE_LINKEDIN_CLIENT_ID= +VITE_LINKEDIN_CLIENT_SECRET= + +VITE_SPOTIFY_CLIENT_ID= +VITE_SPOTIFY_CLIENT_SECRET= + +# Server URLs +API_URL=http://localhost:8080 +WEB_CLIENT_URL=http://localhost:8081 +MOBILE_CLIENT_URL=http://localhost:8082 + +# OAuth credentials +GITHUB_CLIENT_ID=your_github_client_id +GITHUB_CLIENT_SECRET=your_github_client_secret + +# Add other environment variables as needed \ No newline at end of file diff --git a/client_web/.gitignore b/client_web/.gitignore index 6f522c2..0bb4cfe 100644 --- a/client_web/.gitignore +++ b/client_web/.gitignore @@ -27,3 +27,7 @@ dist-ssr *.sw? .vite/ + +# Environment files +.env +.env.local \ No newline at end of file diff --git a/client_web/Dockerfile b/client_web/Dockerfile index 9b07e3e..2d06b21 100644 --- a/client_web/Dockerfile +++ b/client_web/Dockerfile @@ -16,51 +16,39 @@ RUN curl -JLO "https://dl.filippo.io/mkcert/latest?for=linux/amd64" \ ###----------------------- Build stage for Node.js application -----------------------### -FROM node:18-alpine AS app-builder +FROM node:latest AS builder WORKDIR /app -# Copy certificates from cert-builder -COPY --from=cert-builder /localhost* ./ - -# Copy only package files first for better layer caching -COPY package*.json ./ +ARG VITE_PORT +ARG VITE_ENDPOINT +ARG VITE_GOOGLE_CLIENT_ID +ARG VITE_GOOGLE_CLIENT_SECRET +ARG VITE_MICROSOFT_CLIENT_ID +ARG VITE_LINKEDIN_CLIENT_ID +ARG VITE_LINKEDIN_CLIENT_SECRET +ARG VITE_SPOTIFY_CLIENT_ID +ARG VITE_SPOTIFY_CLIENT_SECRET +ARG API_URL +ARG WEB_CLIENT_URL +ARG MOBILE_CLIENT_URL +ARG GITHUB_CLIENT_ID +ARG GITHUB_CLIENT_SECRET + +COPY ./package*.json ./ RUN npm install - -# Copy source files and build COPY . . -RUN npm run build - - -###----------------------- Python tools stage -----------------------### -FROM alpine:3.19 AS tools-builder -# Install Python and tools -RUN apk add --no-cache \ - python3 \ - py3-virtualenv \ - py3-pip - -# Setup Python environment -RUN python3 -m venv /opt/venv -ENV PATH="/opt/venv/bin:$PATH" -RUN pip3 install --no-cache-dir sphinx sphinx_rtd_theme +RUN npm run build ###----------------------- Production stage -----------------------### -FROM nginx:alpine +FROM nginx:alpine AS production -# Create directory for mobile builds +COPY --from=builder /app/dist /usr/share/nginx/html +COPY ./nginx.conf /etc/nginx/conf.d/default.conf RUN mkdir -p /usr/share/nginx/html/mobile_builds -# Copy production assets -COPY --from=app-builder /app/dist /usr/share/nginx/html -COPY --from=cert-builder /localhost* /etc/nginx/certs/ -COPY nginx.conf /etc/nginx/conf.d/default.conf - -# Create volume mount point for shared mobile builds -VOLUME /usr/share/nginx/html/mobile_builds - -EXPOSE 8081 +EXPOSE ${VITE_PORT} -CMD ["nginx", "-g", "daemon off;"] +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/client_web/nginx.conf b/client_web/nginx.conf index 9559b2a..036ee9a 100644 --- a/client_web/nginx.conf +++ b/client_web/nginx.conf @@ -4,11 +4,14 @@ server { location / { root /usr/share/nginx/html; + index index.html index.htm; try_files $uri $uri/ /index.html; } # Serve mobile client binary location /client.apk { alias /usr/share/nginx/html/mobile_builds/client.apk; + add_header Content-Type application/vnd.android.package-archive; + add_header Content-Disposition attachment; } } \ No newline at end of file diff --git a/client_web/package-lock.json b/client_web/package-lock.json index 374542b..3b8bfca 100644 --- a/client_web/package-lock.json +++ b/client_web/package-lock.json @@ -10,6 +10,9 @@ "dependencies": { "@azure/msal-browser": "^3.27.0", "@react-oauth/google": "^0.12.1", + "@tsparticles/engine": "^3.7.1", + "@tsparticles/react": "^3.0.0", + "@tsparticles/slim": "^3.7.1", "@types/js-cookie": "^3.0.6", "antd": "^5.22.1", "axios": "^1.7.7", @@ -17,8 +20,10 @@ "framer-motion": "^11.11.17", "js-cookie": "^3.0.5", "react": "^18.3.1", + "react-color": "^2.19.3", "react-dom": "^18.3.1", - "react-router-dom": "^6.28.0" + "react-router-dom": "^6.28.0", + "react-toastify": "^10.0.6" }, "devDependencies": { "@eslint/js": "^9.13.0", @@ -1099,6 +1104,15 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@icons/material": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz", + "integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -1636,6 +1650,470 @@ "node": ">=10" } }, + "node_modules/@tsparticles/basic": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/basic/-/basic-3.7.1.tgz", + "integrity": "sha512-oJMJ3qzYUROYaOEsaFXkVynxT2OTWBXbQ9MNc1bJi/bVc1VOU44VN7X/KmiZjD+w1U+Qalk6BeVvDRwpFshblw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/matteobruni" + }, + { + "type": "github", + "url": "https://github.com/sponsors/tsparticles" + }, + { + "type": "buymeacoffee", + "url": "https://www.buymeacoffee.com/matteobruni" + } + ], + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1", + "@tsparticles/move-base": "3.7.1", + "@tsparticles/plugin-hex-color": "3.7.1", + "@tsparticles/plugin-hsl-color": "3.7.1", + "@tsparticles/plugin-rgb-color": "3.7.1", + "@tsparticles/shape-circle": "3.7.1", + "@tsparticles/updater-color": "3.7.1", + "@tsparticles/updater-opacity": "3.7.1", + "@tsparticles/updater-out-modes": "3.7.1", + "@tsparticles/updater-size": "3.7.1" + } + }, + "node_modules/@tsparticles/engine": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/engine/-/engine-3.7.1.tgz", + "integrity": "sha512-GYzBgq/oOE9YJdOL1++MoawWmYg4AvVct6CIrJGx84ZRb3U2owYmLsRGabYl0qX1CWWOvUG569043RJmyp/vQA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/matteobruni" + }, + { + "type": "github", + "url": "https://github.com/sponsors/tsparticles" + }, + { + "type": "buymeacoffee", + "url": "https://www.buymeacoffee.com/matteobruni" + } + ], + "hasInstallScript": true, + "license": "MIT" + }, + "node_modules/@tsparticles/interaction-external-attract": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-attract/-/interaction-external-attract-3.7.1.tgz", + "integrity": "sha512-cpnMsFJ7ZJNKccpQvskKvSs1ofknByHE6FGqbEb17ij7HqvbECQOCOVKHPFnYipHe14cXor/Cd+nVisRcTASoQ==", + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1" + } + }, + "node_modules/@tsparticles/interaction-external-bounce": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-bounce/-/interaction-external-bounce-3.7.1.tgz", + "integrity": "sha512-npvU9Qt6WDonjezHqi+hWM44ga2Oh5yXdr8eSoJpvuHZrCP7rIdRSz5XseHouO1bMS9DbXk86sx4qwrhB5w58w==", + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1" + } + }, + "node_modules/@tsparticles/interaction-external-bubble": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-bubble/-/interaction-external-bubble-3.7.1.tgz", + "integrity": "sha512-WdbYL46lMfuf2g5kfVB1hhhxRBtEXDvnwz8PJwLKurSThL/27bqsqysyXsMzXtLByXUneGhtJTj4D5I5RYdgjA==", + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1" + } + }, + "node_modules/@tsparticles/interaction-external-connect": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-connect/-/interaction-external-connect-3.7.1.tgz", + "integrity": "sha512-hqx0ANIbjLIz/nxmk0LvqANBiNLLmVybbCA7N+xDHtEORvpKjNlKEvMz6Razocl6vRjoHZ/olSwcxIG84dh/cg==", + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1" + } + }, + "node_modules/@tsparticles/interaction-external-grab": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-grab/-/interaction-external-grab-3.7.1.tgz", + "integrity": "sha512-JMYpFW+7YvkpK5MYlt4Ec3Gwb5ZxS7RLVL8IRUSd5yJOw25husPTYg+FQywxrt5WhKe+tPsCAYo+uGIbTTHi9w==", + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1" + } + }, + "node_modules/@tsparticles/interaction-external-pause": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-pause/-/interaction-external-pause-3.7.1.tgz", + "integrity": "sha512-Kkp+7sCe24hawH0XvS1V6UCCuHfMvpLK7oseqSam9Gt4SyGrFvaqIXxkjXhRhn9MysJyKFPBV4/dtBM1HR9p6A==", + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1" + } + }, + "node_modules/@tsparticles/interaction-external-push": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-push/-/interaction-external-push-3.7.1.tgz", + "integrity": "sha512-4VoaR5jvXgQdB7irtq4uSZYr5c+D6TBTVEnLVpBfJhUs6jhw6mgN5g7yp5izIYkK0AlcO431MHn8dvJacvRLDw==", + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1" + } + }, + "node_modules/@tsparticles/interaction-external-remove": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-remove/-/interaction-external-remove-3.7.1.tgz", + "integrity": "sha512-FRBW7U7zD5MkO6/b7e8iSMk/UTtRLY2XiIVFZNsKri3Re3yPpvZzzd5tl2YlYGQlg1Xc+K8SJYMQQA3PtgQ/Tg==", + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1" + } + }, + "node_modules/@tsparticles/interaction-external-repulse": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-repulse/-/interaction-external-repulse-3.7.1.tgz", + "integrity": "sha512-mwM06dVmg2FEvHMQsPOfRBQWACbjf3qnelODkqI9DSVxQ0B8DESP4BYNXyraFGYv00YiPzRv5Xy/uejHdbsQUA==", + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1" + } + }, + "node_modules/@tsparticles/interaction-external-slow": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-slow/-/interaction-external-slow-3.7.1.tgz", + "integrity": "sha512-CfCAs3kUQC3pLOj0dbzn5AolQyBHgjxORLdfnYBhepvFV1BXB+4ytChRfXBzjykBPI6U+rCnw5Fk/vVjAroSFA==", + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1" + } + }, + "node_modules/@tsparticles/interaction-particles-attract": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/interaction-particles-attract/-/interaction-particles-attract-3.7.1.tgz", + "integrity": "sha512-UABbBORKaiItAT8vR0t4ye2H3VE6/Ah4zcylBlnq0Jd5yDkyP4rnkwhicaY6y4Zlfwyx+0PWdAC4f/ziFAyObg==", + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1" + } + }, + "node_modules/@tsparticles/interaction-particles-collisions": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/interaction-particles-collisions/-/interaction-particles-collisions-3.7.1.tgz", + "integrity": "sha512-0GY9++Gn2KXViyeifXWkH7a2UO5+uRwyS1rDeTN8eleyiq2j9zQf4xztZEIft8T0hTetq2rkWxQ92j2kev6NVA==", + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1" + } + }, + "node_modules/@tsparticles/interaction-particles-links": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/interaction-particles-links/-/interaction-particles-links-3.7.1.tgz", + "integrity": "sha512-BxCXAAOBNmEvlyOQzwprryW8YdtMIop2v4kgSCff5MCtDwYWoQIfzaQlWbBAkD9ey6BoF8iMjhBUaY1MnDecTA==", + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1" + } + }, + "node_modules/@tsparticles/move-base": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/move-base/-/move-base-3.7.1.tgz", + "integrity": "sha512-LPtMHwJHhzwfRIcSAk814fY9NcRiICwaEbapaJSYyP1DwscSXqOWoyAEWwzV9hMgAcPdsED6nGeg8RCXGm58lw==", + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1" + } + }, + "node_modules/@tsparticles/move-parallax": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/move-parallax/-/move-parallax-3.7.1.tgz", + "integrity": "sha512-B40azo6EJyMdI+kmIxpqWDaObPwODTYLDCikzkZ73n5tS6OhFUlkz81Scfo+g1iGTdryKFygUKhVGcG1EFuA5g==", + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1" + } + }, + "node_modules/@tsparticles/plugin-easing-quad": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/plugin-easing-quad/-/plugin-easing-quad-3.7.1.tgz", + "integrity": "sha512-nSwKCRe6C/noCi3dyZlm1GiQGask0aXdWDuS36b82iwzwQ01cBTXeXR25mLr4fsfMLFfYAZXyBxEMMpw3rkSiw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/matteobruni" + }, + { + "type": "github", + "url": "https://github.com/sponsors/tsparticles" + }, + { + "type": "buymeacoffee", + "url": "https://www.buymeacoffee.com/matteobruni" + } + ], + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1" + } + }, + "node_modules/@tsparticles/plugin-hex-color": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/plugin-hex-color/-/plugin-hex-color-3.7.1.tgz", + "integrity": "sha512-7xu3MV8EdNNejjYyEmrq5fCDdYAcqz/9VatLpnwtwR5Q5t2qI0tD4CrcGaFfC/rTAVJacfiJe02UV/hlj03KKA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/matteobruni" + }, + { + "type": "github", + "url": "https://github.com/sponsors/tsparticles" + }, + { + "type": "buymeacoffee", + "url": "https://www.buymeacoffee.com/matteobruni" + } + ], + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1" + } + }, + "node_modules/@tsparticles/plugin-hsl-color": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/plugin-hsl-color/-/plugin-hsl-color-3.7.1.tgz", + "integrity": "sha512-zzAI1CuoCMBJhgeYZ5Rq42nGbPg35ZzIs11eQegjsWG5Msm5QKSj60qPzERnoUcCc4HCKtIWP7rYMz6h3xpoEg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/matteobruni" + }, + { + "type": "github", + "url": "https://github.com/sponsors/tsparticles" + }, + { + "type": "buymeacoffee", + "url": "https://www.buymeacoffee.com/matteobruni" + } + ], + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1" + } + }, + "node_modules/@tsparticles/plugin-rgb-color": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/plugin-rgb-color/-/plugin-rgb-color-3.7.1.tgz", + "integrity": "sha512-taEraTpCYR6jpjflqBL95tN0zFU8JrAChXTt8mxVn7gddxoNMHI/LGymEPRCweLukwV6GQyAGOkeGEdWDPtYTA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/matteobruni" + }, + { + "type": "github", + "url": "https://github.com/sponsors/tsparticles" + }, + { + "type": "buymeacoffee", + "url": "https://www.buymeacoffee.com/matteobruni" + } + ], + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1" + } + }, + "node_modules/@tsparticles/react": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@tsparticles/react/-/react-3.0.0.tgz", + "integrity": "sha512-hjGEtTT1cwv6BcjL+GcVgH++KYs52bIuQGW3PWv7z3tMa8g0bd6RI/vWSLj7p//NZ3uTjEIeilYIUPBh7Jfq/Q==", + "peerDependencies": { + "@tsparticles/engine": "^3.0.2", + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@tsparticles/shape-circle": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/shape-circle/-/shape-circle-3.7.1.tgz", + "integrity": "sha512-kmOWaUuFwuTtcCFYjuyJbdA5qDqWdGsharLalYnIczkLu2c1I8jJo/OmGePKhWn62ocu7mqKMomfElkIcw2AsA==", + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1" + } + }, + "node_modules/@tsparticles/shape-emoji": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/shape-emoji/-/shape-emoji-3.7.1.tgz", + "integrity": "sha512-mX18c/xhYVljS/r5Xbowzclw+1YwhtWoQFOOfkmjjZppA+RjgcwSKLvH6E20PaH1yVTjBOfSF+3POKpwsULzTg==", + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1" + } + }, + "node_modules/@tsparticles/shape-image": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/shape-image/-/shape-image-3.7.1.tgz", + "integrity": "sha512-eDzfkQhqLY6fb9QH2Vo9TGfdJBFFpYnojhxQxc7IdzIwOFMD3JK4B52RVl9oowR+rNE8dNp6P2L+eMAF4yld0g==", + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1" + } + }, + "node_modules/@tsparticles/shape-line": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/shape-line/-/shape-line-3.7.1.tgz", + "integrity": "sha512-lMPYApUpg7avxmYPfHHr4dQepZSNn/g0Q1/g2+lnTi8ZtUBiCZ2WMVy9R3GOzyozbnzigLQ6AJRnOpsUZV7H4g==", + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1" + } + }, + "node_modules/@tsparticles/shape-polygon": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/shape-polygon/-/shape-polygon-3.7.1.tgz", + "integrity": "sha512-5FrRfpYC3qnvV2nXBLE4Q0v+SMNWJO8xgzh6MBFwfptvqH4EOrqc/58eS5x0jlf+evwf9LjPgeGkOTcwaHHcYQ==", + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1" + } + }, + "node_modules/@tsparticles/shape-square": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/shape-square/-/shape-square-3.7.1.tgz", + "integrity": "sha512-7VCqbRwinjBZ+Ryme27rOtl+jKrET8qDthqZLrAoj3WONBqyt+R9q6SXAJ9WodqEX68IBvcluqbFY5qDZm8iAQ==", + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1" + } + }, + "node_modules/@tsparticles/shape-star": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/shape-star/-/shape-star-3.7.1.tgz", + "integrity": "sha512-3G4oipioyWKLEQYT11Sx3k6AObu3dbv/A5LRqGGTQm5IR6UACa+INwykZYI0a+MdJJMb83E0e4Fn3hlZbi0/8w==", + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1" + } + }, + "node_modules/@tsparticles/slim": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/slim/-/slim-3.7.1.tgz", + "integrity": "sha512-OtJEhud2KleX7OxiG2r/VYriHNIwTpFm3sPFy4EOJzAD0EW7KZoKXGpGn5gwGI1NWeB0jso92yNTrTC2ZTW0qw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/matteobruni" + }, + { + "type": "github", + "url": "https://github.com/sponsors/tsparticles" + }, + { + "type": "buymeacoffee", + "url": "https://www.buymeacoffee.com/matteobruni" + } + ], + "license": "MIT", + "dependencies": { + "@tsparticles/basic": "3.7.1", + "@tsparticles/engine": "3.7.1", + "@tsparticles/interaction-external-attract": "3.7.1", + "@tsparticles/interaction-external-bounce": "3.7.1", + "@tsparticles/interaction-external-bubble": "3.7.1", + "@tsparticles/interaction-external-connect": "3.7.1", + "@tsparticles/interaction-external-grab": "3.7.1", + "@tsparticles/interaction-external-pause": "3.7.1", + "@tsparticles/interaction-external-push": "3.7.1", + "@tsparticles/interaction-external-remove": "3.7.1", + "@tsparticles/interaction-external-repulse": "3.7.1", + "@tsparticles/interaction-external-slow": "3.7.1", + "@tsparticles/interaction-particles-attract": "3.7.1", + "@tsparticles/interaction-particles-collisions": "3.7.1", + "@tsparticles/interaction-particles-links": "3.7.1", + "@tsparticles/move-parallax": "3.7.1", + "@tsparticles/plugin-easing-quad": "3.7.1", + "@tsparticles/shape-emoji": "3.7.1", + "@tsparticles/shape-image": "3.7.1", + "@tsparticles/shape-line": "3.7.1", + "@tsparticles/shape-polygon": "3.7.1", + "@tsparticles/shape-square": "3.7.1", + "@tsparticles/shape-star": "3.7.1", + "@tsparticles/updater-life": "3.7.1", + "@tsparticles/updater-rotate": "3.7.1", + "@tsparticles/updater-stroke-color": "3.7.1" + } + }, + "node_modules/@tsparticles/updater-color": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/updater-color/-/updater-color-3.7.1.tgz", + "integrity": "sha512-QimV3yn17dcdJx7PpTwLtw9BhkQ0q8qFF035OdcZpnynBPAO/hg0zvSMpMGoeuDVFH02wWBy4h2/BYCv6wh6Sw==", + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1" + } + }, + "node_modules/@tsparticles/updater-life": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/updater-life/-/updater-life-3.7.1.tgz", + "integrity": "sha512-NY5gUrgO5AsARNC0usP9PKahXf7JCxbP/H1vzTfA0SJw4veANfWTldOvhIlcm2CHVP5P1b827p0hWsBHECwz7A==", + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1" + } + }, + "node_modules/@tsparticles/updater-opacity": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/updater-opacity/-/updater-opacity-3.7.1.tgz", + "integrity": "sha512-YcyviCooTv7SAKw7sxd84CfJqZ7dYPSdYZzCpedV6TKIObRiwLqXlyLXQGJ3YltghKQSCSolmVy8woWBCDm1qA==", + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1" + } + }, + "node_modules/@tsparticles/updater-out-modes": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/updater-out-modes/-/updater-out-modes-3.7.1.tgz", + "integrity": "sha512-Cb5sWquRtUYLSiFpmBjjYKRdpNV52diCo9+qMtK1oVlldDBhUwqO+1TQjdlaA2yl5DURlY9ZfOHXvY+IT7CHCw==", + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1" + } + }, + "node_modules/@tsparticles/updater-rotate": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/updater-rotate/-/updater-rotate-3.7.1.tgz", + "integrity": "sha512-toVHwl+h6SvtA8dyxSA2kMH2QdDA71vehuAa+HoRqf1y06h5kxyYiMKZFHCqDJ6lFfRPs47MjrC9dD2bDz14MQ==", + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1" + } + }, + "node_modules/@tsparticles/updater-size": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/updater-size/-/updater-size-3.7.1.tgz", + "integrity": "sha512-+Y0H0PnDJVIsJ+zHTyubYu1jtRFmVnY1dAv3VCjScIDw6bcpL/ol+HrtHTGIX0WbMyUfjCyALfAoaXi/Wm8VcQ==", + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1" + } + }, + "node_modules/@tsparticles/updater-stroke-color": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@tsparticles/updater-stroke-color/-/updater-stroke-color-3.7.1.tgz", + "integrity": "sha512-VHhQkCNuxjx/Hy7A+g0Yijb24T0+wQ3jNsF/yfrR9dEdZWSBiimZLvV1bilPdAeEtieAJTAZo2VNhcD1snF0iQ==", + "license": "MIT", + "dependencies": { + "@tsparticles/engine": "3.7.1" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2376,6 +2854,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3664,6 +4151,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -3717,6 +4216,12 @@ "node": ">=10" } }, + "node_modules/material-colors": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", + "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==", + "license": "ISC" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3838,6 +4343,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -4028,6 +4542,23 @@ "node": ">=0.4.0" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -4706,6 +5237,24 @@ "node": ">=0.10.0" } }, + "node_modules/react-color": { + "version": "2.19.3", + "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz", + "integrity": "sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==", + "license": "MIT", + "dependencies": { + "@icons/material": "^0.2.4", + "lodash": "^4.17.15", + "lodash-es": "^4.17.15", + "material-colors": "^1.2.1", + "prop-types": "^15.5.10", + "reactcss": "^1.2.0", + "tinycolor2": "^1.4.1" + }, + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -4767,6 +5316,28 @@ "react-dom": ">=16.8" } }, + "node_modules/react-toastify": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.6.tgz", + "integrity": "sha512-yYjp+omCDf9lhZcrZHKbSq7YMuK0zcYkDFTzfRFgTXkTFHZ1ToxwAonzA4JI5CxA91JpjFLmwEsZEgfYfOqI1A==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/reactcss": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", + "integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==", + "license": "MIT", + "dependencies": { + "lodash": "^4.0.1" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", @@ -5062,6 +5633,12 @@ "node": ">=12.22" } }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/client_web/package.json b/client_web/package.json index 80fa366..8013ac0 100644 --- a/client_web/package.json +++ b/client_web/package.json @@ -16,6 +16,9 @@ "dependencies": { "@azure/msal-browser": "^3.27.0", "@react-oauth/google": "^0.12.1", + "@tsparticles/engine": "^3.7.1", + "@tsparticles/react": "^3.0.0", + "@tsparticles/slim": "^3.7.1", "@types/js-cookie": "^3.0.6", "antd": "^5.22.1", "axios": "^1.7.7", @@ -23,8 +26,10 @@ "framer-motion": "^11.11.17", "js-cookie": "^3.0.5", "react": "^18.3.1", + "react-color": "^2.19.3", "react-dom": "^18.3.1", - "react-router-dom": "^6.28.0" + "react-router-dom": "^6.28.0", + "react-toastify": "^10.0.6" }, "devDependencies": { "@eslint/js": "^9.13.0", diff --git a/client_web/public/discord-icon.png b/client_web/public/discord-icon.png new file mode 100644 index 0000000..45547f7 Binary files /dev/null and b/client_web/public/discord-icon.png differ diff --git a/client_web/public/google-icon.png b/client_web/public/google-icon.png new file mode 100644 index 0000000..494aced Binary files /dev/null and b/client_web/public/google-icon.png differ diff --git a/client_web/src/App.tsx b/client_web/src/App.tsx index f8466f3..8a0b4d9 100644 --- a/client_web/src/App.tsx +++ b/client_web/src/App.tsx @@ -1,36 +1,173 @@ import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; -import { ContextManager } from "./Context/ContextManager.tsx"; +import { ContextManager } from "./Context/ContextManager"; +// @ts-ignore +import { uri } from '@Config/uri'; -import Home from './pages/Home'; -import NotFound from './pages/NotFound'; -import Layout from './Layout'; -import Login from './pages/Auth/Login.tsx'; -import Register from './pages/Auth/Register.tsx'; +import { useEffect, useMemo, useState } from "react"; +import Particles, { initParticlesEngine } from "@tsparticles/react"; +import { + type Container, + type ISourceOptions, + MoveDirection, + OutMode, +} from "@tsparticles/engine"; +import { loadSlim } from "@tsparticles/slim"; -import LinkedinCallback from "./pages/Auth/LinkedinCallback.tsx"; -import SpotifyCallback from './pages/Auth/SpotifyCallback.tsx'; -import MicrosoftCallback from './pages/Auth/MicrosoftCallback.tsx'; +import Home from './Pages/Home'; +import NotFound from './Pages/Errors/NotFound'; +import ApiNotConnected from "@/Pages/Errors/ApiNotConnected"; +// @ts-ignore +import Layout from '@/Components/Layout/Layout'; +import Login from './Pages/Auth/Forms/Login'; +import Register from './Pages/Auth/Forms/Register'; -import Dashboard from './pages/Dashboard/Dashboard.tsx'; +import LinkedinCallback from "./Pages/Auth/Callback/LinkedinCallback"; +import SpotifyCallback from './Pages/Auth/Callback/SpotifyCallback'; +import MicrosoftCallback from './Pages/Auth/Callback/MicrosoftCallback'; +import DiscordCallback from './Pages/Auth/Callback/DiscordCallback'; + +import CreateWorkflow from "./Pages/Workflows/CreateWorkflow"; +import WorkflowsTable from "./Pages/Workflows/WorkflowsTable"; + +import Dashboard from './Pages/Dashboard/Dashboard'; + +import { ToastContainer } from 'react-toastify'; +// @ts-ignore +import 'react-toastify/dist/ReactToastify.css'; const App = () => { + const [init, setInit] = useState(false); + const [backgroundColor, setBackgroundColor] = useState("#FFA500"); + + useEffect(() => { + initParticlesEngine(async (engine) => { + await loadSlim(engine); + }).then(() => { + setInit(true); + }); + setBackgroundColor(sessionStorage.getItem("backgroundColor") || "#FFA500"); + }, []); + + const particlesLoaded = async (container?: Container): Promise => {}; + + const options: ISourceOptions = useMemo( + () => ({ + background: { + color: { + value: backgroundColor, + }, + }, + fpsLimit: 60, + interactivity: { + events: { + onClick: { + enable: true, + mode: "push", + }, + onHover: { + enable: true, + mode: "repulse", + }, + }, + modes: { + push: { + quantity: 1, + }, + repulse: { + distance: 200, + duration: 0.4, + }, + }, + }, + particles: { + color: { + value: "#ffffff", + }, + links: { + color: "#ffffff", + distance: 150, + enable: true, + opacity: 0.5, + width: 1, + }, + move: { + direction: MoveDirection.none, + enable: true, + outModes: { + default: OutMode.out, + }, + random: false, + speed: 6, + straight: false, + }, + number: { + density: { + enable: true, + }, + value: 80, + }, + opacity: { + value: 0.5, + }, + shape: { + type: "circle", + }, + size: { + value: { min: 1, max: 5 }, + }, + }, + detectRetina: true, + }), + [backgroundColor], + ); + return ( - - - - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - - + <> + + {init && ( + + )} + + + + + } /> + } /> + } /> + } /> + + } /> + } /> + } /> + } /> + + } /> + } /> + } /> + + } /> + } /> + + + + + ); }; diff --git a/client_web/src/Components/Auth/Buttons/DiscordAuth.tsx b/client_web/src/Components/Auth/Buttons/DiscordAuth.tsx new file mode 100644 index 0000000..9d90b85 --- /dev/null +++ b/client_web/src/Components/Auth/Buttons/DiscordAuth.tsx @@ -0,0 +1,56 @@ +import { Form, Button } from 'antd'; +import { FC } from 'react'; +// @ts-ignore +import { uri } from '@Config/uri'; + +interface DiscordAuthProps { + buttonText: string; +} + +const DiscordAuth: FC = ({ buttonText }) => { + const scope = [ + 'identify', + 'email', + 'guilds', + // TODO: Add other scopes as needed + ].join(' '); + + const handleDiscordLogin = () => { + const state = crypto.randomUUID().substring(0, 16); + localStorage.setItem('discord_auth_state', state); + + const authUrl = new URL('https://discord.com/api/oauth2/authorize'); + const params = { + response_type: 'code', + client_id: uri.discord.auth.clientId, + scope: scope, + redirect_uri: uri.discord.auth.redirectUri, + state: state, + }; + + authUrl.search = new URLSearchParams(params).toString(); + window.location.href = authUrl.toString(); + }; + + if (!uri.discord.auth.clientId) { + return null; + } + + return ( + + + + ); +}; + +export default DiscordAuth; diff --git a/client_web/src/Components/Auth/Buttons/GoogleAuth.tsx b/client_web/src/Components/Auth/Buttons/GoogleAuth.tsx new file mode 100644 index 0000000..ae8b42e --- /dev/null +++ b/client_web/src/Components/Auth/Buttons/GoogleAuth.tsx @@ -0,0 +1,54 @@ +import { Form, Button } from 'antd'; +import { GoogleOAuthProvider } from '@react-oauth/google'; +// @ts-ignore +import { uri } from '@Config/uri'; + +interface GoogleAuthProps { + onSuccess: (response: unknown) => void; + onError: () => void; + buttonText: string; +} + +const GoogleAuth = ({ onSuccess, onError, buttonText }: GoogleAuthProps) => { + if (!uri.google.auth.clientId || uri.google.auth.clientId === '') { + return null; + } + + const handleGoogleLogin = () => { + // Initialize the Google Sign-In client + let google: any; + const client = google.accounts.oauth2.initCodeClient({ + client_id: uri.google.auth.clientId, + scope: 'email profile', // TODO: Add other scopes as needed + callback: (response: unknown) => { + // @ts-ignore + if (response?.code) { + onSuccess(response); + } else { + onError(); + } + }, + }); + client.requestCode(); + }; + + return ( + + + + + + ); +}; + +export default GoogleAuth; diff --git a/client_web/src/components/auth/LinkedinAuth.tsx b/client_web/src/Components/Auth/Buttons/LinkedinAuth.tsx similarity index 77% rename from client_web/src/components/auth/LinkedinAuth.tsx rename to client_web/src/Components/Auth/Buttons/LinkedinAuth.tsx index ae455f3..2b68136 100644 --- a/client_web/src/components/auth/LinkedinAuth.tsx +++ b/client_web/src/Components/Auth/Buttons/LinkedinAuth.tsx @@ -1,4 +1,6 @@ import {Button, Form} from 'antd'; +// @ts-ignore +import { uri } from '@Config/uri'; interface LinkedinAuthProps { onSuccess: (response: unknown) => void; @@ -7,11 +9,6 @@ interface LinkedinAuthProps { } const LinkedinAuth = ({ buttonText }: LinkedinAuthProps) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - const clientId = import.meta.env.VITE_LINKEDIN_CLIENT_ID; - const redirectUri = `${window.location.origin}/auth/linkedin/callback`; - const handleLinkedinAuth = () => { const scope = 'r_liteprofile r_emailaddress'; const state = Math.random().toString(36).substring(7); @@ -20,13 +17,13 @@ const LinkedinAuth = ({ buttonText }: LinkedinAuthProps) => { window.location.href = `https://www.linkedin.com/oauth/v2/authorization?` + `response_type=code&` + - `client_id=${clientId}&` + - `redirect_uri=${encodeURIComponent(redirectUri)}&` + + `client_id=${uri.linkedin.auth.clientId}&` + + `redirect_uri=${encodeURIComponent(uri.linkedin.auth.redirectUri)}&` + `state=${state}&` + `scope=${encodeURIComponent(scope)}`; }; - if (!clientId) { + if (!uri.linkedin.auth.clientId) { return null; } diff --git a/client_web/src/components/auth/MicrosoftAuth.tsx b/client_web/src/Components/Auth/Buttons/MicrosoftAuth.tsx similarity index 97% rename from client_web/src/components/auth/MicrosoftAuth.tsx rename to client_web/src/Components/Auth/Buttons/MicrosoftAuth.tsx index a95c810..64adbc7 100644 --- a/client_web/src/components/auth/MicrosoftAuth.tsx +++ b/client_web/src/Components/Auth/Buttons/MicrosoftAuth.tsx @@ -1,7 +1,8 @@ import { Form, Button } from 'antd'; import { PublicClientApplication } from '@azure/msal-browser'; import { useEffect, useState } from 'react'; -import { uri } from '../../uri.ts'; +// @ts-ignore +import { uri } from '@Config/uri'; interface MicrosoftAuthProps { onSuccess: (response: unknown) => void; diff --git a/client_web/src/components/auth/SpotifyAuth.tsx b/client_web/src/Components/Auth/Buttons/SpotifyAuth.tsx similarity index 92% rename from client_web/src/components/auth/SpotifyAuth.tsx rename to client_web/src/Components/Auth/Buttons/SpotifyAuth.tsx index 714916c..b1132ef 100644 --- a/client_web/src/components/auth/SpotifyAuth.tsx +++ b/client_web/src/Components/Auth/Buttons/SpotifyAuth.tsx @@ -1,6 +1,7 @@ import { Form, Button } from 'antd'; import { FC } from 'react'; -import { uri } from '../../uri.ts'; +// @ts-ignore +import { uri } from '@Config/uri'; interface SpotifyOAuthProps { buttonText: string; @@ -18,7 +19,7 @@ const SpotifyAuth: FC = ({ buttonText }) => { const state = crypto.randomUUID().substring(0, 16); localStorage.setItem('spotify_auth_state', state); - console.log("Set state: ", state); + console.log("Set state: ", state); // TODO: Remove after api connection const authUrl = new URL('https://accounts.spotify.com/authorize'); const params = { diff --git a/client_web/src/Components/Auth/OAuthButtons.tsx b/client_web/src/Components/Auth/OAuthButtons.tsx new file mode 100644 index 0000000..b6379e4 --- /dev/null +++ b/client_web/src/Components/Auth/OAuthButtons.tsx @@ -0,0 +1,60 @@ +import GoogleAuth from './Buttons/GoogleAuth'; +import MicrosoftAuth from './Buttons/MicrosoftAuth'; +import LinkedinAuth from './Buttons/LinkedinAuth'; +import SpotifyAuth from './Buttons/SpotifyAuth'; +import DiscordAuth from './Buttons/DiscordAuth'; +import { Divider } from 'antd'; + +interface OAuthButtonsProps { + mode: 'signin' | 'signup'; + onGoogleSuccess: (response: unknown) => void; + onGoogleError: () => void; + onMicrosoftSuccess: (response: unknown) => void; + onMicrosoftError: (error: unknown) => void; + onLinkedinSuccess: (response: unknown) => void; + onLinkedinError: (error: unknown) => void; +} + +const OAuthButtons = ({ + mode, + onGoogleSuccess, + onGoogleError, + onMicrosoftSuccess, + onMicrosoftError, + onLinkedinSuccess, + onLinkedinError +}: OAuthButtonsProps) => { + return ( + <> + Or + + + + + + + + + + + + ); +}; + +export default OAuthButtons; diff --git a/client_web/src/Components/Layout/Footer.tsx b/client_web/src/Components/Layout/Footer.tsx new file mode 100644 index 0000000..dbb0508 --- /dev/null +++ b/client_web/src/Components/Layout/Footer.tsx @@ -0,0 +1,28 @@ +import { Layout } from 'antd'; +import { useTheme } from '@/Context/ContextHooks'; +import React from "react"; + +const { Footer } = Layout; + +const AppFooter: React.FC = () => { + const { theme } = useTheme(); + + return ( +
+
+ ©2024 ASM. All Rights Reserved. +
+
+ ); +}; + +export default AppFooter; diff --git a/client_web/src/Components/Layout/Header.tsx b/client_web/src/Components/Layout/Header.tsx new file mode 100644 index 0000000..ec3c7f1 --- /dev/null +++ b/client_web/src/Components/Layout/Header.tsx @@ -0,0 +1,42 @@ +import { Layout, Menu } from 'antd'; +import { Link, useLocation } from 'react-router-dom'; +import { useTheme } from '@/Context/ContextHooks'; +import React, { useEffect, useState } from "react"; + +const { Header: AntHeader } = Layout; + +interface MenuItems { + key: string; + label: React.ReactNode; +} + +const menuItems: MenuItems[] = [ + { key: '/', label: Home }, + { key: '/dashboard', label: Dashboard }, +]; + +const Header: React.FC = () => { + const { theme } = useTheme(); + const location = useLocation(); + const [selectedKey, setSelectedKey] = useState(location.pathname); + + useEffect(() => { + setSelectedKey(location.pathname); + }, [location.pathname]); + + return ( +
+ + + +
+ ); +}; + +export default Header; diff --git a/client_web/src/Layout.tsx b/client_web/src/Components/Layout/Layout.tsx similarity index 56% rename from client_web/src/Layout.tsx rename to client_web/src/Components/Layout/Layout.tsx index 1cc78ff..f5d1934 100644 --- a/client_web/src/Layout.tsx +++ b/client_web/src/Components/Layout/Layout.tsx @@ -1,6 +1,6 @@ import { useLocation } from 'react-router-dom'; -import Header from './pages/Header'; -import AppFooter from './pages/Footer'; +import Header from './Header'; +import AppFooter from './Footer'; import React from "react"; interface LayoutProps { @@ -9,24 +9,24 @@ interface LayoutProps { const Layout: React.FC = ({ children }) => { const location = useLocation(); - const headerExcludedPaths: (string | RegExp)[] = [/.*/, '/', '/login', '/register', /^\/auth\/.*\/callback$/]; - const footerExcludedPaths: (string | RegExp)[] = [/.*/, '/login', '/register', /^\/auth\/.*\/callback$/]; + const headerIncludedPaths: (string | RegExp)[] = ['/', '/dashboard', '/workflow/create', '/workflows']; + const footerIncludedPaths: (string | RegExp)[] = ['/', '/dashboard']; - const isHeaderExcluded = headerExcludedPaths.some(path => + const isHeaderIncluded = headerIncludedPaths.some(path => typeof path === 'string' ? path === location.pathname : path.test(location.pathname) ); - const isFooterExcluded = footerExcludedPaths.some(path => + const isFooterIncluded = footerIncludedPaths.some(path => typeof path === 'string' ? path === location.pathname : path.test(location.pathname) ); return (
- {!isHeaderExcluded &&
} + {isHeaderIncluded &&
}
{children}
- {!isFooterExcluded && } + {isFooterIncluded && }
); }; diff --git a/client_web/src/Components/LinkButton.tsx b/client_web/src/Components/LinkButton.tsx new file mode 100644 index 0000000..5408f50 --- /dev/null +++ b/client_web/src/Components/LinkButton.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import { Button } from 'antd'; +import { useNavigate } from 'react-router-dom'; + +interface LinkButtonProps { + text: string; + url?: string; + style?: React.CSSProperties; + type?: "primary" | "default" | "dashed" | "link" | "text" | "danger" | undefined; + goBack?: boolean; + disabled?: boolean; +} + +const LinkButton: React.FC = ({ text, url, type = "primary", style = {}, goBack = false, disabled = false }) => { + const navigate = useNavigate(); + + const handleClick = () => { + if (goBack) { + navigate(-1); + } else if (url) { + navigate(url); + } + }; + + const buttonStyle = type === "danger" ? { ...style, backgroundColor: 'red', borderColor: 'red', color: 'white' } : style; + + return ( + + ); +}; + +export default LinkButton; diff --git a/client_web/src/Components/Security.tsx b/client_web/src/Components/Security.tsx new file mode 100644 index 0000000..cc3f63d --- /dev/null +++ b/client_web/src/Components/Security.tsx @@ -0,0 +1,60 @@ +import React, { useState, useEffect } from "react"; +import { useAuth } from "@/Context/ContextHooks"; +import { useNavigate } from "react-router-dom"; +import { Spin } from 'antd'; +import { instance, instanceWithAuth, root, auth } from "@Config/backend.routes"; +import {toast} from "react-toastify"; + +type SecurityProps = { + children: React.ReactNode; +}; + +const Security = ({ children }: SecurityProps) => { + const { isAuthenticated, jsonWebToken, setIsAuthenticated, setJsonWebToken } = useAuth(); + const navigate = useNavigate(); + const [loading, setLoading] = useState(true); + const [_, setPingResponse] = React.useState(false); + const hasPinged = React.useRef(false); + + useEffect(() => { + const checkAuth = () => { + if (!isAuthenticated || !jsonWebToken) { + if (localStorage.getItem("jsonWebToken")) { + instanceWithAuth.get(auth.health) + .then(() => { + setIsAuthenticated(true); + setJsonWebToken(localStorage.getItem("jsonWebToken") as string); + }) + .catch((error) => { + console.error(error); + setIsAuthenticated(false); + setJsonWebToken(""); + localStorage.removeItem("jsonWebToken"); + navigate("/"); + }) + .finally(() => { + setLoading(false); + }); + } else { + localStorage.removeItem("jsonWebToken"); + setIsAuthenticated(false); + setJsonWebToken(""); + navigate("/"); + setLoading(false); + } + } else { + setLoading(false); + } + }; + + checkAuth(); + }, [isAuthenticated, jsonWebToken]); + + if (loading) { + return ; + } + + return <>{children}; +}; + +export default Security; diff --git a/client_web/src/backend.routes.ts b/client_web/src/Config/backend.routes.ts similarity index 79% rename from client_web/src/backend.routes.ts rename to client_web/src/Config/backend.routes.ts index 9019cb1..f1165bc 100644 --- a/client_web/src/backend.routes.ts +++ b/client_web/src/Config/backend.routes.ts @@ -31,14 +31,25 @@ instanceWithAuth.interceptors.request.use( } ); +const root = { + ping: `${endpoint}/ping`, + about: `${endpoint}/about.json`, +}; + const auth = { login: `${endpoint}/auth/login`, register: `${endpoint}/auth/register`, - google: `${endpoint}/auth/google`, + health: `${endpoint}/auth/health`, +} + +const workflow = { + create: `${endpoint}/workflow/create`, } export { instance, instanceWithAuth, - auth + root, + auth, + workflow } diff --git a/client_web/src/uri.ts b/client_web/src/Config/uri.ts similarity index 95% rename from client_web/src/uri.ts rename to client_web/src/Config/uri.ts index 6fd07e2..34434e3 100644 --- a/client_web/src/uri.ts +++ b/client_web/src/Config/uri.ts @@ -63,8 +63,7 @@ const uri: UriConfig = { auth: { // @ts-expect-error clientId: import.meta.env.VITE_DISCORD_CLIENT_ID as string, - // @ts-expect-error - clientSecret: import.meta.env.VITE_DISCORD_CLIENT_SECRET as string, + clientSecret: "", // Not expected to be provided redirectUri: `${window.location.origin}/auth/discord/callback` } } diff --git a/client_web/src/Context/CombinedProviders.tsx b/client_web/src/Context/CombinedProviders.tsx index 3d2709f..c058035 100644 --- a/client_web/src/Context/CombinedProviders.tsx +++ b/client_web/src/Context/CombinedProviders.tsx @@ -1,7 +1,7 @@ import { ReactNode } from 'react'; -import { UserProvider } from './UserContext'; -import { ThemeProvider } from './ThemeContext'; -import { AuthProvider } from './AuthContext'; +import { UserProvider } from './Scopes/UserContext'; +import { ThemeProvider } from './Scopes/ThemeContext'; +import { AuthProvider } from './Scopes/AuthContext'; const providers = [ UserProvider, diff --git a/client_web/src/Context/ContextHooks.ts b/client_web/src/Context/ContextHooks.ts index 88110be..7eedb9e 100644 --- a/client_web/src/Context/ContextHooks.ts +++ b/client_web/src/Context/ContextHooks.ts @@ -1,7 +1,7 @@ import { useContext } from 'react'; -import { UserContext } from './UserContext'; -import { ThemeContext } from './ThemeContext'; -import { AuthContext } from './AuthContext'; +import { UserContext } from './Scopes/UserContext'; +import { ThemeContext } from './Scopes/ThemeContext'; +import { AuthContext } from './Scopes/AuthContext'; export const useUser = () => { const context = useContext(UserContext); diff --git a/client_web/src/Context/AuthContext.tsx b/client_web/src/Context/Scopes/AuthContext.tsx similarity index 74% rename from client_web/src/Context/AuthContext.tsx rename to client_web/src/Context/Scopes/AuthContext.tsx index f8d38aa..94e179d 100644 --- a/client_web/src/Context/AuthContext.tsx +++ b/client_web/src/Context/Scopes/AuthContext.tsx @@ -1,6 +1,6 @@ -import { createContext, useState, ReactNode, useContext } from 'react'; +import {createContext, useState, ReactNode, useContext, useEffect} from 'react'; -interface AuthContextType { +export interface AuthContextType { isAuthenticated: boolean; jsonWebToken: string; setIsAuthenticated: (isAuthenticated: boolean) => void; @@ -13,6 +13,14 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { const [isAuthenticated, setIsAuthenticated] = useState(false); const [jsonWebToken, setJsonWebToken] = useState(''); + useEffect(() => { + const token = localStorage.getItem('jsonWebToken'); + if (token) { + setJsonWebToken(token); + setIsAuthenticated(true); + } + }, []); + return ( void; }; diff --git a/client_web/src/Context/UserContext.tsx b/client_web/src/Context/Scopes/UserContext.tsx similarity index 93% rename from client_web/src/Context/UserContext.tsx rename to client_web/src/Context/Scopes/UserContext.tsx index 4bf6bd8..6981a53 100644 --- a/client_web/src/Context/UserContext.tsx +++ b/client_web/src/Context/Scopes/UserContext.tsx @@ -5,7 +5,7 @@ interface User { name: string; } -interface UserContextType { +export interface UserContextType { user: User | null; setUser: (user: User | null) => void; } diff --git a/client_web/src/Pages/Auth/Callback/DiscordCallback.tsx b/client_web/src/Pages/Auth/Callback/DiscordCallback.tsx new file mode 100644 index 0000000..920b2f5 --- /dev/null +++ b/client_web/src/Pages/Auth/Callback/DiscordCallback.tsx @@ -0,0 +1,82 @@ +import { Card, Spin } from 'antd'; +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; + +const DiscordCallback = () => { + const navigate = useNavigate(); + const [error, setError] = useState(null); + + useEffect(() => { + const handleCallback = async () => { + const urlParams = new URLSearchParams(window.location.search); + const code = urlParams.get('code'); + const error = urlParams.get('error'); + const state = urlParams.get('state'); + const storedState = localStorage.getItem('discord_auth_state'); + + try { + if (state === null || state !== storedState) { + throw new Error('State mismatch. Please try again.'); + } + + if (error) { + throw new Error('Failed to connect with Discord'); + } + + if (!code) { + throw new Error('No authorization code received'); + } + + // TEMP: Store the code as if it were a token + // TODO: Implement actual token exchange with your backend + sessionStorage.setItem('access_token', code); + sessionStorage.setItem('refresh_token', 'dummy_refresh_token'); + + // Only navigate to dashboard if we reach this point successfully + setTimeout(() => { + if (code && !error && state === storedState) { + localStorage.removeItem('discord_auth_state'); + navigate('/dashboard'); + } + }, 2000); + return; + } catch (error: unknown) { + setError((error as Error)?.message || 'Failed to connect with Discord'); + setTimeout(() => { + navigate('/login'); + }, 2000); + } + }; + + handleCallback().catch(console.error); + }, [navigate]); + + return ( +
+ + {error ? ( + <> +

+ {error} +

+

+ Redirecting you back... +

+ + ) : ( + <> + +

+ Connecting to Discord +

+

+ Please wait while we complete your authentication... +

+ + )} +
+
+ ); +}; + +export default DiscordCallback; \ No newline at end of file diff --git a/client_web/src/pages/Auth/LinkedinCallback.tsx b/client_web/src/Pages/Auth/Callback/LinkedinCallback.tsx similarity index 100% rename from client_web/src/pages/Auth/LinkedinCallback.tsx rename to client_web/src/Pages/Auth/Callback/LinkedinCallback.tsx diff --git a/client_web/src/pages/Auth/MicrosoftCallback.tsx b/client_web/src/Pages/Auth/Callback/MicrosoftCallback.tsx similarity index 97% rename from client_web/src/pages/Auth/MicrosoftCallback.tsx rename to client_web/src/Pages/Auth/Callback/MicrosoftCallback.tsx index a3744e0..56ee531 100644 --- a/client_web/src/pages/Auth/MicrosoftCallback.tsx +++ b/client_web/src/Pages/Auth/Callback/MicrosoftCallback.tsx @@ -1,7 +1,7 @@ import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { PublicClientApplication } from '@azure/msal-browser'; -import { uri } from '../../uri'; +import { uri } from '../../../Config/uri'; const MicrosoftCallback = () => { const navigate = useNavigate(); diff --git a/client_web/src/pages/Auth/SpotifyCallback.tsx b/client_web/src/Pages/Auth/Callback/SpotifyCallback.tsx similarity index 100% rename from client_web/src/pages/Auth/SpotifyCallback.tsx rename to client_web/src/Pages/Auth/Callback/SpotifyCallback.tsx diff --git a/client_web/src/pages/Auth/Login.tsx b/client_web/src/Pages/Auth/Forms/Login.tsx similarity index 68% rename from client_web/src/pages/Auth/Login.tsx rename to client_web/src/Pages/Auth/Forms/Login.tsx index 7554976..eda0da3 100644 --- a/client_web/src/pages/Auth/Login.tsx +++ b/client_web/src/Pages/Auth/Forms/Login.tsx @@ -1,18 +1,36 @@ -import {Form, Input, Button, Card, Divider} from 'antd'; +import {Form, Input, Button, Card } from 'antd'; import { Link } from 'react-router-dom'; -import GoogleAuth from '../../components/auth/GoogleAuth'; -import MicrosoftAuth from '../../components/auth/MicrosoftAuth'; -import LinkedinAuth from '../../components/auth/LinkedinAuth'; -import SpotifyAuth from '../../components/auth/SpotifyAuth.tsx'; +import OAuthButtons from '../../../Components/Auth/OAuthButtons'; +import { instance, auth } from "@Config/backend.routes"; +import { useAuth } from "@/Context/ContextHooks"; +import { useNavigate } from 'react-router-dom'; +import {toast} from "react-toastify"; const Login = () => { + const { setJsonWebToken, isAuthenticated, setIsAuthenticated } = useAuth(); + + const navigate = useNavigate(); + const onFinish = (values: { email: string, password: string }) => { - console.log('Success:', values); - // TODO: Call login API function here + instance.post(auth.login, values) + .then((response) => { + if (!response?.data?.jwt) { + console.error('JWT not found in response'); + return; + } + localStorage.setItem('jsonWebToken', response?.data?.jwt); + setJsonWebToken(response?.data?.jwt); + setIsAuthenticated(true); + navigate('/dashboard'); + }) + .catch((error) => { + console.error('Failed:', error); + toast.error('Failed to login: ' + error?.response?.data?.error); + }); }; const onFinishFailed = (errorInfo: unknown) => { - console.log('Failed:', errorInfo); + console.error('Failed:', errorInfo); }; const handleGoogleSuccess = (credentialResponse: unknown) => { @@ -44,9 +62,6 @@ const Login = () => { return (
{ width: '100%', top: 0, left: 0 - }}> + }} role="main">
{ - Or - - - - - - - - diff --git a/client_web/src/pages/Auth/Register.tsx b/client_web/src/Pages/Auth/Forms/Register.tsx similarity index 73% rename from client_web/src/pages/Auth/Register.tsx rename to client_web/src/Pages/Auth/Forms/Register.tsx index 1edfb76..528aab9 100644 --- a/client_web/src/pages/Auth/Register.tsx +++ b/client_web/src/Pages/Auth/Forms/Register.tsx @@ -1,18 +1,38 @@ -import { Form, Input, Button, Card, Divider } from 'antd'; +import { Form, Input, Button, Card } from 'antd'; import { Link } from 'react-router-dom'; -import GoogleAuth from '../../components/auth/GoogleAuth'; -import MicrosoftAuth from '../../components/auth/MicrosoftAuth'; -import LinkedinAuth from "../../components/auth/LinkedinAuth.tsx"; -import SpotifyAuth from "../../components/auth/SpotifyAuth.tsx"; +import OAuthButtons from '@/Components/Auth/OAuthButtons'; +import { instance, auth } from "@Config/backend.routes"; +import { useAuth } from "@/Context/ContextHooks"; +import { useNavigate } from 'react-router-dom'; +import {toast} from "react-toastify"; const Register = () => { + const { setJsonWebToken, isAuthenticated, setIsAuthenticated } = useAuth(); + + const navigate = useNavigate(); + const onFinish = (values: unknown) => { - console.log('Success:', values); - // TODO: Call register API function here + instance.post(auth.register, values) + .then((response) => { + if (!response?.data?.jwt) { + console.error('JWT not found in response'); + return; + } + localStorage.setItem('jsonWebToken', response?.data?.jwt); + setJsonWebToken(response?.data?.jwt); + setIsAuthenticated(true); + if(isAuthenticated) { + navigate('/dashboard'); + } + }) + .catch((error) => { + console.error('Failed:', error); + toast.error('Failed to register: ' + error?.response?.data?.error); + }); }; const onFinishFailed = (errorInfo: unknown) => { - console.log('Failed:', errorInfo); + console.error('Failed:', errorInfo); }; const handleGoogleSuccess = (credentialResponse: unknown) => { @@ -44,9 +64,6 @@ const Register = () => { return (
{ width: '100%', top: 0, left: 0 - }}> + }} role="main"> { - Or - - - - - - - - diff --git a/client_web/src/pages/Dashboard/Dashboard.tsx b/client_web/src/Pages/Dashboard/Dashboard.tsx similarity index 51% rename from client_web/src/pages/Dashboard/Dashboard.tsx rename to client_web/src/Pages/Dashboard/Dashboard.tsx index 4ea45fa..b37a877 100644 --- a/client_web/src/pages/Dashboard/Dashboard.tsx +++ b/client_web/src/Pages/Dashboard/Dashboard.tsx @@ -1,44 +1,47 @@ import { FC } from "react"; import { Row, Col, Card, Typography, Space } from "antd"; import { RobotOutlined, BarChartOutlined, SettingOutlined } from "@ant-design/icons"; -import Security from "../../components/Security.tsx"; +import Security from "@/Components/Security"; +import LinkButton from "@/Components/LinkButton"; const { Title } = Typography; const Dashboard: FC = () => { return ( -
- {/* Header */} - + <div style={{padding: 24, position: 'relative', zIndex: 1}} role="main"> + <Title level={3} style={{marginBottom: 16}}> Dashboard {/* Stats Cards */} - + - - - 12 + + + {/* TODO: Replace X with the number of active automations */} + X Active Automations - - - 1,234 + + + {/* TODO: Replace X with the number of tasks completed */} + X Tasks Completed - - - 5 + + + {/* TODO: Replace X with the number of pending updates */} + X Pending Updates @@ -48,13 +51,16 @@ const Dashboard: FC = () => { {/* Main Content Area */} - + {/* Activity content would go here */} - - {/* Quick actions content would go here */} + + + + + diff --git a/client_web/src/Pages/Errors/ApiNotConnected.tsx b/client_web/src/Pages/Errors/ApiNotConnected.tsx new file mode 100644 index 0000000..3d5c36b --- /dev/null +++ b/client_web/src/Pages/Errors/ApiNotConnected.tsx @@ -0,0 +1,19 @@ +import { Result } from 'antd'; +import LinkButton from "@/Components/LinkButton"; +import React from 'react'; + +const ApiNotConnected: React.FC = () => { + return ( + + } + style={{ zIndex: 1, position: 'relative' }} + /> + ); +}; + +export default ApiNotConnected; diff --git a/client_web/src/pages/NotFound.tsx b/client_web/src/Pages/Errors/NotFound.tsx similarity index 59% rename from client_web/src/pages/NotFound.tsx rename to client_web/src/Pages/Errors/NotFound.tsx index 9cf1700..cf9872b 100644 --- a/client_web/src/pages/NotFound.tsx +++ b/client_web/src/Pages/Errors/NotFound.tsx @@ -1,5 +1,5 @@ -import { Result, Button } from 'antd'; -import { Link } from 'react-router-dom'; +import { Result } from 'antd'; +import LinkButton from "@/Components/LinkButton"; import React from 'react'; const NotFound: React.FC = () => { @@ -9,10 +9,9 @@ const NotFound: React.FC = () => { title="404" subTitle="Sorry, the page you visited does not exist." extra={ - + } + style={{ zIndex: 1, position: 'relative' }} /> ); }; diff --git a/client_web/src/Pages/Home.tsx b/client_web/src/Pages/Home.tsx new file mode 100644 index 0000000..a730599 --- /dev/null +++ b/client_web/src/Pages/Home.tsx @@ -0,0 +1,190 @@ +import React from 'react'; +import { Layout, Typography, Row, Col, Button, Card, Modal } from 'antd'; +import { ThunderboltOutlined, ApiOutlined, SafetyCertificateOutlined } from '@ant-design/icons'; +import { motion } from 'framer-motion'; +// @ts-ignore +import { BlockPicker } from 'react-color'; +import { instance, root } from "@Config/backend.routes"; +import { toast } from "react-toastify"; +import { useNavigate } from "react-router-dom"; +import { useAuth } from "@/Context/ContextHooks"; + +const { Content } = Layout; +const { Title, Paragraph } = Typography; + +interface HomeProps { + backgroundColor: string; + setBackgroundColor: (color: string) => void; +} + +const Home: React.FC = ({ backgroundColor, setBackgroundColor }) => { + const [isModalVisible, setIsModalVisible] = React.useState(false); + const [tempColor, setTempColor] = React.useState(backgroundColor); + const [pingResponse, setPingResponse] = React.useState(false); + const hasPinged = React.useRef(false); + const navigate = useNavigate(); + + const { isAuthenticated } = useAuth(); + + const handleColorChange = (color: { hex: any; }) => { + setTempColor(color.hex); + }; + + const showModal = () => { + setTempColor(backgroundColor); + setIsModalVisible(true); + }; + + const handleOk = () => { + setBackgroundColor(tempColor); + sessionStorage.setItem('backgroundColor', tempColor); + setIsModalVisible(false); + }; + + const handleCancel = () => { + setIsModalVisible(false); + }; + + const ping = () => { + const response = instance.get(root.ping) + .then((response) => { + setPingResponse(true); + }) + .catch((error) => { + setPingResponse(false); + console.error(error); + toast.error('Failed to ping the server'); + }); + }; + + React.useEffect(() => { + if (!hasPinged.current) { + ping(); + hasPinged.current = true; + } + }, []); + + const handleGetStarted = () => { + if (isAuthenticated) { + navigate('/dashboard'); + } else { + navigate('/login'); + } + }; + + return ( + + + + + + Connect Your Digital World + + + Automate your life by connecting your favorite services. Create powerful automation flows with just a few clicks. + + + + + Automation Illustration + + + + + + + + + + + Easy Automation + + Create powerful automation workflows with our intuitive drag-and-drop interface. + No coding required! + + + + + + + + + Connect Services + + Integrate with popular services and apps. Make them work together seamlessly. + + + + + + + + + Secure & Reliable + + Your data is protected with enterprise-grade security. Run your automations with confidence. + + + + + + + +
+
+ + How It Works + + + {[ + { step: '1', title: 'Choose a Trigger', description: 'Select an event that starts your automation' }, + { step: '2', title: 'Add Actions', description: 'Define what happens when the trigger fires' }, + { step: '3', title: 'Watch It Work', description: 'Sit back and let Area handle the rest' }, + ].map((item) => ( + + + + + {item.step} + + {item.title} + {item.description} + + + + ))} + +
+
+ +
+
+ + + +
+ ); +}; + +export default Home; diff --git a/client_web/src/Pages/Workflows/CreateWorkflow.tsx b/client_web/src/Pages/Workflows/CreateWorkflow.tsx new file mode 100644 index 0000000..f598472 --- /dev/null +++ b/client_web/src/Pages/Workflows/CreateWorkflow.tsx @@ -0,0 +1,646 @@ +import React, { useState } from "react"; +import { Button, Row, Typography, Spin, Space, Card, List, Form, Input, Col, Collapse } from "antd"; +import Security from "@/Components/Security"; +import LinkButton from "@/Components/LinkButton"; +import { normalizeName } from "@/Pages/Workflows/CreateWorkflow.utils"; + +// Import types correctly +import { About, Service, Action, Reaction, Workflow, Parameter } from "@/types"; +import {toast} from "react-toastify"; +import {instanceWithAuth} from "@/Config/backend.routes"; +import {workflow as workflowRoute} from "@/Config/backend.routes"; + +const { Title, Text } = Typography; +const { Panel } = Collapse; + +interface SelectedAction extends Action { + id: string; +} + +interface SelectedReaction extends Reaction { + id: string; +} + +const _data: About = { // Fixtures + client: { + host: "10.101.53.35" + }, + server: { + current_time: 1531680780, + services: [ + { + name: "facebook", + actions: [ + { + name: "new_message_in_group_w/o", + description: "A new message is posted in the group", + parameters: [] + }, + { + name: "new_message_inbox", + description: "A new private message is received by the user", + parameters: [ + { name: "message_id", description: "ID of the message", type: "number" } + ] + }, + { + name: "new_like_w/o", + description: "The user gains a like from one of their messages", + parameters: [] + } + ], + reactions: [ + { + name: "like_message", + description: "The user likes a message", + parameters: [ + { name: "message_id", description: "ID of the message to like", type: "string" } + ] + } + ] + }, + { + name: "twitter", + actions: [ + { + name: "new_tweet", + description: "A new tweet is posted", + parameters: [ + { name: "tweet_id", description: "ID of the tweet", type: "string" } + ] + }, + { + name: "new_follower_w/o", + description: "The user gains a new follower", + parameters: [] + } + ], + reactions: [ + { + name: "retweet", + description: "The user retweets a tweet", + parameters: [ + { name: "tweet_id", description: "ID of the tweet to retweet", type: "string" } + ] + }, + { + name: "like_tweet_w/o", + description: "The user likes a tweet", + parameters: [] + } + ] + }, + { + name: "github", + actions: [ + { + name: "new_issue", + description: "A new issue is created in a repository", + parameters: [ + { name: "issue_id", description: "ID of the issue", type: "string" } + ] + }, + { + name: "new_pull_request_w/o", + description: "A new pull request is created in a repository", + parameters: [] + } + ], + reactions: [ + { + name: "create_issue", + description: "The user creates a new issue", + parameters: [ + { name: "repository", description: "Name of the repository", type: "string" }, + { name: "title", description: "Title of the issue", type: "string" } + ] + }, + { + name: "merge_pull_request_w/o", + description: "The user merges a pull request", + parameters: [] + } + ] + }, + { + name: "slack", + actions: [ + { + name: "new_message", + description: "A new message is posted in a channel", + parameters: [ + { name: "channel_id", description: "ID of the channel", type: "string" }, + { name: "message_text", description: "Text of the message", type: "string" } + ] + }, + { + name: "new_reaction_w/o", + description: "A new reaction is added to a message", + parameters: [] + } + ], + reactions: [ + { + name: "send_message", + description: "The user sends a message to a channel", + parameters: [ + { name: "channel_id", description: "ID of the channel", type: "string" }, + { name: "message_text", description: "Text of the message", type: "string" } + ] + }, + { + name: "add_reaction_w/o", + description: "The user adds a reaction to a message", + parameters: [] + } + ] + } + ] + } +}; + +const CreateWorkflow: React.FC = () => { + const [loading, setLoading] = React.useState(false); + const [about, setAbout] = React.useState(null); + const [selectedActions, setSelectedActions] = React.useState([]); + const [selectedReactions, setSelectedReactions] = React.useState([]); + const [workflowName, setWorkflowName] = React.useState(''); + const [workflowDescription, setWorkflowDescription] = React.useState(''); + const [workflowNameTouched, setWorkflowNameTouched] = React.useState(false); + const [activeActionKeys, setActiveActionKeys] = useState([]); + const [activeReactionKeys, setActiveReactionKeys] = useState([]); + + React.useEffect(() => { + setLoading(true); + setAbout(_data); // TODO: Fetch workflow from the server + setLoading(false); + }, []); + + const toggleAction = (action: Action) => { + + if (selectedActions.length > 0) { + return toast.error("Only one action is allowed for now"); + } + + const parameters = action.parameters?.length + ? action.parameters.reduce((acc: Record, param: Parameter) => ({...acc, [param.name]: ''}), {}) + : undefined; + + // @ts-ignore + setSelectedActions(prev => [ + ...prev, + { + id: `${action.name}-${Date.now()}`, + name: action.name, + description: action.description, + parameters + } + ]); + }; + + const toggleReaction = (reaction: Reaction) => { + + if (selectedReactions.length > 0) { + return toast.error("Only one reaction is allowed for now"); + } + + const parameters = reaction.parameters?.length + ? reaction.parameters.reduce((acc: Record, param: Parameter) => ({...acc, [param.name]: ''}), {}) + : undefined; + + // @ts-ignore + setSelectedReactions(prev => [ + ...prev, + { + id: `${reaction.name}-${Date.now()}`, + name: reaction.name, + description: reaction.description, + parameters + } + ]); + }; + + const areAllParametersFilled = () => { + const actionsComplete = selectedActions.every(action => { + if (!action.parameters) return true; + // @ts-ignore + return Object.values(action.parameters).every(value => value !== ''); + }); + + const reactionsComplete = selectedReactions.every(reaction => { + if (!reaction.parameters) return true; + // @ts-ignore + return Object.values(reaction.parameters).every(value => value !== ''); + }); + + return actionsComplete && reactionsComplete; + }; + + const handleCreateWorkflow = () => { + const workflow: Workflow = { + name: workflowName, + description: workflowDescription, + service: about?.server.services.find(service => + service.actions.some(action => action.name === selectedActions[0]?.name) + )?.name ?? "unknown", + events: + [ + ...selectedActions.map(action => { + const actionDef = about?.server.services + .flatMap((s: Service) => s.actions) + .find((a: Action) => a.name === action.name); + + return { + name: action.name, + type: 'action' as "action", + description: action.description, + parameters: Object.entries(action.parameters || {}).map(([name, value]) => { + const paramDef = actionDef?.parameters.find((p: Parameter) => p.name === name); + return { + name, + type: paramDef?.type || 'string', + value + }; + }) + } + }), + ...selectedReactions.map(reaction => { + const reactionDef = about?.server.services + .flatMap((s: Service) => s.reactions) + .find((r: Reaction) => r.name === reaction.name); + + return { + name: reaction.name, + type: 'reaction' as "reaction", + description: reaction.description, + parameters: Object.entries(reaction.parameters || {}).map(([name, value]) => { + const paramDef = reactionDef?.parameters.find((p: Parameter) => p.name === name); + return { + name, + type: paramDef?.type || 'string', + value + }; + }) + }; + }) + ] + } + instanceWithAuth.post(workflowRoute.create, workflow) + .then(() => { + toast.success("Workflow successfully published") + //TODO: Go to /workflow/{id} + }) + .catch((error) => { + console.error(error); + }); + }; + + const handleFoldAllActions = () => { + setActiveActionKeys([]); + }; + + const handleUnfoldAllActions = () => { + if (about) { + setActiveActionKeys(about.server.services.map((service: Service) => service.name)); + } + }; + + const handleFoldAllReactions = () => { + setActiveReactionKeys([]); + }; + + const handleUnfoldAllReactions = () => { + if (about) { + setActiveReactionKeys(about.server.services.map((service: Service) => service.name)); + } + }; + + return ( + +
+ + Create Workflow + + + {loading ? ( + + ) : ( + <> + + + + + + setWorkflowName(e.target.value)} + onBlur={() => setWorkflowNameTouched(true)} + aria-required="true" + /> + + + setWorkflowDescription(e.target.value)} + /> + + + + + + + + + + + + + + + {about?.server?.services.map((service: Service) => ( + + ( + toggleAction(action)} + style={{ + cursor: 'pointer', + backgroundColor: selectedActions.some(a => a.name === action.name) + ? '#e6f7ff' + : 'transparent' + }} + > + + {service.name} +
+ {action.description} + + } + /> +
+ )} + /> +
+ ))} +
+
+ + + + +
+ {selectedActions.length > 0 && ( + + When: + + {selectedActions.map(action => ( + { + e.stopPropagation(); + setSelectedActions(prev => prev.filter(a => a.id !== action.id)); + }} + > + × + + } + > + + {normalizeName(action.name)} + {action.parameters && Object.entries(action.parameters).map(([key, value]) => { + const paramDef = about?.server.services + .flatMap((s: any) => s.actions) + .find((a: any) => a.name === action.name) + ?.parameters + .find((p: any) => p.name === key); + + return ( + + {paramDef?.type === 'number' ? ( + { + setSelectedActions(prev => prev.map(a => + a.id === action.id + ? {...a, parameters: {...a.parameters, [key]: e.target.value}} + : a + )); + }} + /> + ) : ( + { + setSelectedActions(prev => prev.map(a => + a.id === action.id + ? {...a, parameters: {...a.parameters, [key]: e.target.value}} + : a + )); + }} + /> + )} + + ); + })} + + + ))} + + + )} + + {selectedReactions.length > 0 && ( + + Then: + + {selectedReactions.map(reaction => ( + { + e.stopPropagation(); + setSelectedReactions(prev => prev.filter(r => r.id !== reaction.id)); + }} + > + × + + } + > + + {normalizeName(reaction.name)} + {reaction.parameters && Object.entries(reaction.parameters).map(([key, value]) => { + const paramDef = about?.server.services + .flatMap((s: any) => s.reactions) + .find((r: any) => r.name === reaction.name) + ?.parameters + .find((p: any) => p.name === key); + + return ( + + {paramDef?.type === 'number' ? ( + { + setSelectedReactions(prev => prev.map(r => + r.id === reaction.id + ? {...r, parameters: {...r.parameters, [key]: e.target.value}} + : r + )); + }} + /> + ) : ( + { + setSelectedReactions(prev => prev.map(r => + r.id === reaction.id + ? {...r, parameters: {...r.parameters, [key]: e.target.value}} + : r + )); + }} + /> + )} + + ); + })} + + + ))} + + + )} + + + + + + +
+
+ + + + + + + + + + {about?.server?.services.map((service: Service) => ( + + ( + toggleReaction(reaction)} + style={{ + cursor: 'pointer', + backgroundColor: selectedReactions.some(r => r.name === reaction.name) + ? '#e6f7ff' + : 'transparent' + }} + > + + {service.name} +
+ {reaction.description} + + } + /> +
+ )} + /> +
+ ))} +
+
+ +
+ + )} +
+
+ ); +}; + +export default CreateWorkflow; diff --git a/client_web/src/Pages/Workflows/CreateWorkflow.utils.ts b/client_web/src/Pages/Workflows/CreateWorkflow.utils.ts new file mode 100644 index 0000000..424b810 --- /dev/null +++ b/client_web/src/Pages/Workflows/CreateWorkflow.utils.ts @@ -0,0 +1,3 @@ +export const normalizeName = (name: string): string => { + return name.replace(/_/g, " ").replace(/\b\w/g, (c: string): string => c.toUpperCase()); +}; diff --git a/client_web/src/Pages/Workflows/WorkflowsTable.tsx b/client_web/src/Pages/Workflows/WorkflowsTable.tsx new file mode 100644 index 0000000..c372afb --- /dev/null +++ b/client_web/src/Pages/Workflows/WorkflowsTable.tsx @@ -0,0 +1,51 @@ +import React from "react"; +import { Row, Typography, Spin, Space } from "antd"; +// @ts-ignore +import Security from "@/Components/Security.tsx"; +// @ts-ignore +import LinkButton from "@/Components/LinkButton.tsx"; + +// @ts-ignore +import { Workflow } from "@/types"; + +const { Title } = Typography; + +const WorkflowTable: React.FC = () => { + const [loading, setLoading] = React.useState(false); + const [_, setWorkflows] = React.useState([]); + + React.useEffect(() => { + setLoading(true); + setWorkflows([]); // TODO: Fetch workflows from the server + setLoading(false); + }, []); + + return ( + +
+ + Create Workflow + + + {loading ? ( + + ) : ( + <> + + + This is where you see all your workflows. + + + + + + + + + )} +
+
+ ); +}; + +export default WorkflowTable; diff --git a/client_web/src/components/Security.tsx b/client_web/src/components/Security.tsx deleted file mode 100644 index a3a3da7..0000000 --- a/client_web/src/components/Security.tsx +++ /dev/null @@ -1,27 +0,0 @@ -// import React, { useEffect } from "react"; -// import { useAuth } from "../Context/ContextHooks"; -// import { useNavigate } from "react-router-dom"; - -type SecurityProps = { - children: React.ReactNode; -}; - -const Security = ({ children }: SecurityProps) => { - // const { isAuthenticated } = useAuth(); - // const navigate = useNavigate(); - - // TODO: Connect to the backend and check if the user is authenticated - // useEffect(() => { - // if (!isAuthenticated) { - // navigate("/"); - // } - // }, [isAuthenticated, navigate]); - - // if (!isAuthenticated) { - // return null; - // } - - return <>{children}; -}; - -export default Security; diff --git a/client_web/src/components/auth/GoogleAuth.tsx b/client_web/src/components/auth/GoogleAuth.tsx deleted file mode 100644 index 29210e6..0000000 --- a/client_web/src/components/auth/GoogleAuth.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Form } from 'antd'; -import { GoogleLogin, GoogleOAuthProvider } from '@react-oauth/google'; - -interface GoogleAuthProps { - onSuccess: (response: unknown) => void; - onError: () => void; - buttonText?: string; -} - -const GoogleAuth = ({ onSuccess, onError, buttonText = "signin_with"}: GoogleAuthProps) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - const googleClientId: string = import.meta.env.VITE_GOOGLE_CLIENT_ID as string; - - if (!googleClientId || googleClientId === '') { - return null; - } - - return ( - - - - - - ); -}; - -export default GoogleAuth; diff --git a/client_web/src/main.tsx b/client_web/src/main.tsx index f7ed199..144714e 100644 --- a/client_web/src/main.tsx +++ b/client_web/src/main.tsx @@ -1,6 +1,6 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' -import App from "./App.tsx"; +import App from "./App"; createRoot(document.getElementById('root')!).render( diff --git a/client_web/src/pages/Footer.tsx b/client_web/src/pages/Footer.tsx deleted file mode 100644 index 0aa025d..0000000 --- a/client_web/src/pages/Footer.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Layout } from 'antd'; -import { useTheme } from '../Context/ContextHooks'; -import React from "react"; - -const { Footer } = Layout; - -const AppFooter: React.FC = () => { - const { theme } = useTheme(); - - return ( -
- ©2024 ASM. All Rights Reserved. -
- ); -}; - -export default AppFooter; diff --git a/client_web/src/pages/Header.tsx b/client_web/src/pages/Header.tsx deleted file mode 100644 index 4564975..0000000 --- a/client_web/src/pages/Header.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Layout, Menu } from 'antd'; -import { Link } from 'react-router-dom'; -import { useTheme } from '../Context/ContextHooks'; -import React from "react"; - -const { Header: AntHeader } = Layout; - -interface MenuItems { - key: string; - label: React.ReactNode; -} - -const menuItems: MenuItems[] = [ - { key: '/', label: Home }, - { key: '/about', label: About }, -]; - -const Header: React.FC = () => { - const { theme } = useTheme(); - - return ( - - - - ); -}; - -export default Header; diff --git a/client_web/src/pages/Home.tsx b/client_web/src/pages/Home.tsx deleted file mode 100644 index b39954c..0000000 --- a/client_web/src/pages/Home.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { Layout, Typography, Row, Col, Button, Card } from 'antd'; -import { ThunderboltOutlined, ApiOutlined, SafetyCertificateOutlined } from '@ant-design/icons'; -import { motion } from 'framer-motion'; -import { Link } from 'react-router-dom'; - -const { Content } = Layout; -const { Title, Paragraph } = Typography; - -const Home = () => { - return ( - - {/* Hero Section */} - - - - - Connect Your Digital World - - - Automate your life by connecting your favorite services. Create powerful automation flows with just a few clicks. - - - - - - - Automation Illustration - - - - - {/* Features Section */} - - - - - - - Easy Automation - - Create powerful automation workflows with our intuitive drag-and-drop interface. - No coding required! - - - - - - - - - Connect Services - - Integrate with popular services and apps. Make them work together seamlessly. - - - - - - - - - Secure & Reliable - - Your data is protected with enterprise-grade security. Run your automations with confidence. - - - - - - - - {/* How It Works Section */} -
-
- - How It Works - - - {[ - { step: '1', title: 'Choose a Trigger', description: 'Select an event that starts your automation' }, - { step: '2', title: 'Add Actions', description: 'Define what happens when the trigger fires' }, - { step: '3', title: 'Watch It Work', description: 'Sit back and let Area handle the rest' }, - ].map((item) => ( - - - - {item.step} - - {item.title} - {item.description} - - - ))} - -
-
-
- ); -}; - -export default Home; diff --git a/client_web/src/types.ts b/client_web/src/types.ts new file mode 100644 index 0000000..8965467 --- /dev/null +++ b/client_web/src/types.ts @@ -0,0 +1,65 @@ +export type Parameter = { + name: string; + description: string; + type: "string" | "number" | "datetime"; +}; + +export type Action = { + name: string; + description: string; + parameters: Parameter[]; +}; + +export type Reaction = { + name: string; + description: string; + parameters: Parameter[]; +}; + +export type Service = { + name: string; + actions: Action[]; + reactions: Reaction[]; +}; + +export type Server = { + current_time: number; + services: Service[]; +}; + +export type Client = { + host: string; +}; + +export type About = { + client: Client; + server: Server; +}; + +/* --------------------------------- */ + +export interface WorkflowParameter { + name: string; + type: "string" | "number" | "datetime"; + value: unknown; +} + +export interface WorkflowDefinition { + name: string; + type: "action" | "reaction"; + description: string; + parameters: WorkflowParameter[]; +} + +export type Workflow = { + name: string; + service: string; + description: string; + events: WorkflowDefinition[]; +}; + +export interface WorkflowItem { + id: string; + name: string; + parameters?: Record; +} diff --git a/client_web/tsconfig.app.json b/client_web/tsconfig.app.json index f867de0..ec2a48e 100644 --- a/client_web/tsconfig.app.json +++ b/client_web/tsconfig.app.json @@ -1,26 +1,15 @@ { + "extends": "./tsconfig.base.json", "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "target": "ES2020", "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "Bundler", - "allowImportingTsExtensions": true, - "isolatedModules": true, - "moduleDetection": "force", + "composite": true, + "outDir": "./dist/app", "noEmit": true, - "jsx": "react-jsx", - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true + "jsx": "react-jsx" }, - "include": ["src"] + "include": [ + "src", + "src/app" + ] } diff --git a/client_web/tsconfig.base.json b/client_web/tsconfig.base.json new file mode 100644 index 0000000..9672bbf --- /dev/null +++ b/client_web/tsconfig.base.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force" + } +} diff --git a/client_web/tsconfig.json b/client_web/tsconfig.json index 1ffef60..3391afc 100644 --- a/client_web/tsconfig.json +++ b/client_web/tsconfig.json @@ -1,7 +1,18 @@ { - "files": [], - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ] + "compilerOptions": { + "baseUrl": "./", + "paths": { + "@/*": ["src/*"], + "@Config/*": ["src/Config/*"] + }, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "commonjs", + "target": "es2015", + "noEmit": true, + "jsx": "react-jsx", + "esModuleInterop": true, + }, + "include": ["src"] } diff --git a/client_web/tsconfig.node.json b/client_web/tsconfig.node.json index abcd7f0..1bf6cb1 100644 --- a/client_web/tsconfig.node.json +++ b/client_web/tsconfig.node.json @@ -1,24 +1,16 @@ { + "extends": "./tsconfig.base.json", "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", "target": "ES2022", "lib": ["ES2023"], - "module": "ESNext", - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "Bundler", - "allowImportingTsExtensions": true, - "isolatedModules": true, - "moduleDetection": "force", - "noEmit": true, - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true + "composite": true, + "outDir": "./dist/node", + "noEmit": true }, - "include": ["vite.config.ts"] + "include": [ + "src/app", + "vite.config.ts", + "src/node" + ] } diff --git a/client_web/vite.config.ts b/client_web/vite.config.ts index 9d8406e..1371208 100644 --- a/client_web/vite.config.ts +++ b/client_web/vite.config.ts @@ -15,5 +15,12 @@ export default defineConfig(({ mode }) => { cert: fs.readFileSync(path.resolve(__dirname, 'localhost.pem')), }, }, + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + '@Config': path.resolve(__dirname, './src/Config'), + // ... other aliases + } + } } }) diff --git a/docker-compose.yml b/docker-compose.yml index ba5d500..f8dde58 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,32 +1,63 @@ services: - # server: - # build: ./server - # ports: - # - "8080:8080" - # networks: - # - app_network - - # client_mobile: - # build: ./client_mobile - # volumes: - # - client_build:/app/build - # networks: - # - app_network + area-client-mobile: + build: + context: ./client_mobile + dockerfile: Dockerfile + args: + - VITE_PORT + - VITE_ENDPOINT + - VITE_GOOGLE_CLIENT_ID + - VITE_GOOGLE_CLIENT_SECRET + - VITE_MICROSOFT_CLIENT_ID + - VITE_LINKEDIN_CLIENT_ID + - VITE_LINKEDIN_CLIENT_SECRET + - VITE_SPOTIFY_CLIENT_ID + - VITE_SPOTIFY_CLIENT_SECRET + - API_URL + - WEB_CLIENT_URL + - MOBILE_CLIENT_URL + - GITHUB_CLIENT_ID + - GITHUB_CLIENT_SECRET + volumes: + - area-client-data:/app/build/app/outputs/flutter-apk + networks: + - area-network + env_file: + - ./client_mobile/.env.mobile - client_web: - build: ./client_web + area-client-web: + build: + context: ./client_web + dockerfile: Dockerfile + args: + - VITE_PORT + - VITE_ENDPOINT + - VITE_GOOGLE_CLIENT_ID + - VITE_GOOGLE_CLIENT_SECRET + - VITE_MICROSOFT_CLIENT_ID + - VITE_LINKEDIN_CLIENT_ID + - VITE_LINKEDIN_CLIENT_SECRET + - VITE_SPOTIFY_CLIENT_ID + - VITE_SPOTIFY_CLIENT_SECRET + - API_URL + - WEB_CLIENT_URL + - MOBILE_CLIENT_URL + - GITHUB_CLIENT_ID + - GITHUB_CLIENT_SECRET ports: - - "8081:8081" + - "${VITE_PORT}:${VITE_PORT}" volumes: - - client_build:/usr/share/nginx/html/mobile_builds - # depends_on: - # - client_mobile - # - server + - area-client-data:/usr/share/nginx/html/mobile_builds + depends_on: + - area-client-mobile networks: - - app_network + - area-network + env_file: + - ./client_web/.env.local volumes: - client_build: + area-client-data: + networks: - app_network: + area-network: diff --git a/docs/source/client_web/index.rst b/docs/source/client_web/index.rst index 0f4fcca..e357c70 100644 --- a/docs/source/client_web/index.rst +++ b/docs/source/client_web/index.rst @@ -32,7 +32,8 @@ Contents -------- .. toctree:: - :maxdepth: 2 + :maxdepth: 2 - installation - context + installation + context + workflowCreation diff --git a/docs/source/client_web/workflowCreation.rst b/docs/source/client_web/workflowCreation.rst new file mode 100644 index 0000000..4a32989 --- /dev/null +++ b/docs/source/client_web/workflowCreation.rst @@ -0,0 +1,52 @@ +Create Workflow +=============== + +The "Create Workflow" feature allows users to create custom workflows by selecting actions and reactions from various services. Follow the steps below to create a workflow. + +Steps to Create a Workflow +-------------------------- + +1. **Enter Workflow Name**: + - Provide a name for your workflow in the "Enter workflow name" field. + - This field is required. + +2. **Enter Workflow Description**: + - Optionally, provide a description for your workflow in the "Enter workflow description" field. + +3. **Select Actions**: + - Browse through the available actions listed under "Available Actions". + - Click on an action to add it to your workflow. + - Use the "Fold All" and "Unfold All" buttons to manage the visibility of action categories. + +4. **Select Reactions**: + - Browse through the available reactions listed under "Available Reactions". + - Click on a reaction to add it to your workflow. + - Use the "Fold All" and "Unfold All" buttons to manage the visibility of reaction categories. + +5. **Review Selected Items**: + - The selected actions and reactions will be displayed in the "Selected Items" section. + - You can remove any selected action or reaction by clicking the "×" button next to it. + +6. **Clear Selections**: + - Use the "Clear Actions" button to remove all selected actions. + - Use the "Clear Reactions" button to remove all selected reactions. + +7. **Create Workflow**: + - Click the "Create Workflow" button to create your workflow. + - Ensure that you have provided a workflow name and selected at least one action and one reaction. + - If the workflow creation is successful, a confirmation message will be displayed. + +8. **Cancel**: + - Click the "Cancel" button to discard the workflow creation process and go back to the previous page. + +Accessibility Features +---------------------- + +- The "Create Workflow" page is designed to be accessible with keyboard navigation and screen readers. +- The "Fold All" and "Unfold All" buttons help manage the visibility of action and reaction categories for easier navigation. + +Error Handling +-------------- + +- If the workflow name is not provided, an error message will be displayed indicating that the workflow name is required. +- If the workflow creation fails, an error message will be displayed. diff --git a/docs/source/Project/benchmark.rst b/docs/source/project/benchmark.rst similarity index 100% rename from docs/source/Project/benchmark.rst rename to docs/source/project/benchmark.rst diff --git a/docs/source/project/devops/ci_cd.rst b/docs/source/project/devops/ci_cd.rst new file mode 100644 index 0000000..bd2da6a --- /dev/null +++ b/docs/source/project/devops/ci_cd.rst @@ -0,0 +1,99 @@ +CI/CD Pipeline +============= + +This page details our Continuous Integration and Continuous Deployment (CI/CD) pipelines implemented using GitHub Actions. + +Documentation Deployment +---------------------- + +Our documentation is automatically built and deployed using GitHub Actions whenever changes are pushed to the main branch. + +Workflow Configuration +~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: yaml + + name: Deploy Documentation + + on: + push: + branches: + - main + paths: + - 'docs/**' + - '.github/workflows/deploy-documentation.yml' + + jobs: + deploy-documentation: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install sphinx sphinx_rtd_theme + + - name: Build documentation + run: | + cd docs + make html + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./docs/build/html + +Workflow Details +~~~~~~~~~~~~~~ + +Triggers +^^^^^^^^ +The workflow is triggered on: + +- Push events to the ``main`` branch +- Changes to files in the ``docs/`` directory +- Changes to the workflow file itself + +Steps Explanation +^^^^^^^^^^^^^^^ + +1. **Checkout Repository** + + - Uses ``actions/checkout@v4`` to clone the repository + - Ensures all documentation source files are available + +2. **Python Setup** + + - Sets up Python environment using ``actions/setup-python@v4`` + - Uses latest Python version + +3. **Dependencies Installation** + + - Upgrades pip to latest version + - Installs Sphinx and Read the Docs theme + +4. **Documentation Build** + + - Changes to docs directory + - Runs ``make html`` to build documentation + - Generates HTML files in ``docs/build/html`` + +5. **GitHub Pages Deployment** + + - Uses ``peaceiris/actions-gh-pages@v3`` + - Deploys built documentation to GitHub Pages + - Uses ``GITHUB_TOKEN`` for authentication + - Publishes content from ``./docs/build/html`` + +Access Points +----------- + +The deployed documentation can be accessed at:``https://asm-studios.github.io/AREA/`` \ No newline at end of file diff --git a/docs/source/project/devops/dependencies.rst b/docs/source/project/devops/dependencies.rst new file mode 100644 index 0000000..f1840eb --- /dev/null +++ b/docs/source/project/devops/dependencies.rst @@ -0,0 +1,91 @@ +Project Dependencies +================== + +This page details all the technologies, tools, and external projects used in our DevOps setup. + +Container Technologies +------------------- + +Docker +~~~~~~ +- **Version**: Latest +- **Usage**: Container orchestration and deployment +- **Purpose**: Ensures consistent development and production environments +- **Source**: `Official Docker `_ + +Docker Compose +~~~~~~~~~~~~ +- **Version**: 3.x +- **Usage**: Multi-container application definition and running +- **Purpose**: Orchestrates our microservices architecture +- **Source**: `Docker Compose `_ + +Web Technologies +-------------- + +Nginx +~~~~~ +- **Version**: Alpine-based +- **Usage**: Web server and reverse proxy +- **Purpose**: Serves web client and handles APK downloads +- **Image**: nginx:alpine +- **Source**: `Official Nginx `_ + +Development Technologies +--------------------- + +Flutter +~~~~~~ +- **Version**: Stable +- **Usage**: Mobile client development +- **Purpose**: Cross-platform mobile application development +- **Image**: ghcr.io/cirruslabs/flutter:stable +- **Source**: `CirrusLabs Flutter `_ + +Node.js +~~~~~~~ +- **Version**: Latest LTS +- **Usage**: Web client development +- **Purpose**: React application building and serving +- **Image**: node:latest +- **Source**: `Official Node.js `_ + +Development Tools +--------------- + +mkcert +~~~~~~ +- **Purpose**: Local SSL certificate generation +- **Usage**: Development SSL certificates +- **Source**: `FiloSottile/mkcert `_ + +Sphinx +~~~~~~ +- **Purpose**: Documentation generation +- **Version**: Latest +- **Usage**: Project documentation +- **Source**: `Sphinx Documentation `_ + +Package Managers +-------------- + +npm +~~~ +- **Purpose**: Node.js package management +- **Used By**: Web client +- **Source**: `npm `_ + +pub +~~~ +- **Purpose**: Flutter/Dart package management +- **Used By**: Mobile client +- **Source**: `pub.dev `_ + +Version Control +------------- + +Git +~~~ +- **Usage**: Source code management +- **Purpose**: Version control and collaboration +- **Source**: `Git `_ \ No newline at end of file diff --git a/docs/source/project/devops/docker_setup.rst b/docs/source/project/devops/docker_setup.rst new file mode 100644 index 0000000..f8262e4 --- /dev/null +++ b/docs/source/project/devops/docker_setup.rst @@ -0,0 +1,69 @@ +Docker Setup +=========== + +Our project uses Docker and Docker Compose for containerization and orchestration. +The following examples could vary depending on the project's environment variables (ports and endpoints). +Here's an overview of our Docker setup: + +Container Structure +----------------- + +The project consists of several Docker containers: + +- **area-client-web**: React-based web client (Port 8081) +- **area-client-mobile**: Flutter-based mobile client +- **shared volumes**: For APK distribution + +Docker Compose Configuration +-------------------------- + +Our ``docker-compose.yml`` defines the following services: + +Client Web +~~~~~~~~~ +.. code-block:: yaml + + area-client-web: + build: ./client_web + ports: + - "8081:8081" + volumes: + - area-client-data:/usr/share/nginx/html/mobile_builds + +Client Mobile +~~~~~~~~~~~~ +.. code-block:: yaml + + area-client-mobile: + build: ./client_mobile + volumes: + - area-client-data:/app/build/app/outputs/flutter-apk + +Volume Management +--------------- + +We use a shared volume ``area-client-data`` to handle APK distribution between containers: + +.. code-block:: yaml + + volumes: + area-client-data: + +This allows the mobile client's APK to be accessible from the web client for downloads. + +Building and Running +------------------ + +To build and run the project: + +.. code-block:: bash + + make start + +Access Points +----------- + +Locally: + +- Web Client: ``http://localhost:8081`` +- Mobile APK Download: ``http://localhost:8081/client.apk`` \ No newline at end of file diff --git a/docs/source/project/devops/index.rst b/docs/source/project/devops/index.rst new file mode 100644 index 0000000..ffeb93f --- /dev/null +++ b/docs/source/project/devops/index.rst @@ -0,0 +1,12 @@ +DevOps Environment +================ + +This section covers the DevOps setup and infrastructure of the AREA project. + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + docker_setup + dependencies + ci_cd \ No newline at end of file diff --git a/docs/source/Project/index.rst b/docs/source/project/index.rst similarity index 96% rename from docs/source/Project/index.rst rename to docs/source/project/index.rst index 977afd1..d76a820 100644 --- a/docs/source/Project/index.rst +++ b/docs/source/project/index.rst @@ -42,5 +42,6 @@ Contents .. toctree:: :maxdepth: 2 + :caption: Project Overview - benchmark + devops/index diff --git a/docs/source/server/backend.rst b/docs/source/server/backend.rst new file mode 100644 index 0000000..938cf67 --- /dev/null +++ b/docs/source/server/backend.rst @@ -0,0 +1,180 @@ +Backend Structure +================= + +Overview +-------- + +The backend is structured to follow the principles of separation of concerns. Each layer is responsible for a specific task, making the code more modular and easier to maintain. + +Router +------------ + +The router is set up using the `gin-gonic` framework. It includes routes for public and protected endpoints. Middleware (such as authentication) is applied at this level. + +Example: + +.. code-block:: go + + package routes + + import ( + "controllers" + "github.com/gin-gonic/gin" + ) + + func SetupRouter() *gin.Engine { + router := gin.Default() + + // Public routes + public := router.Group("/api") + { + public.POST("/login", controllers.Login) + public.POST("/register", controllers.Register) + } + + // Protected routes (requires authentication) + protected := router.Group("/api") + protected.Use(AuthMiddleware()) + { + protected.GET("/profile", controllers.GetProfile) + protected.POST("/update-profile", controllers.UpdateProfile) + } + + return router + } + +Route Groups +^^^^^^^^^^^^ + +The `gin-gonic` framework allows organizing routes into groups. This improves readability and makes it easier to apply middleware to specific groups of routes. + +1. **Public Routes**: Do not require authentication. These routes are accessible to all users. +2. **Protected Routes**: Require the user to be authenticated. Middleware is used to enforce this requirement. + +Middleware in the Router +^^^^^^^^^^^^^^^^^^^^^^^^ + +Middleware can be applied to route groups or individual routes. For example: + +.. code-block:: go + + protected.Use(AuthMiddleware()) + +This ensures that all routes under the `protected` group require a valid token for access. + +Router Initialization +^^^^^^^^^^^^^^^^^^^^^ + +The router is initialized and run in the `main.go` file: + +.. code-block:: go + + package main + + import ( + "routes" + ) + + func main() { + router := routes.SetupRouter() + router.Run(":8080") // Start the server on port 8080 + } + + + +Controllers +----------- + +Controllers handle incoming HTTP requests and return appropriate responses. They typically: + +- Parse request data (e.g., JSON payloads). +- Call services or business logic layers to perform operations. +- Return HTTP responses to the client. + +Example: + +.. code-block:: go + + func Login(c *gin.Context) { + email := c.PostForm("email") + password := c.PostForm("password") + token, err := authService.Login(email, password) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"}) + return + } + c.JSON(http.StatusOK, gin.H{"token": token}) + } + + +Middleware +---------- + +Middleware is used to handle cross-cutting concerns like authentication, logging, or error handling. Middleware functions are executed before or after controllers handle requests. + +Example Authentication Middleware: + +.. code-block:: go + + func AuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + token := c.GetHeader("Authorization") + if token == "" || !authService.IsValidToken(token) { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return + } + c.Next() + } + } + + +Models +------ + +Models represent the structure of data stored in the MariaDB database. They define the schema for each table and map the database structure to Go objects. Models are managed using the `gorm` library. + +Example User Model: + +.. code-block:: go + + package models + + import "gorm.io/gorm" + + type User struct { + ID uint `gorm:"primaryKey"` + Email string `gorm:"unique;not null"` + Password string `gorm:"not null"` + Token string + } + + func Migrate(db *gorm.DB) { + db.AutoMigrate(&User{}) + } + +Explanation: + +- **ID**: The primary key for the `users` table. +- **Email**: A unique and non-nullable field for storing user emails. +- **Password**: A non-nullable field for storing hashed passwords. +- **Token**: Used for authentication and session management. + +To migrate models to the database, run the migration function during initialization: + +.. code-block:: go + + import ( + "gorm.io/driver/mysql" + "gorm.io/gorm" + "models" + ) + + func InitDB() *gorm.DB { + dsn := "user:password@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local" + db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) + if err != nil { + panic("Failed to connect to the database!") + } + models.Migrate(db) + return db + } diff --git a/docs/source/server/consumer.rst b/docs/source/server/consumer.rst new file mode 100644 index 0000000..9b8f958 --- /dev/null +++ b/docs/source/server/consumer.rst @@ -0,0 +1,83 @@ +RabbitMQ: Example Consumer +========================== + +What is a Consumer? +-------------------- + +A consumer is a service or component that listens to a RabbitMQ queue for incoming messages. Consumers are used to handle tasks asynchronously and process data as it becomes available. + +How Does It Work? +------------------ + +1. **Connection**: The consumer connects to the RabbitMQ server using credentials and the queue name. +2. **Listening**: It listens to the specified queue for messages. +3. **Processing**: Upon receiving a message, it processes the data (e.g., storing it in a database or performing calculations). +4. **Acknowledgment**: Once the processing is done, the message is acknowledged to RabbitMQ. + +How to Create a Consumer? +-------------------------- + +1. Import the RabbitMQ connection library. +2. Define the queue name and configure the connection. +3. Implement a callback function to handle incoming messages. +4. Start the consumer to listen for messages. + +.. code-block:: go + + package main + + import ( + "log" + "github.com/streadway/amqp" + ) + + func main() { + conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/") + if err != nil { + log.Fatalf("Failed to connect to RabbitMQ: %s", err) + } + defer conn.Close() + + ch, err := conn.Channel() + if err != nil { + log.Fatalf("Failed to open a channel: %s", err) + } + defer ch.Close() + + q, err := ch.QueueDeclare( + "example_queue", + false, + false, + false, + false, + nil, + ) + if err != nil { + log.Fatalf("Failed to declare a queue: %s", err) + } + + msgs, err := ch.Consume( + q.Name, + "", + true, + false, + false, + false, + nil, + ) + if err != nil { + log.Fatalf("Failed to register a consumer: %s", err) + } + + forever := make(chan bool) + + go func() { + for d := range msgs { + log.Printf("Received a message: %s", d.Body) + // Process the message here + } + }() + + log.Printf(" [*] Waiting for messages. To exit press CTRL+C") + <-forever + } \ No newline at end of file diff --git a/docs/source/server/deployment.rst b/docs/source/server/deployment.rst new file mode 100644 index 0000000..217ac9d --- /dev/null +++ b/docs/source/server/deployment.rst @@ -0,0 +1,118 @@ +Deployment Documentation +====================== + +This documentation outlines the steps for deploying and managing the project using the provided shell scripts. The deployment utilizes Docker Compose to run RabbitMQ, MariaDB, and the server application. + +Prerequisites +============= + +Before deploying the project, ensure the following are installed on your system: + +- `Docker` +- `Docker Compose` +- `.env` file with the required environment variables (such as `DB_USER`, `DB_PASSWORD`, `DB_NAME`, etc.) + +Deployment Scripts +================== + +Start the Server +---------------- + +To deploy and start all containers (RabbitMQ, MariaDB, and the server), use the `./start_server.sh` script. + +This script does the following: + +1. Loads the `.env` file. +2. Starts the Docker Compose setup defined in `build/docker-compose.yml`. + +**Command:** + +.. code-block:: bash + + ./start_server.sh + +**What Happens:** + +- The RabbitMQ container starts on the configured ports. +- The MariaDB container starts with the credentials and database specified in `.env`. +- The server application is built (if necessary) and started. + +Stop the Server +--------------- + +To stop and remove all running containers, use the `./stop_server.sh` script. + +This script does the following: + +1. Stops all services defined in the Docker Compose file. +2. Optionally cleans up unused containers. + +**Command:** + +.. code-block:: bash + + ./stop_server.sh + +**What Happens:** + +- RabbitMQ, MariaDB, and the server containers are stopped. +- Docker networks and volumes are preserved unless explicitly removed. + +Rebuilding the Server +--------------------- + +If you need to rebuild the server application, you can do so by cleaning up and restarting the Docker Compose setup. + +1. Run the following command to remove existing containers and their build cache: + +.. code-block:: bash + + docker-compose -f build/docker-compose.yml down --rmi all --volumes + +2. Restart the server: + +.. code-block:: bash + + ./start_server.sh + +Environment Variables +===================== + +The `.env` file is essential for the deployment. It should define all required environment variables, such as: + +.. code-block:: text + + DB_HOST= + DB_PORT= + DB_NAME= + DB_USER= + DB_PASSWORD= + SECRET_KEY= + +Ensure the `.env` file is placed in the root directory of the project. + +Docker Compose Services +======================= + +The `build/docker-compose.yml` file defines the following services: + +1. **RabbitMQ**: Message broker for managing asynchronous communication. + - Management console exposed on `http://localhost:15672`. + - Default ports: `15672` (management) and `5672` (message broker). + +2. **MariaDB**: Database service for persistent storage. + - Default port: `3306`. + - Credentials and database name configured in `.env`. + +3. **Server**: The Go-based backend application. + - Default port: `8080`. + +Troubleshooting +=============== + +- **Port Conflicts:** Ensure that the ports specified in the `build/docker-compose.yml` file are not already in use. + Use the following command to list all running containers and their ports: + + .. code-block:: bash + + docker ps diff --git a/docs/source/server/developer.rst b/docs/source/server/developer.rst new file mode 100644 index 0000000..3f598ef --- /dev/null +++ b/docs/source/server/developer.rst @@ -0,0 +1,24 @@ +Project Documentation for Devs +=============================== + +Introduction +------------ + +This documentation is intended for developers working on the project. It explains the core concepts, including the use of RabbitMQ for :doc:`consumer`, the :doc:`backend` structure (controllers, middleware, models, etc.), and how to create and manage consumers. The backend uses MariaDB for data storage. + +Further Reading +--------------- + +- `RabbitMQ Documentation `_ +- `Gin Framework Documentation `_ +- `MariaDB Documentation `_ +- `GORM Documentation `_ + +Contents +-------- + +.. toctree:: + :maxdepth: 5 + + backend + consumer \ No newline at end of file diff --git a/docs/source/server/index.rst b/docs/source/server/index.rst index b9b6daa..971357c 100644 --- a/docs/source/server/index.rst +++ b/docs/source/server/index.rst @@ -22,10 +22,20 @@ Getting Started To get started with the AREA Server, follow the installation and setup instructions in the :doc:`installation` section. +Developer Guide +--------------- +For more details on developing with the AREA Server, refer to the :doc:`developer` section. + +Deployment +---------- +For information on deploying the AREA Server, refer to the :doc:`deployment` section. + Contents -------- .. toctree:: - :maxdepth: 2 + :maxdepth: 5 - installation + installation + developer + deployment \ No newline at end of file diff --git a/docs/source/server/installation.rst b/docs/source/server/installation.rst index 01e6f9a..8c072ba 100644 --- a/docs/source/server/installation.rst +++ b/docs/source/server/installation.rst @@ -16,11 +16,28 @@ Follow these steps to set up and run the AREA Client Web project. .. code-block:: sh cd AREA/server - // TODO: define the command to install the GO packages + go mod tidy -3. Run the project +This command will ensure all required dependencies are downloaded and your go.mod and go.sum files are up to date. + +3. Set Up the Environment Variables +------------------ +.. code-block:: sh + + cp .env.example .env + +Update the .env file with the required environment variables. + +4. Run the project ------------------ .. code-block:: sh go run + +alternatively, you can run the project with the following command: + +.. code-block:: sh + + go build -o area-server main.go + ./area-server diff --git a/example_consumer/.env.example b/example_consumer/.env.example new file mode 100644 index 0000000..14452dc --- /dev/null +++ b/example_consumer/.env.example @@ -0,0 +1,4 @@ +LOG_LEVEL=debug +RMQ_URL=amqp://guest:guest@localhost:5000/ +GOOGLE_CREDENTIAL=your-google-credential +CLIENT_ID=your-client-id \ No newline at end of file diff --git a/example_consumer/consts/const.go b/example_consumer/consts/const.go new file mode 100644 index 0000000..ce21816 --- /dev/null +++ b/example_consumer/consts/const.go @@ -0,0 +1,6 @@ +package consts + +const EnvFile = ".env" +const EnvFileDirectory = "." + +const MessageQueue = "message_queue" diff --git a/example_consumer/go.mod b/example_consumer/go.mod new file mode 100644 index 0000000..52ca476 --- /dev/null +++ b/example_consumer/go.mod @@ -0,0 +1,58 @@ +module consumer + +go 1.23.3 + +require ( + github.com/lakhinsu/rabbitmq-go-example/consumer v0.0.0-20220116173101-cd008c3ff7d7 + github.com/rs/zerolog v1.33.0 + github.com/spf13/viper v1.19.0 +) + +require ( + cloud.google.com/go/auth v0.11.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect + cloud.google.com/go/compute/metadata v0.5.2 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/google/s2a-go v0.1.8 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect + github.com/googleapis/gax-go/v2 v2.14.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/rabbitmq/amqp091-go v1.10.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/streadway/amqp v1.0.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 // indirect + go.opentelemetry.io/otel v1.32.0 // indirect + go.opentelemetry.io/otel/metric v1.32.0 // indirect + go.opentelemetry.io/otel/trace v1.32.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/crypto v0.29.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/net v0.31.0 // indirect + golang.org/x/oauth2 v0.24.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/text v0.20.0 // indirect + google.golang.org/api v0.209.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 // indirect + google.golang.org/grpc v1.68.0 // indirect + google.golang.org/protobuf v1.35.2 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/example_consumer/go.sum b/example_consumer/go.sum new file mode 100644 index 0000000..8daf9c3 --- /dev/null +++ b/example_consumer/go.sum @@ -0,0 +1,240 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= +cloud.google.com/go/auth v0.10.2 h1:oKF7rgBfSHdp/kuhXtqU/tNDr0mZqhYbEh+6SiqzkKo= +cloud.google.com/go/auth v0.10.2/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI= +cloud.google.com/go/auth v0.11.0 h1:Ic5SZz2lsvbYcWT5dfjNWgw6tTlGi2Wc8hyQSC9BstA= +cloud.google.com/go/auth v0.11.0/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI= +cloud.google.com/go/auth/oauth2adapt v0.2.5 h1:2p29+dePqsCHPP1bqDJcKj4qxRyYCcbzKpFyKGt3MTk= +cloud.google.com/go/auth/oauth2adapt v0.2.5/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8= +cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyTuciBG5hFkU= +cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8= +cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg= +cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= +cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= +github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= +github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= +github.com/googleapis/gax-go/v2 v2.14.0 h1:f+jMrjBPl+DL9nI4IQzLUxMq7XrAqFYB7hBPqMNIe8o= +github.com/googleapis/gax-go/v2 v2.14.0/go.mod h1:lhBCnjdLrWRaPvLWhmc8IS24m9mr07qSYnHncrgo+zk= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lakhinsu/rabbitmq-go-example/consumer v0.0.0-20220116173101-cd008c3ff7d7 h1:9UTdbwLm52JbIatGaWsmdyqCLdhWsN8+pzhGgZ5+Bi4= +github.com/lakhinsu/rabbitmq-go-example/consumer v0.0.0-20220116173101-cd008c3ff7d7/go.mod h1:QQcUPx9vkEvxYTrgWaB1oEc8fRqpP47Kr6o0JHCufGM= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= +github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/streadway/amqp v1.0.0 h1:kuuDrUJFZL1QYL9hUNuCxNObNzB0bV/ZG5jV3RWAQgo= +github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= +go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= +go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.209.0 h1:Ja2OXNlyRlWCWu8o+GgI4yUn/wz9h/5ZfFbKz+dQX+w= +google.golang.org/api v0.209.0/go.mod h1:I53S168Yr/PNDNMi5yPnDc0/LGRZO6o7PoEbl/HY3CM= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20241113202542-65e8d215514f h1:zDoHYmMzMacIdjNe+P2XiTmPsLawi/pCbSPfxt6lTfw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f h1:C1QccEa9kUwvMgEUORqQD9S17QesQijxjZ84sO82mfo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 h1:LWZqQOEjDyONlF1H6afSWpAL/znlREo2tHfLoe+8LMA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= +google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/example_consumer/handlers/handlers.go b/example_consumer/handlers/handlers.go new file mode 100644 index 0000000..1eeeb1c --- /dev/null +++ b/example_consumer/handlers/handlers.go @@ -0,0 +1,31 @@ +package handlers + +import ( + "consumer/service" + "consumer/utils" + amqp "github.com/rabbitmq/amqp091-go" + "github.com/rs/zerolog/log" + "strings" +) + +func HandleMessage(queue string, msg amqp.Delivery, err error) { + if err != nil { + log.Err(err).Msg("Error occurred in RMQ consumer") + } + log.Info().Msgf("Message received on '%s' queue: %s", queue, string(msg.Body)) + + if strings.HasPrefix(string(msg.Body), "send email:") { + parts := strings.SplitN(string(msg.Body), ":", 2) + if len(parts) == 2 { + email := strings.TrimSpace(parts[1]) + err := service.SendEmailTo(utils.Token, email) + if err != nil { + log.Err(err).Msg("Error sending email") + } else { + log.Info().Msgf("Email sent to %s", email) + } + } else { + log.Error().Msg("Invalid message format") + } + } +} diff --git a/example_consumer/main.go b/example_consumer/main.go new file mode 100644 index 0000000..4869377 --- /dev/null +++ b/example_consumer/main.go @@ -0,0 +1,33 @@ +package main + +import ( + "consumer/consts" + "consumer/handlers" + "consumer/utils" + "github.com/rs/zerolog/log" + "net/http" +) + +func main() { + http.HandleFunc("/auth/google/login", utils.GetAuthURL) + http.HandleFunc("/auth/google/callback", utils.HandleGoogleCallback) + log.Info().Msg("Server started successfully on port http://localhost:8042") + log.Info().Msg("Login with Google at http://localhost:8042/auth/google/login") + log.Info().Msg("Callback URL: http://localhost:8042/auth/google/callback") + connectionString := utils.GetEnvVar("RMQ_URL") + + messageQueue := utils.RMQConsumer{ + Queue: consts.MessageQueue, + ConnectionString: connectionString, + MsgHandler: handlers.HandleMessage, + } + forever := make(chan bool) + + go messageQueue.Consume() + err := http.ListenAndServe(":8042", nil) + if err != nil { + log.Fatal().Err(err).Msg("Server failed to start") + } + <-forever + +} diff --git a/example_consumer/service/email.go b/example_consumer/service/email.go new file mode 100644 index 0000000..87fae1e --- /dev/null +++ b/example_consumer/service/email.go @@ -0,0 +1,29 @@ +package service + +import ( + "consumer/utils" + "encoding/base64" + "fmt" + "golang.org/x/oauth2" + "google.golang.org/api/gmail/v1" +) + +func SendEmailTo(token *oauth2.Token, toEmail string) error { + service, err := utils.InitGoogleAPI(token, "gmail") + if err != nil { + return err + } + + gmailService := service.(*gmail.Service) + message := []byte(fmt.Sprintf("To: %s\r\nSubject: Test Email\r\n\r\nThis is a test email sent from AREA!", toEmail)) + msg := gmail.Message{ + Raw: base64.URLEncoding.EncodeToString(message), + } + + _, err = gmailService.Users.Messages.Send("me", &msg).Do() + if err != nil { + return err + } + + return nil +} diff --git a/example_consumer/utils/consumer.go b/example_consumer/utils/consumer.go new file mode 100644 index 0000000..24f7467 --- /dev/null +++ b/example_consumer/utils/consumer.go @@ -0,0 +1,59 @@ +package utils + +import ( + amqp "github.com/rabbitmq/amqp091-go" + "github.com/rs/zerolog/log" +) + +type RMQConsumer struct { + Queue string + ConnectionString string + MsgHandler func(queue string, msg amqp.Delivery, err error) +} + +func (x RMQConsumer) OnError(err error, msg string) { + if err != nil { + x.MsgHandler(x.Queue, amqp.Delivery{}, err) + } +} + +func (x RMQConsumer) Consume() { + conn, err := amqp.Dial(x.ConnectionString) + x.OnError(err, "Failed to connect to RabbitMQ") + defer conn.Close() + + ch, err := conn.Channel() + x.OnError(err, "Failed to open a channel") + defer ch.Close() + + q, err := ch.QueueDeclare( + x.Queue, // name + false, // durable + false, // delete when unused + false, // exclusive + false, // no-wait + nil, // arguments + ) + x.OnError(err, "Failed to declare a queue") + + msgs, err := ch.Consume( + q.Name, // queue + "", // consumer + true, // auto-ack + false, // exclusive + false, // no-local + false, // no-wait + nil, // args + ) + x.OnError(err, "Failed to register a consumer") + + forever := make(chan bool) + + go func() { + for d := range msgs { + x.MsgHandler(x.Queue, d, nil) + } + }() + log.Info().Msgf("Started listening for messages on '%s' queue", x.Queue) + <-forever +} diff --git a/example_consumer/utils/googleAuth.go b/example_consumer/utils/googleAuth.go new file mode 100644 index 0000000..10a109d --- /dev/null +++ b/example_consumer/utils/googleAuth.go @@ -0,0 +1,107 @@ +package utils + +import ( + "context" + "encoding/json" + "fmt" + "github.com/lakhinsu/rabbitmq-go-example/consumer/utils" + "github.com/rs/zerolog/log" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + "google.golang.org/api/gmail/v1" + "google.golang.org/api/option" + "google.golang.org/api/people/v1" + "net/http" + "time" +) + +var ( + Token *oauth2.Token + refreshToken string +) + +var config = &oauth2.Config{ + RedirectURL: "http://localhost:8042/auth/google/callback", + ClientID: utils.GetEnvVar("CLIENT_ID"), + ClientSecret: utils.GetEnvVar("GOOGLE_CREDENTIAL"), + Scopes: []string{"https://www.googleapis.com/auth/userinfo.profile", "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/gmail.send"}, + Endpoint: google.Endpoint, +} + +func GetAuthURL(w http.ResponseWriter, r *http.Request) { + url := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline) + http.Redirect(w, r, url, http.StatusTemporaryRedirect) +} + +func RefreshToken() error { + ctx := context.Background() + token := &oauth2.Token{RefreshToken: refreshToken} + newToken, err := config.TokenSource(ctx, token).Token() + if err != nil { + return err + } + Token = newToken + return nil +} + +func ExchangeCodeForToken(code string) (*oauth2.Token, error) { + ctx := context.Background() + token, err := config.Exchange(ctx, code) + if err != nil { + return nil, err + } + Token = token + refreshToken = token.RefreshToken + return token, nil +} + +func InitGoogleAPI(token *oauth2.Token, apiName string) (interface{}, error) { + if Token.Expiry.Before(time.Now()) { + if err := RefreshToken(); err != nil { + return nil, err + } + } + ctx := context.Background() + httpClient := config.Client(ctx, token) + + switch apiName { + case "gmail": + service, err := gmail.NewService(ctx, option.WithHTTPClient(httpClient)) + if err != nil { + return nil, err + } + return service, nil + case "people": + service, err := people.NewService(ctx, option.WithHTTPClient(httpClient)) + if err != nil { + return nil, err + } + return service, nil + default: + return nil, fmt.Errorf("unsupported API: %s", apiName) + } +} + +func HandleGoogleCallback(w http.ResponseWriter, r *http.Request) { + code := r.FormValue("code") + _, err := ExchangeCodeForToken(code) + if err != nil { + http.Error(w, "Failed to exchange token", http.StatusInternalServerError) + return + } + + response, err := http.Get("https://www.googleapis.com/oauth2/v2/userinfo?access_token=" + Token.AccessToken) + if err != nil { + http.Error(w, "Failed to get user info", http.StatusInternalServerError) + return + } + defer response.Body.Close() + + var userInfo map[string]interface{} + err = json.NewDecoder(response.Body).Decode(&userInfo) + if err != nil { + http.Error(w, "Failed to decode user info", http.StatusInternalServerError) + return + } + log.Log().Msgf("User info: %+v", userInfo) +} diff --git a/example_consumer/utils/utils.go b/example_consumer/utils/utils.go new file mode 100644 index 0000000..c861256 --- /dev/null +++ b/example_consumer/utils/utils.go @@ -0,0 +1,27 @@ +package utils + +import ( + "consumer/consts" + "github.com/rs/zerolog/log" + "github.com/spf13/viper" +) + +func init() { + viper.SetConfigFile(consts.EnvFile) + viper.AddConfigPath(consts.EnvFileDirectory) + err := viper.ReadInConfig() + if err != nil { + log.Debug().Err(err). + Msg("Error occurred while reading env file, might fallback to OS env config") + } + viper.AutomaticEnv() +} + +func GetEnvVar(name string) string { + if !viper.IsSet(name) { + log.Debug().Msgf("Environment variable %s is not set", name) + return "" + } + value := viper.GetString(name) + return value +} diff --git a/server/.env.example b/server/.env.example new file mode 100644 index 0000000..510adf3 --- /dev/null +++ b/server/.env.example @@ -0,0 +1,7 @@ +SECRET_KEY= +DB_HOST= +DB_PORT= +DB_NAME= +DB_USER= +DB_PASSWORD= +RMQ_URL= diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..cc75960 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,22 @@ +FROM golang:1.23.3-alpine as builder +WORKDIR /app + +COPY . . +RUN go mod tidy +RUN go mod download +RUN go get -u github.com/swaggo/swag +RUN go get -u github.com/gin-gonic/gin +RUN go build -o main . + + +FROM debian:bullseye-slim + +WORKDIR /app +COPY --from=builder /app/main . +COPY --from=builder /app/.env . +COPY --from=builder /app/config.json . + +RUN chmod +x /app/main + +EXPOSE 8080 +CMD ["./main"] diff --git a/server/build/docker-compose.yml b/server/build/docker-compose.yml new file mode 100644 index 0000000..a3aeff1 --- /dev/null +++ b/server/build/docker-compose.yml @@ -0,0 +1,60 @@ +services: + rabbitmq: + image: rabbitmq:3-management + ports: + - "8082:15672" + - "5000:5673" + networks: + - app_network + volumes: + - ./rabbit-mq/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf + healthcheck: + test: [ "CMD", "rabbitmqctl", "status" ] + interval: 5s + timeout: 15s + retries: 5 + + mariadb: + image: mariadb:10.4 + ports: + - "3306:3306" + environment: + MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} + MYSQL_DATABASE: ${DB_NAME} + volumes: + - mariadb_data:/var/lib/mysql + networks: + - app_network + healthcheck: + test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ] + interval: 10s + timeout: 5s + retries: 5 + + go-app: + build: + context: ../. + dockerfile: Dockerfile + ports: + - "8080:8080" + environment: + DB_HOST: mariadb + DB_PORT: 3306 + DB_NAME: ${DB_NAME} + DB_USER: root + DB_PASSWORD: ${DB_PASSWORD} + SECRET_KEY: ${SECRET_KEY} + depends_on: + mariadb: + condition: service_healthy + rabbitmq: + condition: service_healthy + networks: + - app_network + +volumes: + mariadb_data: + +networks: + app_network: + driver: bridge \ No newline at end of file diff --git a/server/build/rabbit-mq/rabbitmq.conf b/server/build/rabbit-mq/rabbitmq.conf new file mode 100644 index 0000000..3b1a462 --- /dev/null +++ b/server/build/rabbit-mq/rabbitmq.conf @@ -0,0 +1 @@ +listeners.tcp.default = 5673 \ No newline at end of file diff --git a/server/config.json b/server/config.json new file mode 100644 index 0000000..e145ef1 --- /dev/null +++ b/server/config.json @@ -0,0 +1,11 @@ +{ + "app_name": "AREA", + "port": 8080, + "gin_mode": "debug", + "cors": true, + "swagger": true, + "cors_origins": [ + "https://localhost:8081", + "http://localhost:8080" + ] +} diff --git a/server/docs/docs.go b/server/docs/docs.go new file mode 100644 index 0000000..e414698 --- /dev/null +++ b/server/docs/docs.go @@ -0,0 +1,430 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "API Support", + "url": "http://www.swagger.io/support", + "email": "support@example.com" + }, + "license": { + "name": "GPL-3.0", + "url": "https://www.gnu.org/licenses/gpl-3.0.en.html#license-text" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/about.json": { + "get": { + "description": "about", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "about" + ], + "summary": "About", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "msg" + } + } + } + } + }, + "/auth/health": { + "get": { + "description": "Validate the token and return 200 if valid, 401 if expired or invalid", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Check if the JWT is valid", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/auth/login": { + "post": { + "description": "Authenticate a user and return a JWT token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Login a user", + "parameters": [ + { + "description": "Login", + "name": "Login", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/auth/register": { + "post": { + "description": "Create a new user and return a JWT token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Register a user", + "parameters": [ + { + "description": "Register", + "name": "Register", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.RegisterRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "409": { + "description": "Conflict", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/ping": { + "get": { + "description": "ping", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ping" + ], + "summary": "Ping", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/publish/message": { + "post": { + "description": "publish/Message", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "publish/Message" + ], + "summary": "Message", + "parameters": [ + { + "type": "string", + "description": "Message", + "name": "message", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/workflow/create": { + "post": { + "description": "Create a new workflow", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workflow" + ], + "summary": "Create a workflow", + "parameters": [ + { + "description": "workflow", + "name": "workflow", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Workflow" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Workflow" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/workflow/delete/{id}": { + "delete": { + "description": "Delete a workflow by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workflow" + ], + "summary": "Delete a workflow", + "parameters": [ + { + "type": "integer", + "description": "workflow ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/workflow/list": { + "get": { + "description": "List all workflows", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workflow" + ], + "summary": "List workflows", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Workflow" + } + } + } + } + } + } + }, + "definitions": { + "models.LoginRequest": { + "type": "object", + "required": [ + "email", + "password" + ], + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "models.RegisterRequest": { + "type": "object", + "required": [ + "email", + "password", + "username" + ], + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "models.Workflow": { + "type": "object" + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "localhost:8080", + BasePath: "/", + Schemes: []string{}, + Title: "AREA API", + Description: "API documentation for AREA backend", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/server/docs/swagger.json b/server/docs/swagger.json new file mode 100644 index 0000000..6befab3 --- /dev/null +++ b/server/docs/swagger.json @@ -0,0 +1,406 @@ +{ + "swagger": "2.0", + "info": { + "description": "API documentation for AREA backend", + "title": "AREA API", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "API Support", + "url": "http://www.swagger.io/support", + "email": "support@example.com" + }, + "license": { + "name": "GPL-3.0", + "url": "https://www.gnu.org/licenses/gpl-3.0.en.html#license-text" + }, + "version": "1.0" + }, + "host": "localhost:8080", + "basePath": "/", + "paths": { + "/about.json": { + "get": { + "description": "about", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "about" + ], + "summary": "About", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "msg" + } + } + } + } + }, + "/auth/health": { + "get": { + "description": "Validate the token and return 200 if valid, 401 if expired or invalid", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Check if the JWT is valid", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/auth/login": { + "post": { + "description": "Authenticate a user and return a JWT token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Login a user", + "parameters": [ + { + "description": "Login", + "name": "Login", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/auth/register": { + "post": { + "description": "Create a new user and return a JWT token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Register a user", + "parameters": [ + { + "description": "Register", + "name": "Register", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.RegisterRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "409": { + "description": "Conflict", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/ping": { + "get": { + "description": "ping", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ping" + ], + "summary": "Ping", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/publish/message": { + "post": { + "description": "publish/Message", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "publish/Message" + ], + "summary": "Message", + "parameters": [ + { + "type": "string", + "description": "Message", + "name": "message", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/workflow/create": { + "post": { + "description": "Create a new workflow", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workflow" + ], + "summary": "Create a workflow", + "parameters": [ + { + "description": "workflow", + "name": "workflow", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Workflow" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Workflow" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/workflow/delete/{id}": { + "delete": { + "description": "Delete a workflow by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workflow" + ], + "summary": "Delete a workflow", + "parameters": [ + { + "type": "integer", + "description": "workflow ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/workflow/list": { + "get": { + "description": "List all workflows", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workflow" + ], + "summary": "List workflows", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Workflow" + } + } + } + } + } + } + }, + "definitions": { + "models.LoginRequest": { + "type": "object", + "required": [ + "email", + "password" + ], + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "models.RegisterRequest": { + "type": "object", + "required": [ + "email", + "password", + "username" + ], + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "models.Workflow": { + "type": "object" + } + } +} \ No newline at end of file diff --git a/server/docs/swagger.yaml b/server/docs/swagger.yaml new file mode 100644 index 0000000..4c7f76c --- /dev/null +++ b/server/docs/swagger.yaml @@ -0,0 +1,268 @@ +basePath: / +definitions: + models.LoginRequest: + properties: + email: + type: string + password: + type: string + required: + - email + - password + type: object + models.RegisterRequest: + properties: + email: + type: string + password: + type: string + username: + type: string + required: + - email + - password + - username + type: object + models.Workflow: + type: object +host: localhost:8080 +info: + contact: + email: support@example.com + name: API Support + url: http://www.swagger.io/support + description: API documentation for AREA backend + license: + name: GPL-3.0 + url: https://www.gnu.org/licenses/gpl-3.0.en.html#license-text + termsOfService: http://swagger.io/terms/ + title: AREA API + version: "1.0" +paths: + /about.json: + get: + consumes: + - application/json + description: about + produces: + - application/json + responses: + "200": + description: OK + schema: + type: msg + summary: About + tags: + - about + /auth/health: + get: + consumes: + - application/json + description: Validate the token and return 200 if valid, 401 if expired or invalid + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: + type: string + type: object + "401": + description: Unauthorized + schema: + additionalProperties: + type: string + type: object + summary: Check if the JWT is valid + tags: + - auth + /auth/login: + post: + consumes: + - application/json + description: Authenticate a user and return a JWT token + parameters: + - description: Login + in: body + name: Login + required: true + schema: + $ref: '#/definitions/models.LoginRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: + type: string + type: object + "401": + description: Unauthorized + schema: + additionalProperties: + type: string + type: object + summary: Login a user + tags: + - auth + /auth/register: + post: + consumes: + - application/json + description: Create a new user and return a JWT token + parameters: + - description: Register + in: body + name: Register + required: true + schema: + $ref: '#/definitions/models.RegisterRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: + type: string + type: object + "409": + description: Conflict + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + summary: Register a user + tags: + - auth + /ping: + get: + consumes: + - application/json + description: ping + produces: + - application/json + responses: + "200": + description: OK + schema: + type: string + summary: Ping + tags: + - ping + /publish/message: + post: + consumes: + - application/json + description: publish/Message + parameters: + - description: Message + in: formData + name: message + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + type: string + summary: Message + tags: + - publish/Message + /workflow/create: + post: + consumes: + - application/json + description: Create a new workflow + parameters: + - description: workflow + in: body + name: workflow + required: true + schema: + $ref: '#/definitions/models.Workflow' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.Workflow' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + summary: Create a workflow + tags: + - workflow + /workflow/delete/{id}: + delete: + consumes: + - application/json + description: Delete a workflow by ID + parameters: + - description: workflow ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: + type: string + type: object + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "404": + description: Not Found + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + summary: Delete a workflow + tags: + - workflow + /workflow/list: + get: + consumes: + - application/json + description: List all workflows + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.Workflow' + type: array + summary: List workflows + tags: + - workflow +swagger: "2.0" diff --git a/server/go.mod b/server/go.mod new file mode 100644 index 0000000..ba3c3a8 --- /dev/null +++ b/server/go.mod @@ -0,0 +1,81 @@ +module AREA + +require ( + github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/gin-contrib/cors v1.7.2 + github.com/gin-gonic/gin v1.10.0 + github.com/gookit/config/v2 v2.2.5 + github.com/rs/zerolog v1.33.0 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.0 + github.com/swaggo/swag v1.16.4 +) + +require ( + dario.cat/mergo v1.0.1 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/bytedance/sonic v1.12.4 // indirect + github.com/bytedance/sonic/loader v0.2.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.6 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/spec v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.23.0 // indirect + github.com/go-sql-driver/mysql v1.7.0 // indirect + github.com/goccy/go-json v0.10.3 // indirect + github.com/gookit/color v1.5.4 // indirect + github.com/gookit/goutil v0.6.17 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/rabbitmq/amqp091-go v1.10.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.19.0 // indirect + github.com/streadway/amqp v1.1.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/arch v0.12.0 // indirect + golang.org/x/crypto v0.29.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/net v0.31.0 // indirect + golang.org/x/sync v0.9.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/term v0.26.0 // indirect + golang.org/x/text v0.20.0 // indirect + golang.org/x/tools v0.27.0 // indirect + google.golang.org/protobuf v1.35.2 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/driver/mysql v1.5.7 // indirect + gorm.io/gorm v1.25.12 // indirect +) + +go 1.23.3 diff --git a/server/go.sum b/server/go.sum new file mode 100644 index 0000000..52de721 --- /dev/null +++ b/server/go.sum @@ -0,0 +1,231 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/bytedance/sonic v1.12.4 h1:9Csb3c9ZJhfUWeMtpCDCq6BUoH5ogfDFLUgQ/jG+R0k= +github.com/bytedance/sonic v1.12.4/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E= +github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= +github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc= +github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc= +github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw= +github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E= +github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= +github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= +github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= +github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.11.2 h1:joq77SxuyIs9zzxEjgyLBugMQ9NEgTWxXfz2wVqwAaQ= +github.com/goccy/go-yaml v1.11.2/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= +github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= +github.com/gookit/config/v2 v2.2.5 h1:RECbYYbtherywmzn3LNeu9NA5ZqhD7MSKEMsJ7l+MpU= +github.com/gookit/config/v2 v2.2.5/go.mod h1:NeX+yiNYn6Ei10eJvCQFXuHEPIE/IPS8bqaFIsszzaM= +github.com/gookit/goutil v0.6.17 h1:SxmbDz2sn2V+O+xJjJhJT/sq1/kQh6rCJ7vLBiRPZjI= +github.com/gookit/goutil v0.6.17/go.mod h1:rSw1LchE1I3TDWITZvefoAC9tS09SFu3lHXLCV7EaEY= +github.com/gookit/ini/v2 v2.2.3 h1:nSbN+x9OfQPcMObTFP+XuHt8ev6ndv/fWWqxFhPMu2E= +github.com/gookit/ini/v2 v2.2.3/go.mod h1:Vu6p7P7xcfmb8KYu3L0ek8bqu/Im63N81q208SCCZY4= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= +github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/streadway/amqp v1.1.0 h1:py12iX8XSyI7aN/3dUT8DFIDJazNJsVJdxNVEpnQTZM= +github.com/streadway/amqp v1.1.0/go.mod h1:WYSrTEYHOXHd0nwFeUXAe2G2hRnQT+deZJJf88uS9Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= +github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= +github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A= +github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg= +golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= +golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= +golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= +gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/server/internal/config/config.go b/server/internal/config/config.go new file mode 100644 index 0000000..acb5dc6 --- /dev/null +++ b/server/internal/config/config.go @@ -0,0 +1,33 @@ +package config + +import ( + "github.com/gookit/config/v2" + "github.com/gookit/config/v2/json" + "log" +) + +var AppConfig *Config + +type Config struct { + AppName string `mapstructure:"app_name"` + Port int `mapstructure:"port"` + GinMode string `mapstructure:"gin_mode"` + Cors bool `mapstructure:"cors"` + CorsOrigins []string `mapstructure:"cors_origins"` + Swagger bool `mapstructure:"swagger"` +} + +func LoadConfig() { + + config.WithOptions(config.ParseEnv) + config.AddDriver(json.Driver) + + if err := config.LoadFiles("config.json"); err != nil { + log.Fatalf("Error loading config file: %v", err) + } + + AppConfig = &Config{} + if err := config.BindStruct("", AppConfig); err != nil { + log.Fatalf("Error binding config to struct: %v", err) + } +} diff --git a/server/internal/consts/const.go b/server/internal/consts/const.go new file mode 100644 index 0000000..ce21816 --- /dev/null +++ b/server/internal/consts/const.go @@ -0,0 +1,6 @@ +package consts + +const EnvFile = ".env" +const EnvFileDirectory = "." + +const MessageQueue = "message_queue" diff --git a/server/internal/controllers/about.go b/server/internal/controllers/about.go new file mode 100644 index 0000000..d459a94 --- /dev/null +++ b/server/internal/controllers/about.go @@ -0,0 +1,68 @@ +package controllers + +import ( + "github.com/gin-gonic/gin" + "net/http" + "strconv" + "time" +) + +type action struct { + Name string `json:"name"` + Description string `json:"description"` +} + +type reaction struct { + Name string `json:"name"` + Description string `json:"description"` +} + +type service struct { + Name string `json:"name"` + Actions []action `json:"actions"` + Reaction []reaction `json:"reaction"` +} + +func getServiceList() []service { + return []service{ + { + Name: "mail", + Actions: []action{ + { + Name: "send", + Description: "send mail", + }, + }, + Reaction: []reaction{ + { + Name: "receive", + Description: "receive mail", + }, + }, + }, + } +} + +// About godoc +// @Summary About +// @Description about +// @Tags about +// @Accept json +// @Produce json +// @Success 200 {msg} string +// @Router /about.json [get] +func About(c *gin.Context) { + var msg struct { + Client struct { + Host string `json:"host"` + } `json:"client"` + Server struct { + CurrentTime string `json:"current_time"` + Services []service + } `json:"server"` + } + msg.Client.Host = c.ClientIP() + msg.Server.CurrentTime = strconv.FormatInt(time.Now().Unix(), 10) + msg.Server.Services = getServiceList() + c.JSON(http.StatusOK, msg) +} diff --git a/server/internal/controllers/auth.go b/server/internal/controllers/auth.go new file mode 100644 index 0000000..61acd6e --- /dev/null +++ b/server/internal/controllers/auth.go @@ -0,0 +1,104 @@ +package controllers + +import ( + "AREA/internal/models" + db "AREA/internal/pkg" + "AREA/internal/utils" + "github.com/gin-gonic/gin" + "log" + "net/http" +) + +// Login godoc +// @Summary Login a user +// @Description Authenticate a user and return a JWT token +// @Tags auth +// @Accept json +// @Produce json +// @Param Login body models.LoginRequest true "Login" +// @Success 200 {object} map[string]string +// @Failure 401 {object} map[string]string +// @Router /auth/login [post] +func Login(c *gin.Context) { + var LoginData models.LoginRequest + err := c.ShouldBindJSON(&LoginData) + log.Println(LoginData) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + var user models.User + db.DB.Where("email = ?", LoginData.Email).First(&user) + if user.ID == 0 { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"}) + return + } + if err := utils.VerifyPassword(LoginData.Password, user.Password, user.Salt); err != nil { + println(err) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"}) + return + } + tokenString := utils.NewToken(c, LoginData.Email) + db.DB.Model(&user).Update("token", tokenString) + c.JSON(http.StatusOK, gin.H{"jwt": tokenString}) +} + +// Register godoc +// @Summary Register a user +// @Description Create a new user and return a JWT token +// @Tags auth +// @Accept json +// @Produce json +// @Param Register body models.RegisterRequest true "Register" +// @Success 200 {object} map[string]string +// @Failure 409 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Router /auth/register [post] +func Register(c *gin.Context) { + var RegisterData models.RegisterRequest + err := c.ShouldBindJSON(&RegisterData) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + tokenString := utils.NewToken(c, RegisterData.Email) + var user models.User + db.DB.Where("email = ?", RegisterData.Email).First(&user) + if user.ID != 0 { + c.JSON(http.StatusConflict, gin.H{"error": "User already exists"}) + return + } + db.DB.Where("username = ?", RegisterData.Username).First(&user) + if user.ID != 0 { + c.JSON(http.StatusConflict, gin.H{"error": "Username already exists"}) + return + } + password, salt := utils.HashPassword(RegisterData.Password) + db.DB.Create(&models.User{ + Email: RegisterData.Email, + Username: RegisterData.Username, + Password: password, + Salt: salt, + Token: tokenString, + }) + c.JSON(http.StatusOK, gin.H{"username": RegisterData.Username, "email": RegisterData.Email, "jwt": tokenString}) +} + +// Health godoc +// @Summary Check if the JWT is valid +// @Description Validate the token and return 200 if valid, 401 if expired or invalid +// @Tags auth +// @Accept json +// @Produce json +// @Success 200 {object} map[string]string +// @Failure 401 {object} map[string]string +// @Router /auth/health [get] +func Health(c *gin.Context) { + _, err := utils.VerifyToken(c) + + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized: " + err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"status": "healthy"}) +} diff --git a/server/internal/controllers/controllers.go b/server/internal/controllers/controllers.go new file mode 100644 index 0000000..56338ba --- /dev/null +++ b/server/internal/controllers/controllers.go @@ -0,0 +1,20 @@ +package controllers + +import ( + "github.com/gin-gonic/gin" + "net/http" +) + +// Ping godoc +// @Summary Ping +// @Description ping +// @Tags ping +// @Accept json +// @Produce json +// @Success 200 {object} string +// @Router /ping [get] +func Ping(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "message": "pong", + }) +} diff --git a/server/internal/controllers/oauth.go b/server/internal/controllers/oauth.go new file mode 100644 index 0000000..1d1e75d --- /dev/null +++ b/server/internal/controllers/oauth.go @@ -0,0 +1,11 @@ +package controllers + +import ( + "github.com/gin-gonic/gin" +) + +func Google(c *gin.Context) { + c.JSON(200, gin.H{ + "message": "Google", + }) +} diff --git a/server/internal/controllers/rabitmq.go b/server/internal/controllers/rabitmq.go new file mode 100644 index 0000000..b33e623 --- /dev/null +++ b/server/internal/controllers/rabitmq.go @@ -0,0 +1,37 @@ +package controllers + +import ( + "AREA/internal/consts" + "AREA/internal/models" + "AREA/internal/utils" + "github.com/gin-gonic/gin" + "net/http" +) + +// Message godoc +// @Summary Message +// @Description publish/Message +// @Tags publish/Message +// @Accept json +// @Produce json +// @Param message formData string true "Message" +// @Success 200 {object} string +// @Router /publish/message [post] +func Message(c *gin.Context) { + var msg models.Message + message := c.PostForm("message") + msg.Message = message + + connectionString := utils.GetEnvVar("RMQ_URL") + rmqProducer := utils.RMQProducer{ + consts.MessageQueue, + connectionString, + } + + rmqProducer.PublishMessage("text/plain", []byte(msg.Message)) + + c.JSON(http.StatusOK, gin.H{ + "response": "Message received", + }) + +} diff --git a/server/internal/controllers/workflow.go b/server/internal/controllers/workflow.go new file mode 100644 index 0000000..fe6ef79 --- /dev/null +++ b/server/internal/controllers/workflow.go @@ -0,0 +1,85 @@ +package controllers + +import ( + "AREA/internal/models" + "AREA/internal/pkg" + "github.com/gin-gonic/gin" + "net/http" + "strconv" +) + +// WorkflowCreate godoc +// @Summary Create a workflow +// @Description Create a new workflow +// @Tags workflow +// @Accept json +// @Produce json +// @Param workflow body models.Workflow true "workflow" +// @Success 200 {object} models.Workflow +// @Failure 400 {object} map[string]string +// @Router /workflow/create [post] +func WorkflowCreate(c *gin.Context) { + var workflow models.Workflow + err := c.BindJSON(&workflow) + if err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + workflow.UserID, err = pkg.GetUserFromToken(c) + if err != nil { + return + } + pkg.DB.Create(&workflow) + c.JSON(200, gin.H{"workflow": workflow}) +} + +// WorkflowList godoc +// @Summary List workflows +// @Description List all workflows +// @Tags workflow +// @Accept json +// @Produce json +// @Success 200 {object} []models.Workflow +// @Router /workflow/list [get] +func WorkflowList(c *gin.Context) { + var workflows []models.Workflow + userID, err := pkg.GetUserFromToken(c) + if err != nil { + return + } + pkg.DB.Where("user_id = ?", userID).Find(&workflows) + c.JSON(200, gin.H{"workflows": workflows}) +} + +// WorkflowDelete godoc +// @Summary Delete a workflow +// @Description Delete a workflow by ID +// @Tags workflow +// @Accept json +// @Produce json +// @Param id path int true "workflow ID" +// @Success 200 {object} map[string]string +// @Failure 400 {object} map[string]string +// @Failure 404 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Router /workflow/delete/{id} [delete] +func WorkflowDelete(c *gin.Context) { + idParam := c.Param("id") + workflowID, err := strconv.Atoi(idParam) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid workflow ID"}) + return + } + var workflow models.Workflow + result := pkg.DB.First(&workflow, workflowID) + if result.Error != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Workflow not found"}) + return + } + result = pkg.DB.Delete(&workflow) + if result.Error != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete workflow"}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "Workflow deleted successfully"}) +} diff --git a/server/internal/middleware/auth.go b/server/internal/middleware/auth.go new file mode 100644 index 0000000..aba024d --- /dev/null +++ b/server/internal/middleware/auth.go @@ -0,0 +1,38 @@ +package middleware + +import ( + "AREA/internal/models" + db "AREA/internal/pkg" + "AREA/internal/utils" + "github.com/gin-gonic/gin" + "net/http" +) + +func AuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + if isAuthenticated(c) { + c.Next() + return + } + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + } +} + +func isAuthenticated(c *gin.Context) bool { + user := models.User{} + email, err := utils.VerifyToken(c) + if err != nil { + if err.Error() == "Token is expired" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Token is expired"}) + } else { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized: " + err.Error()}) + } + return false + } + db.DB.Where("email = ?", email).First(&user) + if user.ID == 0 { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return false + } + return true +} diff --git a/server/internal/middleware/cors.go b/server/internal/middleware/cors.go new file mode 100644 index 0000000..51ab6e1 --- /dev/null +++ b/server/internal/middleware/cors.go @@ -0,0 +1,15 @@ +package middleware + +import ( + "AREA/internal/config" + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" +) + +func EnableCors() gin.HandlerFunc { + corsConfig := cors.DefaultConfig() + corsConfig.AllowOrigins = config.AppConfig.CorsOrigins + corsConfig.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"} + corsConfig.AllowHeaders = []string{"Origin", "Content-Length", "Content-Type", "Authorization"} + return cors.New(corsConfig) +} diff --git a/server/internal/middleware/error.go b/server/internal/middleware/error.go new file mode 100644 index 0000000..26e18c2 --- /dev/null +++ b/server/internal/middleware/error.go @@ -0,0 +1,16 @@ +package middleware + +import ( + "github.com/gin-gonic/gin" + "net/http" +) + +func ErrorHandlerMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + c.Next() + if len(c.Errors) > 0 { + err := c.Errors.Last() + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + } + } +} diff --git a/server/internal/models/event.go b/server/internal/models/event.go new file mode 100644 index 0000000..b5e9545 --- /dev/null +++ b/server/internal/models/event.go @@ -0,0 +1,20 @@ +package models + +import "gorm.io/gorm" + +type WorkflowStatus string + +type Event struct { + gorm.Model + Name string `json:"name"` + Description string `json:"description"` + ServiceID uint `gorm:"foreignKey:ServiceID" json:"service_id"` + Parameters []Parameters `json:"parameters"` + Type EventType `gorm:"type:enum('action', 'reaction');not null" json:"type"` +} + +const ( + WorkflowStatusPending WorkflowStatus = "pending" + WorkflowStatusProcessed WorkflowStatus = "processed" + WorkflowStatusFailed WorkflowStatus = "failed" +) diff --git a/server/internal/models/msg.go b/server/internal/models/msg.go new file mode 100644 index 0000000..825d9de --- /dev/null +++ b/server/internal/models/msg.go @@ -0,0 +1,5 @@ +package models + +type Message struct { + Message string `json:"message" binding:"required"` +} diff --git a/server/internal/models/requests.go b/server/internal/models/requests.go new file mode 100644 index 0000000..f9bf3c7 --- /dev/null +++ b/server/internal/models/requests.go @@ -0,0 +1,12 @@ +package models + +type LoginRequest struct { + Email string `json:"email" binding:"required"` + Password string `json:"password" binding:"required"` +} + +type RegisterRequest struct { + Email string `json:"email" binding:"required"` + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` +} diff --git a/server/internal/models/service.go b/server/internal/models/service.go new file mode 100644 index 0000000..ac74e0a --- /dev/null +++ b/server/internal/models/service.go @@ -0,0 +1,8 @@ +package models + +import "gorm.io/gorm" + +type Service struct { + gorm.Model + Label string `json:"label"` +} diff --git a/server/internal/models/user.go b/server/internal/models/user.go new file mode 100644 index 0000000..4a6c48e --- /dev/null +++ b/server/internal/models/user.go @@ -0,0 +1,14 @@ +package models + +import ( + "gorm.io/gorm" +) + +type User struct { + gorm.Model + Email string `gorm:"unique;not null" json:"email" binding:"required"` + Username string `gorm:"unique;not null" json:"username" binding:"required"` + Password string `gorm:"not null" json:"password" binding:"required"` + Salt string `gorm:"not null" json:"salt"` + Token string `gorm:"not null" json:"token"` +} diff --git a/server/internal/models/workflow.go b/server/internal/models/workflow.go new file mode 100644 index 0000000..8cdc29c --- /dev/null +++ b/server/internal/models/workflow.go @@ -0,0 +1,36 @@ +package models + +import "gorm.io/gorm" + +type EventType string + +const ( + ActionEventType EventType = "action" + ReactionEventType EventType = "reaction" +) + +type Parameters struct { + gorm.Model + Name string `json:"name"` + Description string `json:"description"` + Type string `json:"type"` + EventID uint `gorm:"foreignKey:EventID" json:"event_id"` +} + +type Workflow struct { + gorm.Model + UserID uint `gorm:"foreignKey:UserID" json:"user_id"` + Name string `json:"name"` + Description string `json:"description"` + Status WorkflowStatus `gorm:"type:enum('pending', 'processed', 'failed')" json:"status"` + IsActive bool `json:"is_active"` + Events []Event `gorm:"many2many:workflow_events" json:"events"` +} + +func (w *Workflow) BeforeCreate(tx *gorm.DB) (err error) { + if w.Status == "" { + w.Status = WorkflowStatusPending + } + w.IsActive = true + return +} diff --git a/server/internal/pkg/db.go b/server/internal/pkg/db.go new file mode 100644 index 0000000..47f6b1e --- /dev/null +++ b/server/internal/pkg/db.go @@ -0,0 +1,46 @@ +package pkg + +import ( + "AREA/internal/models" + "AREA/internal/utils" + "fmt" + "gorm.io/driver/mysql" + "gorm.io/gorm" + "log" +) + +var DB *gorm.DB + +func migrateDB() error { + err := DB.AutoMigrate( + &models.User{}, + &models.Workflow{}, + &models.Event{}, + &models.Parameters{}, + &models.Service{}, + ) + if err != nil { + log.Fatalf("Failed to migrate DB: %v", err) + } + return err +} + +func InitDB() { + dbHost := utils.GetEnvVar("DB_HOST") + dbPort := utils.GetEnvVar("DB_PORT") + dbName := utils.GetEnvVar("DB_NAME") + user := utils.GetEnvVar("DB_USER") + password := utils.GetEnvVar("DB_PASSWORD") + dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true", user, password, dbHost, dbPort, dbName) + err := error(nil) + DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{}) + if err != nil { + log.Fatalf("Failed to connect database: %v", err) + return + } + log.Println("Database connection established") + if migrateDB() != nil { + return + } + log.Println("Migration done") +} diff --git a/server/internal/pkg/request.go b/server/internal/pkg/request.go new file mode 100644 index 0000000..5a326ca --- /dev/null +++ b/server/internal/pkg/request.go @@ -0,0 +1,19 @@ +package pkg + +import ( + "bytes" + "fmt" + "github.com/gin-gonic/gin" + "io" +) + +func PrintRequestJSON(c *gin.Context) { + var requestBody bytes.Buffer + _, err := io.Copy(&requestBody, c.Request.Body) + if err != nil { + c.JSON(500, gin.H{"error": "Failed to read request body"}) + return + } + fmt.Println("Request JSON:", requestBody.String()) + c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody.Bytes())) +} diff --git a/server/internal/pkg/token.go b/server/internal/pkg/token.go new file mode 100644 index 0000000..1767c74 --- /dev/null +++ b/server/internal/pkg/token.go @@ -0,0 +1,21 @@ +package pkg + +import ( + "AREA/internal/models" + "AREA/internal/utils" + "errors" + "github.com/gin-gonic/gin" +) + +func GetUserFromToken(c *gin.Context) (uint, error) { + email, err := utils.VerifyToken(c) + if err != nil { + return 0, err + } + user := models.User{} + DB.Where("email = ?", email).First(&user) + if user.ID == 0 { + return 0, errors.New("User not found") + } + return user.ID, nil +} diff --git a/server/internal/routers/routers.go b/server/internal/routers/routers.go new file mode 100644 index 0000000..2536527 --- /dev/null +++ b/server/internal/routers/routers.go @@ -0,0 +1,69 @@ +package routers + +import ( + _ "AREA/docs" + "AREA/internal/config" + "AREA/internal/controllers" + "AREA/internal/middleware" + "github.com/gin-gonic/gin" + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" +) + +func setUpOauthGroup(router *gin.Engine) { + oauth := router.Group("/oauth") + { + + oauth.POST("/google", controllers.Google) + /*oauth.POST("/spotify", controllers.Spotify) + oauth.POST("/github", controllers.Github) + oauth.POST("/linkedin", controllers.Linkedin) + oauth.POST("/discord", controllers.Discord) + auth.POST("/twitch", controllers.Twitch)*/ + } +} + +func setUpAuthGroup(router *gin.Engine) { + auth := router.Group("/auth") + { + auth.POST("/register", controllers.Register) + auth.POST("/login", controllers.Login) + auth.GET("/health", controllers.Health) + } +} + +func setUpWorkflowGroup(router *gin.Engine) { + workflow := router.Group("/workflow") + workflow.Use(middleware.AuthMiddleware()) + { + workflow.POST("/create", controllers.WorkflowCreate) + workflow.GET("/list", controllers.WorkflowList) + workflow.DELETE("/delete/:id", controllers.WorkflowDelete) + } +} + +func SetupRouter() *gin.Engine { + router := gin.Default() + router.Use(middleware.ErrorHandlerMiddleware()) + if config.AppConfig.Cors { + router.Use(middleware.EnableCors()) + } + if config.AppConfig.Swagger { + router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + } + public := router.Group("/") + { + public.GET("/ping", controllers.Ping) + public.POST("/publish/message", controllers.Message) + } + setUpAuthGroup(router) + setUpOauthGroup(router) + setUpWorkflowGroup(router) + protected := router.Group("/") + protected.Use(middleware.AuthMiddleware()) + { + protected.GET("/about.json", controllers.About) + //protected.GET("/user", controllers.GetUser) + } + return router +} diff --git a/server/internal/utils/auth.go b/server/internal/utils/auth.go new file mode 100644 index 0000000..4850808 --- /dev/null +++ b/server/internal/utils/auth.go @@ -0,0 +1,32 @@ +package utils + +import ( + "crypto/rand" + "encoding/base64" + "errors" + "golang.org/x/crypto/argon2" + "log" +) + +func VerifyPassword(password, hashedPassword, salt string) error { + hash := argon2.IDKey([]byte(password), []byte(salt), 1, 64*1024, 4, 32) + if base64.RawStdEncoding.EncodeToString(hash) != hashedPassword { + return errors.New("invalid password") + } + return nil +} + +func randomSalt() string { + salt := make([]byte, 16) + _, err := rand.Read(salt) + if err != nil { + log.Fatalf("Error occurred while generating random salt: %v", err) + } + return base64.RawStdEncoding.EncodeToString(salt) +} + +func HashPassword(password string) (string, string) { + salt := randomSalt() + hash := argon2.IDKey([]byte(password), []byte(salt), 1, 64*1024, 4, 32) + return base64.RawStdEncoding.EncodeToString(hash), salt +} diff --git a/server/internal/utils/producer.go b/server/internal/utils/producer.go new file mode 100644 index 0000000..debe7df --- /dev/null +++ b/server/internal/utils/producer.go @@ -0,0 +1,48 @@ +package utils + +import ( + amqp "github.com/rabbitmq/amqp091-go" + "github.com/rs/zerolog/log" +) + +type RMQProducer struct { + Queue string + ConnectionString string +} + +func (x RMQProducer) OnError(err error, msg string) { + if err != nil { + log.Err(err).Msgf("Error occurred while publishing message on '%s' queue. Error message: %s", x.Queue, msg) + } +} + +func (x RMQProducer) PublishMessage(contentType string, body []byte) { + conn, err := amqp.Dial(x.ConnectionString) + x.OnError(err, "Failed to connect to RabbitMQ") + defer conn.Close() + + ch, err := conn.Channel() + x.OnError(err, "Failed to open a channel") + defer ch.Close() + + q, err := ch.QueueDeclare( + x.Queue, // name + false, // durable + false, // delete when unused + false, // exclusive + false, // no-wait + nil, // arguments + ) + x.OnError(err, "Failed to declare a queue") + + err = ch.Publish( + "", // exchange + q.Name, // routing key + false, // mandatory + false, // immediate + amqp.Publishing{ + ContentType: contentType, + Body: body, + }) + x.OnError(err, "Failed to publish a message") +} diff --git a/server/internal/utils/token.go b/server/internal/utils/token.go new file mode 100644 index 0000000..d771e9a --- /dev/null +++ b/server/internal/utils/token.go @@ -0,0 +1,58 @@ +package utils + +import ( + "errors" + "github.com/dgrijalva/jwt-go" + "github.com/gin-gonic/gin" + "net/http" + "time" + "strings" +) + +func NewToken(c *gin.Context, email string) string { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "email": email, + "exp": time.Now().Add(time.Hour * 1).Unix(), + }) + tokenString, err := token.SignedString([]byte(GetEnvVar("SECRET_KEY"))) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"}) + } + return tokenString +} + +func VerifyToken(c *gin.Context) (string, error) { + authHeader := c.GetHeader("Authorization") + + if !strings.HasPrefix(authHeader, "Bearer ") { + return "", errors.New("Bearer token is missing") + } + + tokenString := strings.TrimPrefix(authHeader, "Bearer ") + + if tokenString == "" { + return "", errors.New("Authorization token is missing") + } + + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, errors.New("Invalid signing method") + } + return []byte(GetEnvVar("SECRET_KEY")), nil + }) + + if err != nil { + if err.Error() == "Token is expired" { + return "", errors.New("Token is expired") + } + return "", err + } + + claims, ok := token.Claims.(jwt.MapClaims) + if ok && token.Valid { + email := claims["email"].(string) + return email, nil + } + + return "", errors.New("Invalid token") +} diff --git a/server/internal/utils/utils.go b/server/internal/utils/utils.go new file mode 100644 index 0000000..0bf416d --- /dev/null +++ b/server/internal/utils/utils.go @@ -0,0 +1,27 @@ +package utils + +import ( + "AREA/internal/consts" + "github.com/rs/zerolog/log" + "github.com/spf13/viper" +) + +func init() { + viper.SetConfigFile(consts.EnvFile) + viper.AddConfigPath(consts.EnvFileDirectory) + err := viper.ReadInConfig() + if err != nil { + log.Debug().Err(err). + Msg("Error occurred while reading env file, might fallback to OS env config") + } + viper.AutomaticEnv() +} + +func GetEnvVar(name string) string { + if !viper.IsSet(name) { + log.Debug().Msgf("Environment variable %s is not set", name) + return "" + } + value := viper.GetString(name) + return value +} diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..bfb0bef --- /dev/null +++ b/server/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "AREA/internal/config" + "AREA/internal/models" + db "AREA/internal/pkg" + "AREA/internal/routers" + "fmt" + "github.com/gin-gonic/gin" + "log" + "strconv" +) + +// @title AREA API +// @version 1.0 +// @description API documentation for AREA backend +// @termsOfService http://swagger.io/terms/ + +// @contact.name API Support +// @contact.url http://www.swagger.io/support +// @contact.email support@example.com + +// @license.name GPL-3.0 +// @license.url https://www.gnu.org/licenses/gpl-3.0.en.html#license-text + +// @host localhost:8080 +// @BasePath / +func main() { + config.LoadConfig() + db.InitDB() + err := db.DB.AutoMigrate(&models.User{}) + if err != nil { + return + } + gin.SetMode(config.AppConfig.GinMode) + router := routers.SetupRouter() + port := strconv.Itoa(config.AppConfig.Port) + log.Printf("Starting %s on port %s in %s mode", config.AppConfig.AppName, port, config.AppConfig.GinMode) + if err := router.Run(fmt.Sprintf(":%s", port)); err != nil { + log.Fatalf("Failed to start server: %v", err) + } +} diff --git a/server/start_server.sh b/server/start_server.sh new file mode 100755 index 0000000..257b7b0 --- /dev/null +++ b/server/start_server.sh @@ -0,0 +1,12 @@ +#! /bin/bash + +# Start the server +COMPOSE_FILE=build/docker-compose.yml +ENV_FILE=.env + +if command -v docker-compose &> /dev/null +then + docker-compose --env-file $ENV_FILE -f $COMPOSE_FILE -p server up --build -d +else + docker compose --env-file $ENV_FILE -f $COMPOSE_FILE -p server up --build -d +fi diff --git a/server/stop_server.sh b/server/stop_server.sh new file mode 100755 index 0000000..f703b8f --- /dev/null +++ b/server/stop_server.sh @@ -0,0 +1,12 @@ +#! /bin/bash + +# Stop the server +COMPOSE_FILE=build/docker-compose.yml +ENV_FILE=.env + +if command -v docker-compose &> /dev/null +then + docker-compose --env-file $ENV_FILE -f $COMPOSE_FILE down --remove-orphans +else + docker compose --env-file $ENV_FILE -f $COMPOSE_FILE down --remove-orphans +fi