diff --git a/.gitignore b/.gitignore index 3062eea6..b6d2c80e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ deploy/vagrant/.vagrant services/chatbot/db services/certs/ docker-compose.local.yml +*.bak +evidence/member-2/05-vulnerability/testing/*_test.sh +evidence/member-2/05-vulnerability/testing/order_put_request*.sh diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..c5001233 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "crAPI"] + path = crAPI + url = https://github.com/IT24103403/crAPI.git diff --git a/deploy/docker/docker-compose.override.yml b/deploy/docker/docker-compose.override.yml new file mode 100644 index 00000000..a3799945 --- /dev/null +++ b/deploy/docker/docker-compose.override.yml @@ -0,0 +1,12 @@ +version: "3.8" + +services: + crapi-identity: + environment: + - SMTP_HOST=mailhog + - SMTP_PORT=1025 + - SMTP_EMAIL=test@example.com + - SMTP_PASS=dummy + - SMTP_FROM=no-reply@example.com + - SMTP_AUTH=false + - SMTP_STARTTLS=false diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml index 97660701..9bb742f2 100755 --- a/deploy/docker/docker-compose.yml +++ b/deploy/docker/docker-compose.yml @@ -108,7 +108,7 @@ services: crapi-workshop: container_name: crapi-workshop - image: crapi/crapi-workshop:${VERSION:-latest} + image: crapi-workship-fixed:${VERSION:-latest} #ports: # - "${LISTEN_IP:-127.0.0.1}:8000:8000" environment: @@ -226,7 +226,7 @@ services: container_name: crapi-web image: crapi/crapi-web:${VERSION:-latest} ports: - - "${LISTEN_IP:-127.0.0.1}:8888:80" + - "0.0.0.0:8888:80" - "${LISTEN_IP:-127.0.0.1}:30080:80" - "${LISTEN_IP:-127.0.0.1}:8443:443" - "${LISTEN_IP:-127.0.0.1}:30443:443" diff --git a/deploy/docker/docker-compose.yml.backup b/deploy/docker/docker-compose.yml.backup new file mode 100755 index 00000000..07ffddb6 --- /dev/null +++ b/deploy/docker/docker-compose.yml.backup @@ -0,0 +1,360 @@ +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +services: + + crapi-identity: + container_name: crapi-identity + image: crapi/crapi-identity:${VERSION:-latest} + # ports: + # - "${LISTEN_IP:-127.0.0.1}:8080:8080" + volumes: + - ./keys:/app/keys + environment: + - LOG_LEVEL=${LOG_LEVEL:-INFO} + - DB_NAME=crapi + - DB_USER=admin + - DB_PASSWORD=crapisecretpassword + - DB_HOST=postgresdb + - DB_PORT=5432 + - SERVER_PORT=${IDENTITY_SERVER_PORT:-8080} + - ENABLE_SHELL_INJECTION=${ENABLE_SHELL_INJECTION:-false} + - JWT_SECRET=crapi + - MAILHOG_HOST=mailhog + - MAILHOG_PORT=1025 + - MAILHOG_DOMAIN=example.com + - SMTP_HOST=smtp.example.com + - SMTP_PORT=587 + - SMTP_EMAIL=user@example.com + - SMTP_PASS=xxxxxxxxxxxxxx + - SMTP_FROM=no-reply@example.com + - SMTP_AUTH=true + - SMTP_STARTTLS=true + - JWT_EXPIRATION=604800000 + - ENABLE_LOG4J=${ENABLE_LOG4J:-false} + - API_GATEWAY_URL=https://api.mypremiumdealership.com + - TLS_ENABLED=${TLS_ENABLED:-false} + - TLS_KEYSTORE_TYPE=PKCS12 + - TLS_KEYSTORE=classpath:certs/server.p12 + - TLS_KEYSTORE_PASSWORD=passw0rd + - TLS_KEY_PASSWORD=passw0rd + - TLS_KEY_ALIAS=identity + depends_on: + postgresdb: + condition: service_healthy + mongodb: + condition: service_healthy + mailhog: + condition: service_healthy + healthcheck: + test: /app/health.sh + interval: 15s + timeout: 15s + retries: 15 + deploy: + resources: + limits: + cpus: '0.8' + memory: 384M + + crapi-community: + container_name: crapi-community + image: crapi/crapi-community:${VERSION:-latest} + #ports: + # - "${LISTEN_IP:-127.0.0.1}:8087:8087" + environment: + - LOG_LEVEL=${LOG_LEVEL:-INFO} + - IDENTITY_SERVICE=crapi-identity:${IDENTITY_SERVER_PORT:-8080} + - DB_NAME=crapi + - DB_USER=admin + - DB_PASSWORD=crapisecretpassword + - DB_HOST=postgresdb + - DB_PORT=5432 + - SERVER_PORT=${COMMUNITY_SERVER_PORT:-8087} + - MONGO_DB_HOST=mongodb + - MONGO_DB_PORT=27017 + - MONGO_DB_USER=admin + - MONGO_DB_PASSWORD=crapisecretpassword + - MONGO_DB_NAME=crapi + - TLS_ENABLED=${TLS_ENABLED:-false} + - TLS_CERTIFICATE=certs/server.crt + - TLS_KEY=certs/server.key + depends_on: + postgresdb: + condition: service_healthy + mongodb: + condition: service_healthy + crapi-identity: + condition: service_healthy + healthcheck: + test: /app/health.sh + interval: 15s + timeout: 15s + retries: 15 + deploy: + resources: + limits: + cpus: '0.3' + memory: 192M + + crapi-workshop: + container_name: crapi-workshop + image: crapi/crapi-workshop:${VERSION:-latest} + #ports: + # - "${LISTEN_IP:-127.0.0.1}:8000:8000" + environment: + - LOG_LEVEL=${LOG_LEVEL:-INFO} + - IDENTITY_SERVICE=crapi-identity:${IDENTITY_SERVER_PORT:-8080} + - DB_NAME=crapi + - DB_USER=admin + - DB_PASSWORD=crapisecretpassword + - DB_HOST=postgresdb + - DB_PORT=5432 + - SERVER_PORT=${WORKSHOP_SERVER_PORT:-8000} + - MONGO_DB_HOST=mongodb + - MONGO_DB_PORT=27017 + - MONGO_DB_USER=admin + - MONGO_DB_PASSWORD=crapisecretpassword + - MONGO_DB_NAME=crapi + - SECRET_KEY=crapi + - API_GATEWAY_URL=https://api.mypremiumdealership.com + - TLS_ENABLED=${TLS_ENABLED:-false} + - TLS_CERTIFICATE=certs/server.crt + - TLS_KEY=certs/server.key + - FILES_LIMIT=1000 + # Gunicorn configuration for better performance under load + - GUNICORN_WORKERS=${GUNICORN_WORKERS:-4} + - GUNICORN_TIMEOUT=${GUNICORN_TIMEOUT:-120} + - GUNICORN_MAX_REQUESTS=${GUNICORN_MAX_REQUESTS:-1000} + - GUNICORN_MAX_REQUESTS_JITTER=${GUNICORN_MAX_REQUESTS_JITTER:-50} + # Database connection pooling (10 minutes) + - DB_CONN_MAX_AGE=${DB_CONN_MAX_AGE:-600} + depends_on: + postgresdb: + condition: service_healthy + mongodb: + condition: service_healthy + crapi-identity: + condition: service_healthy + crapi-community: + condition: service_healthy + healthcheck: + test: /app/health.sh + interval: 15s + timeout: 15s + retries: 15 + deploy: + resources: + limits: + cpus: '1.0' + memory: 512M + + crapi-chatbot: + container_name: crapi-chatbot + image: crapi/crapi-chatbot:${VERSION:-latest} + ports: + - "${LISTEN_IP:-127.0.0.1}:5500:5500" # MCP server + environment: + - TLS_ENABLED=${TLS_ENABLED:-false} + - SERVER_PORT=${CHATBOT_SERVER_PORT:-5002} + - WEB_SERVICE=crapi-web + - IDENTITY_SERVICE=crapi-identity:${IDENTITY_SERVER_PORT:-8080} + - DB_NAME=crapi + - DB_USER=admin + - DB_PASSWORD=crapisecretpassword + - DB_HOST=postgresdb + - DB_PORT=5432 + - MONGO_DB_HOST=mongodb + - MONGO_DB_PORT=27017 + - MONGO_DB_USER=admin + - MONGO_DB_PASSWORD=crapisecretpassword + - MONGO_DB_NAME=crapi + - API_USER=admin@example.com + - API_PASSWORD=Admin!123 + - OPENAPI_SPEC=/app/resources/crapi-openapi-spec.json + - CHATBOT_LIFE=${CHATBOT_LIFE:-1} + - CHATBOT_LLM_PROVIDER=${CHATBOT_LLM_PROVIDER:-openai} + - CHATBOT_LLM_MODEL=${CHATBOT_LLM_MODEL:-} + - CHATBOT_EMBEDDINGS_MODEL=${CHATBOT_EMBEDDINGS_MODEL:-} + - CHATBOT_EMBEDDINGS_DIMENSIONS=${CHATBOT_EMBEDDINGS_DIMENSIONS:-1536} + - CHATBOT_OPENAI_API_KEY=${CHATBOT_OPENAI_API_KEY:-} + - CHATBOT_OPENAI_BASE_URL=${CHATBOT_OPENAI_BASE_URL:-} + - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-} + - AZURE_OPENAI_API_KEY=${AZURE_OPENAI_API_KEY:-} + - AZURE_AD_TOKEN=${AZURE_AD_TOKEN:-} + - AZURE_OPENAI_ENDPOINT=${AZURE_OPENAI_ENDPOINT:-} + - AZURE_OPENAI_API_VERSION=${AZURE_OPENAI_API_VERSION:-2024-02-15-preview} + - AZURE_OPENAI_CHAT_DEPLOYMENT=${AZURE_OPENAI_CHAT_DEPLOYMENT:-} + - AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT=${AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT:-} + - GROQ_API_KEY=${GROQ_API_KEY:-} + - MISTRAL_API_KEY=${MISTRAL_API_KEY:-} + - COHERE_API_KEY=${COHERE_API_KEY:-} + - AWS_BEARER_TOKEN_BEDROCK=${AWS_BEARER_TOKEN_BEDROCK:-} + - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-} + - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-} + - AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN:-} + - AWS_REGION=${AWS_REGION:-} + - AWS_ASSUME_ROLE_ARN=${AWS_ASSUME_ROLE_ARN:-} + - AWS_EXTERNAL_ID=${AWS_EXTERNAL_ID:-} + - AWS_ROLE_SESSION_NAME=${AWS_ROLE_SESSION_NAME:-crapi-chatbot-session} + - GOOGLE_APPLICATION_CREDENTIALS=${GOOGLE_APPLICATION_CREDENTIALS:-} + - VERTEX_PROJECT=${VERTEX_PROJECT:-} + - VERTEX_LOCATION=${VERTEX_LOCATION:-} + - MAX_CONTENT_LENGTH=50000 + - CHROMA_HOST=chromadb + - CHROMA_PORT=8000 + depends_on: + mongodb: + condition: service_healthy + crapi-identity: + condition: service_healthy + chromadb: + condition: service_healthy + # ports: + # - "${LISTEN_IP:-127.0.0.1}:5002:5002" + + crapi-web: + container_name: crapi-web + image: crapi/crapi-web:${VERSION:-latest} + ports: + - "0.0.0.0:8888:80" + - "${LISTEN_IP:-127.0.0.1}:30080:80" + - "${LISTEN_IP:-127.0.0.1}:8443:443" + - "${LISTEN_IP:-127.0.0.1}:30443:443" + environment: + - COMMUNITY_SERVICE=crapi-community:${COMMUNITY_SERVER_PORT:-8087} + - IDENTITY_SERVICE=crapi-identity:${IDENTITY_SERVER_PORT:-8080} + - WORKSHOP_SERVICE=crapi-workshop:${WORKSHOP_SERVER_PORT:-8000} + - CHATBOT_SERVICE=crapi-chatbot:${CHATBOT_SERVER_PORT:-5002} + - MAILHOG_WEB_SERVICE=mailhog:8025 + - TLS_ENABLED=${TLS_ENABLED:-false} + depends_on: + crapi-community: + condition: service_healthy + crapi-identity: + condition: service_healthy + crapi-workshop: + condition: service_healthy + healthcheck: + test: curl 0.0.0.0:80/health + interval: 15s + timeout: 15s + retries: 15 + deploy: + resources: + limits: + cpus: '0.3' + memory: 128M + + postgresdb: + container_name: postgresdb + image: 'postgres:14' + command: ["postgres", "-c", "max_connections=500"] + environment: + POSTGRES_USER: admin + POSTGRES_PASSWORD: crapisecretpassword + POSTGRES_DB: crapi + #ports: + # - "${LISTEN_IP:-127.0.0.1}:5432:5432" + healthcheck: + test: [ "CMD-SHELL", "pg_isready" ] + interval: 15s + timeout: 15s + retries: 15 + volumes: + - postgresql-data:/var/lib/postgresql/data/ + deploy: + resources: + limits: + cpus: '0.5' + memory: 256M + + mongodb: + container_name: mongodb + image: 'mongo:4.4' + environment: + MONGO_INITDB_ROOT_USERNAME: admin + MONGO_INITDB_ROOT_PASSWORD: crapisecretpassword + #ports: + # - "${LISTEN_IP:-127.0.0.1}:27017:27017" + healthcheck: + test: echo 'db.runCommand("ping").ok' | mongo mongodb:27017/test --quiet + interval: 15s + timeout: 15s + retries: 15 + start_period: 20s + volumes: + - mongodb-data:/data/db + deploy: + resources: + limits: + cpus: '0.3' + memory: 128M + + chromadb: + container_name: chromadb + image: 'chromadb/chroma:latest' + environment: + IS_PERSISTENT: 'TRUE' + healthcheck: + test: [ "CMD", "/bin/bash", "-c", "cat < /dev/null > /dev/tcp/localhost/8000" ] + interval: 15s + timeout: 15s + retries: 15 + start_period: 20s + volumes: + - chromadb-data:/data + # ports: + # - "${LISTEN_IP:-127.0.0.1}:8000:8000" + + mailhog: + user: root + container_name: mailhog + image: crapi/mailhog:${VERSION:-latest} + environment: + MH_MONGO_URI: admin:crapisecretpassword@mongodb:27017 + MH_STORAGE: mongodb + ports: + # - "127.0.0.1:1025:1025" # smtp server + - "${LISTEN_IP:-127.0.0.1}:8025:8025" # Mail ui + healthcheck: + test: [ "CMD", "nc", "-z", "localhost", "8025" ] + interval: 15s + timeout: 15s + retries: 15 + deploy: + resources: + limits: + cpus: '0.3' + memory: 128M + + api.mypremiumdealership.com: + container_name: api.mypremiumdealership.com + image: crapi/gateway-service:${VERSION:-latest} + #ports: + # - "${LISTEN_IP:-127.0.0.1}:8443:443" # https + healthcheck: + test: bash -c 'echo -n "GET / HTTP/1.1\n\n" > /dev/tcp/127.0.0.1/443' + interval: 15s + timeout: 15s + retries: 15 + start_period: 15s + deploy: + resources: + limits: + cpus: '0.1' + memory: 50M + +volumes: + mongodb-data: + postgresql-data: + chromadb-data: diff --git a/deploy/docker/frames/frame_000.png b/deploy/docker/frames/frame_000.png new file mode 100644 index 00000000..ecc9ec5d Binary files /dev/null and b/deploy/docker/frames/frame_000.png differ diff --git a/deploy/docker/frames/frame_001.png b/deploy/docker/frames/frame_001.png new file mode 100644 index 00000000..9201f572 Binary files /dev/null and b/deploy/docker/frames/frame_001.png differ diff --git a/deploy/docker/frames/frame_002.png b/deploy/docker/frames/frame_002.png new file mode 100644 index 00000000..1af5ed72 Binary files /dev/null and b/deploy/docker/frames/frame_002.png differ diff --git a/deploy/docker/frames/frame_003.png b/deploy/docker/frames/frame_003.png new file mode 100644 index 00000000..def76a09 Binary files /dev/null and b/deploy/docker/frames/frame_003.png differ diff --git a/deploy/docker/frames/frame_004.png b/deploy/docker/frames/frame_004.png new file mode 100644 index 00000000..e33761c9 Binary files /dev/null and b/deploy/docker/frames/frame_004.png differ diff --git a/deploy/docker/frames/frame_005.png b/deploy/docker/frames/frame_005.png new file mode 100644 index 00000000..3e10cb88 Binary files /dev/null and b/deploy/docker/frames/frame_005.png differ diff --git a/deploy/docker/frames/frame_006.png b/deploy/docker/frames/frame_006.png new file mode 100644 index 00000000..451a5e11 Binary files /dev/null and b/deploy/docker/frames/frame_006.png differ diff --git a/deploy/docker/frames/frame_007.png b/deploy/docker/frames/frame_007.png new file mode 100644 index 00000000..b29f7024 Binary files /dev/null and b/deploy/docker/frames/frame_007.png differ diff --git a/deploy/docker/frames/frame_008.png b/deploy/docker/frames/frame_008.png new file mode 100644 index 00000000..b163bd8d Binary files /dev/null and b/deploy/docker/frames/frame_008.png differ diff --git a/deploy/docker/frames/frame_009.png b/deploy/docker/frames/frame_009.png new file mode 100644 index 00000000..88523043 Binary files /dev/null and b/deploy/docker/frames/frame_009.png differ diff --git a/deploy/docker/frames/frame_010.png b/deploy/docker/frames/frame_010.png new file mode 100644 index 00000000..49a8e8c0 Binary files /dev/null and b/deploy/docker/frames/frame_010.png differ diff --git a/deploy/docker/frames/frame_011.png b/deploy/docker/frames/frame_011.png new file mode 100644 index 00000000..2a34e8ab Binary files /dev/null and b/deploy/docker/frames/frame_011.png differ diff --git a/deploy/docker/frames/frame_012.png b/deploy/docker/frames/frame_012.png new file mode 100644 index 00000000..3bb32113 Binary files /dev/null and b/deploy/docker/frames/frame_012.png differ diff --git a/deploy/docker/frames/frame_013.png b/deploy/docker/frames/frame_013.png new file mode 100644 index 00000000..ba6296e9 Binary files /dev/null and b/deploy/docker/frames/frame_013.png differ diff --git a/deploy/docker/frames/frame_014.png b/deploy/docker/frames/frame_014.png new file mode 100644 index 00000000..c8802d63 Binary files /dev/null and b/deploy/docker/frames/frame_014.png differ diff --git a/deploy/docker/frames/frame_015.png b/deploy/docker/frames/frame_015.png new file mode 100644 index 00000000..86eefd0f Binary files /dev/null and b/deploy/docker/frames/frame_015.png differ diff --git a/deploy/docker/frames/frame_016.png b/deploy/docker/frames/frame_016.png new file mode 100644 index 00000000..c3e3e7cc Binary files /dev/null and b/deploy/docker/frames/frame_016.png differ diff --git a/deploy/docker/frames/frame_017.png b/deploy/docker/frames/frame_017.png new file mode 100644 index 00000000..8a213f86 Binary files /dev/null and b/deploy/docker/frames/frame_017.png differ diff --git a/deploy/docker/frames/frame_018.png b/deploy/docker/frames/frame_018.png new file mode 100644 index 00000000..e9e47ec4 Binary files /dev/null and b/deploy/docker/frames/frame_018.png differ diff --git a/deploy/docker/frames/frame_019.png b/deploy/docker/frames/frame_019.png new file mode 100644 index 00000000..0632b255 Binary files /dev/null and b/deploy/docker/frames/frame_019.png differ diff --git a/deploy/docker/frames/frame_020.png b/deploy/docker/frames/frame_020.png new file mode 100644 index 00000000..57422a81 Binary files /dev/null and b/deploy/docker/frames/frame_020.png differ diff --git a/deploy/docker/frames/frame_020_enhanced.png b/deploy/docker/frames/frame_020_enhanced.png new file mode 100644 index 00000000..f7ae0deb Binary files /dev/null and b/deploy/docker/frames/frame_020_enhanced.png differ diff --git a/deploy/docker/frames/frame_020_fixed.png b/deploy/docker/frames/frame_020_fixed.png new file mode 100644 index 00000000..82410730 Binary files /dev/null and b/deploy/docker/frames/frame_020_fixed.png differ diff --git a/deploy/docker/frames/frame_021.png b/deploy/docker/frames/frame_021.png new file mode 100644 index 00000000..2323d5dc Binary files /dev/null and b/deploy/docker/frames/frame_021.png differ diff --git a/deploy/docker/frames/frame_022.png b/deploy/docker/frames/frame_022.png new file mode 100644 index 00000000..1f81da6a Binary files /dev/null and b/deploy/docker/frames/frame_022.png differ diff --git a/deploy/docker/frames/frame_023.png b/deploy/docker/frames/frame_023.png new file mode 100644 index 00000000..14e89846 Binary files /dev/null and b/deploy/docker/frames/frame_023.png differ diff --git a/deploy/docker/frames/frame_024.png b/deploy/docker/frames/frame_024.png new file mode 100644 index 00000000..c9a21f95 Binary files /dev/null and b/deploy/docker/frames/frame_024.png differ diff --git a/deploy/docker/frames/frame_025.png b/deploy/docker/frames/frame_025.png new file mode 100644 index 00000000..0a15c38b Binary files /dev/null and b/deploy/docker/frames/frame_025.png differ diff --git a/deploy/docker/frames/frame_026.png b/deploy/docker/frames/frame_026.png new file mode 100644 index 00000000..be16401c Binary files /dev/null and b/deploy/docker/frames/frame_026.png differ diff --git a/deploy/docker/frames/frame_027.png b/deploy/docker/frames/frame_027.png new file mode 100644 index 00000000..9a264f51 Binary files /dev/null and b/deploy/docker/frames/frame_027.png differ diff --git a/deploy/docker/frames/frame_028.png b/deploy/docker/frames/frame_028.png new file mode 100644 index 00000000..a4e51320 Binary files /dev/null and b/deploy/docker/frames/frame_028.png differ diff --git a/deploy/docker/frames/frame_029.png b/deploy/docker/frames/frame_029.png new file mode 100644 index 00000000..ae526acc Binary files /dev/null and b/deploy/docker/frames/frame_029.png differ diff --git a/deploy/docker/frames/frame_030.png b/deploy/docker/frames/frame_030.png new file mode 100644 index 00000000..bff1fabe Binary files /dev/null and b/deploy/docker/frames/frame_030.png differ diff --git a/deploy/docker/frames/frame_031.png b/deploy/docker/frames/frame_031.png new file mode 100644 index 00000000..5e5afa58 Binary files /dev/null and b/deploy/docker/frames/frame_031.png differ diff --git a/deploy/docker/frames/frame_032.png b/deploy/docker/frames/frame_032.png new file mode 100644 index 00000000..1de8f48a Binary files /dev/null and b/deploy/docker/frames/frame_032.png differ diff --git a/deploy/docker/frames/frame_033.png b/deploy/docker/frames/frame_033.png new file mode 100644 index 00000000..132abc2e Binary files /dev/null and b/deploy/docker/frames/frame_033.png differ diff --git a/deploy/docker/frames/frame_034.png b/deploy/docker/frames/frame_034.png new file mode 100644 index 00000000..d98fadff Binary files /dev/null and b/deploy/docker/frames/frame_034.png differ diff --git a/deploy/docker/frames/frame_035.png b/deploy/docker/frames/frame_035.png new file mode 100644 index 00000000..b8740bbe Binary files /dev/null and b/deploy/docker/frames/frame_035.png differ diff --git a/deploy/docker/frames/frame_036.png b/deploy/docker/frames/frame_036.png new file mode 100644 index 00000000..19d8065c Binary files /dev/null and b/deploy/docker/frames/frame_036.png differ diff --git a/deploy/docker/frames/frame_037.png b/deploy/docker/frames/frame_037.png new file mode 100644 index 00000000..11e22d83 Binary files /dev/null and b/deploy/docker/frames/frame_037.png differ diff --git a/deploy/docker/frames/frame_038.png b/deploy/docker/frames/frame_038.png new file mode 100644 index 00000000..8b9bcad7 Binary files /dev/null and b/deploy/docker/frames/frame_038.png differ diff --git a/deploy/docker/frames/frame_039.png b/deploy/docker/frames/frame_039.png new file mode 100644 index 00000000..7321deca Binary files /dev/null and b/deploy/docker/frames/frame_039.png differ diff --git a/deploy/docker/frames/frame_040.png b/deploy/docker/frames/frame_040.png new file mode 100644 index 00000000..c8dca413 Binary files /dev/null and b/deploy/docker/frames/frame_040.png differ diff --git a/deploy/docker/frames/frame_041.png b/deploy/docker/frames/frame_041.png new file mode 100644 index 00000000..099b26a9 Binary files /dev/null and b/deploy/docker/frames/frame_041.png differ diff --git a/deploy/docker/qr1.png b/deploy/docker/qr1.png new file mode 100644 index 00000000..102e14fd Binary files /dev/null and b/deploy/docker/qr1.png differ diff --git a/deploy/docker/qr2.png b/deploy/docker/qr2.png new file mode 100644 index 00000000..102e14fd Binary files /dev/null and b/deploy/docker/qr2.png differ diff --git a/deploy/docker/qr3.png b/deploy/docker/qr3.png new file mode 100644 index 00000000..102e14fd Binary files /dev/null and b/deploy/docker/qr3.png differ diff --git a/deploy/docker/response.bin b/deploy/docker/response.bin new file mode 100644 index 00000000..5d87d854 Binary files /dev/null and b/deploy/docker/response.bin differ diff --git a/evidence/member-4/04-vulnerability/notes.md b/evidence/member-4/04-vulnerability/notes.md new file mode 100644 index 00000000..9fbf1c06 --- /dev/null +++ b/evidence/member-4/04-vulnerability/notes.md @@ -0,0 +1,19 @@ +# Vulnerability 4 Investigation + +Objective: +Identify and assess a server-side request or unsafe API interaction weakness within the OWASP crAPI application. + +Investigation Method: +- Source code review +- OWASP ZAP findings review +- API interaction analysis +- Trust boundary analysis + +Evidence: +- Source analysis screenshots +- Request/response testing +- Root cause identification +- Remediation validation + +Status: +Under investigation. diff --git a/services/workshop/crapi/shop/views.py b/services/workshop/crapi/shop/views.py index 9fd30a96..71554b01 100644 --- a/services/workshop/crapi/shop/views.py +++ b/services/workshop/crapi/shop/views.py @@ -246,13 +246,11 @@ def put(self, request, order_id=None, user=None): {"message": messages.INVALID_STATUS}, status=status.HTTP_400_BAD_REQUEST ) user_details = UserDetails.objects.get(user=order.user) - if "status" in request_data and request_data["status"] != order.status: - order.status = request_data["status"] - if request_data["status"] == Order.STATUS_CHOICES.RETURNED.value: - user_details.available_credit += float( - order.quantity * order.product.price - ) - user_details.save() + if "status" in request_data: + return Response( + {"message": "Direct status modification is not allowed."}, + status=status.HTTP_403_FORBIDDEN, + ) order.save() serializer = OrderSerializer(order) response_data = dict(orders=serializer.data) diff --git a/services/workshop/crapi/shop/views.py.bak b/services/workshop/crapi/shop/views.py.bak new file mode 100644 index 00000000..9fd30a96 --- /dev/null +++ b/services/workshop/crapi/shop/views.py.bak @@ -0,0 +1,433 @@ +# +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +""" +contains views related to Shop APIs +""" +import logging +import uuid +from django.db import connection +from django.utils import timezone +from django.http import FileResponse +from django.urls import reverse +import requests +from crapi_site import settings +from rest_framework import status +from rest_framework.response import Response +from rest_framework.views import APIView +from utils.helper import basic_auth +from crapi.shop.serializers import ( + OrderSerializer, + ProductSerializer, + CouponSerializer, + ProductQuantitySerializer, +) +from crapi.user.serializers import UserSerializer +from utils.jwt import jwt_auth_required +from utils import messages +from crapi.shop.models import Order, Product, AppliedCoupon, Coupon +from crapi.user.models import UserDetails +from utils.logging import log_error +from django.core.exceptions import ObjectDoesNotExist +from rest_framework.pagination import LimitOffsetPagination + + +class ProductView(APIView, LimitOffsetPagination): + """ + Product Controller View + """ + + @jwt_auth_required + def get(self, request, user): + """ + products view for fetching the list of products + :param request: http request for the view + method allowed: GET + http request should be authorised by the jwt token of the user + :param user: User object of the requesting user + :returns Response object with + products list and 200 status if no error + message and corresponding status if error + """ + user_details = UserDetails.objects.get(user=user) + products = Product.objects.all().order_by("-id") + paginated = self.paginate_queryset(products, request, view=self) + serializer = ProductSerializer(paginated, many=True) + response_data = dict( + products=serializer.data, + credit=user_details.available_credit, + next_offset=( + self.offset + self.limit + if self.offset + self.limit < self.count + else None + ), + previous_offset=( + self.offset - self.limit if self.offset - self.limit >= 0 else None + ), + count=self.get_count(paginated), + ) + return Response(response_data, status=status.HTTP_200_OK) + + @jwt_auth_required + def post(self, request, user): + """ + products view for adding a new product + :param request: http request for the view + method allowed: POST + http request should be authorised by the jwt token of the user + mandatory fields for POST and PUT http methods: ['name', 'price', 'image_url'] + :param user: User object of the requesting user + :returns Response object with + products list and 200 status if no error + message and corresponding status if error + """ + user_request_body = request.data + serializer = ProductSerializer(data=user_request_body) + if not serializer.is_valid(): + log_error(request.path, request.data, 400, serializer.errors) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + + +class OrderControlView(APIView): + """ + Order Controller View + """ + + def get(self, request, order_id=None, user=None): + """ + order view for fetching a particular order + :param request: http request for the view + method allowed: GET + http request should be authorised by the jwt token of the user + :param order_id: + order_id of the order referring to\ + :param user: User object of the requesting user + :returns Response object with + order object and 200 status if no error + message and corresponding status if error + """ + order = Order.objects.get(id=order_id) + order_serializer = OrderSerializer(order) + user = order.user + # email user.email, number user.number + payment = {} + try: + user_dict = UserSerializer(user).data + user_details = UserDetails.objects.get(user=user) + user_dict["name"] = user_details.name + gateway_endpoint = settings.API_GATEWAY_URL + "/v1/payment" + gateway_credential = basic_auth( + settings.API_GATEWAY_USERNAME, settings.API_GATEWAY_PASSWORD + ) + logging.debug(gateway_endpoint) + data = {} + data["user"] = user_dict + data["order"] = order_serializer.data + data["amount"] = float(order.product.price) * int(order.quantity) + try: + payment_response = requests.post( + gateway_endpoint, + headers={ + "Authorization": gateway_credential, + "Content-Type": "application/json", + }, + json=data, + verify=False, + timeout=5, + ) + if payment_response.status_code == 200: + payment = payment_response.json() + else: + logging.error( + "Payment response error, {}: {}".format( + payment_response.status_code, payment_response.content + ) + ) + logging.debug("payment response: {}".format(payment)) + except Exception as e: + logging.error(e, exc_info=True) + except Exception as e: + logging.error(e, exc_info=True) + response_data = dict(order=order_serializer.data, payment=payment) + return Response(response_data, status=status.HTTP_200_OK) + + @jwt_auth_required + def post(self, request, order_id=None, user=None): + """ + order view for adding a new order + :param request: http request for the view + method allowed: POST + http request should be authorised by the jwt token of the user + mandatory fields: ['product_id', 'quantity'] + :param order_id: + order_id of the order referring to + mandatory for GET and PUT http methods + :param user: User object of the requesting user + :returns Response object with + order object and 200 status if no error + message and corresponding status if error + """ + request_data = request.data + serializer = ProductQuantitySerializer(data=request_data) + if not serializer.is_valid(): + log_error( + request.path, + request.data, + status.HTTP_400_BAD_REQUEST, + serializer.errors, + ) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + product = Product.objects.get(id=request_data["product_id"]) + user_details = UserDetails.objects.get(user=user) + if user_details.available_credit < product.price: + return Response( + {"message": messages.INSUFFICIENT_BALANCE}, + status=status.HTTP_400_BAD_REQUEST, + ) + user_details.available_credit -= float(product.price * request_data["quantity"]) + order = Order.objects.create( + user=user, + product=product, + quantity=request_data["quantity"], + created_on=timezone.now(), + transaction_id=uuid.uuid4(), + ) + user_details.save() + return Response( + { + "id": order.id, + "message": messages.ORDER_CREATED, + "credit": user_details.available_credit, + }, + status=status.HTTP_200_OK, + ) + + @jwt_auth_required + def put(self, request, order_id=None, user=None): + """ + order view for updating a particular order + :param request: http request for the view + method allowed: PUT + http request should be authorised by the jwt token of the user + mandatory fields for POST and PUT http methods: ['product_id', 'quantity'] + :param order_id: + order_id of the order referring to + mandatory for GET and PUT http methods + :param user: User object of the requesting user + :returns Response object with + order object and 200 status if no error + message and corresponding status if error + """ + request_data = request.data + order = Order.objects.get(id=order_id) + if user != order.user: + return Response( + {"message": messages.RESTRICTED}, status=status.HTTP_403_FORBIDDEN + ) + if "quantity" in request_data: + order.quantity = request_data["quantity"] + if "status" in request_data and not Order.STATUS_CHOICES.has_value( + request_data["status"] + ): + return Response( + {"message": messages.INVALID_STATUS}, status=status.HTTP_400_BAD_REQUEST + ) + user_details = UserDetails.objects.get(user=order.user) + if "status" in request_data and request_data["status"] != order.status: + order.status = request_data["status"] + if request_data["status"] == Order.STATUS_CHOICES.RETURNED.value: + user_details.available_credit += float( + order.quantity * order.product.price + ) + user_details.save() + order.save() + serializer = OrderSerializer(order) + response_data = dict(orders=serializer.data) + return Response(response_data, status=status.HTTP_200_OK) + + +class OrderDetailsView(APIView, LimitOffsetPagination): + """ + Get the details of the orders. + """ + + @jwt_auth_required + def get(self, request, user=None): + """ + returns all the order of the particular user + :param request: http request for the view + method allowed: GET + http request should be authorised by the jwt token of the user + :param user: User object of the requesting user + :returns Response object with + list of order object and 200 status if no error + message and corresponding status if error + """ + orders = Order.objects.filter(user=user).order_by("-id") + paginated = self.paginate_queryset(orders, request, view=self) + serializer = OrderSerializer(paginated, many=True) + response_data = dict( + orders=serializer.data, + next_offset=( + self.offset + self.limit + if self.offset + self.limit < self.count + else None + ), + previous_offset=( + self.offset - self.limit if self.offset - self.limit >= 0 else None + ), + count=self.get_count(paginated), + ) + return Response(response_data, status=status.HTTP_200_OK) + + +class ReturnOrder(APIView): + """ + Return Order View + """ + + @jwt_auth_required + def post(self, request, user=None): + """ + api for returning an order + :param request: http request for the view + method allowed: POST + http request should be authorised by the jwt token of the user + :param user: User object of the requesting user + :returns Response object with + message and 200 status if no error + message and corresponding status if error + """ + order = Order.objects.get(id=request.GET["order_id"]) + if user != order.user: + return Response( + {"message": messages.RESTRICTED}, status=status.HTTP_403_FORBIDDEN + ) + if order.status == Order.STATUS_CHOICES.RETURNED.value: + return Response( + {"message": messages.ORDER_ALREADY_RETURNED}, + status=status.HTTP_400_BAD_REQUEST, + ) + elif order.status == Order.STATUS_CHOICES.RETURN_PENDING.value: + return Response( + {"message": messages.ORDER_RETURNED_PENDING}, + status=status.HTTP_400_BAD_REQUEST, + ) + + qr_code_url = request.build_absolute_uri(reverse("shop-return-qr-code")) + order.status = Order.STATUS_CHOICES.RETURN_PENDING.value + order.save() + serializer = OrderSerializer(order) + return Response( + { + "message": messages.ORDER_RETURNING, + "qr_code_url": qr_code_url, + "order": serializer.data, + }, + status=status.HTTP_200_OK, + ) + + +class ReturnQRCodeView(APIView): + """ + QR code image view + """ + + def get(self, request): + """ + returns a qr code image + :param request: http request for the view + method allowed: GET + :return: FileResponse + """ + img = open("utils/return-qr-code.png", "rb") + return FileResponse(img) + + +class ApplyCouponView(APIView): + """ + Apply Coupon View to increase the available credit + """ + + @jwt_auth_required + def post(self, request, user=None): + """ + api for checking if coupon is already claimed + if claimed before: returns an error message + else: increases the user credit + :param request: http request for the view + method allowed: POST + http request should be authorised by the jwt token of the user + :param user: User object of the requesting user + :returns Response object with + message and 200 status if no error + message and corresponding status if error + """ + coupon_request_body = request.data + + serializer = CouponSerializer(data=coupon_request_body) + if not serializer.is_valid(): + log_error(request.path, request.data, 400, serializer.errors) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + row = None + with connection.cursor() as cursor: + try: + cursor.execute( + "SELECT coupon_code from applied_coupon WHERE user_id = " + + str(user.id) + + " AND coupon_code = '" + + coupon_request_body["coupon_code"] + + "'" + ) + row = cursor.fetchall() + except Exception as e: + log_error(request.path, request.data, 500, e) + return Response( + {"message": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + if row and row != None: + return Response( + { + "message": row[0][0] + " " + messages.COUPON_ALREADY_APPLIED, + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + try: + coupon = Coupon.objects.using("mongodb").get( + coupon_code=coupon_request_body["coupon_code"] + ) + except ObjectDoesNotExist as e: + log_error(request.path, request.data, 400, e) + return Response( + {"message": messages.COUPON_NOT_FOUND}, + status=status.HTTP_400_BAD_REQUEST, + ) + + AppliedCoupon.objects.create( + user=user, coupon_code=coupon_request_body["coupon_code"] + ) + user_details = UserDetails.objects.get(user=user) + user_details.available_credit += coupon_request_body["amount"] + user_details.save() + return Response( + { + "credit": user_details.available_credit, + "message": messages.COUPON_APPLIED, + }, + status=status.HTTP_200_OK, + ) diff --git a/services/workshop/crapi/shop/views_vulnerable.py b/services/workshop/crapi/shop/views_vulnerable.py new file mode 100644 index 00000000..9fd30a96 --- /dev/null +++ b/services/workshop/crapi/shop/views_vulnerable.py @@ -0,0 +1,433 @@ +# +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +""" +contains views related to Shop APIs +""" +import logging +import uuid +from django.db import connection +from django.utils import timezone +from django.http import FileResponse +from django.urls import reverse +import requests +from crapi_site import settings +from rest_framework import status +from rest_framework.response import Response +from rest_framework.views import APIView +from utils.helper import basic_auth +from crapi.shop.serializers import ( + OrderSerializer, + ProductSerializer, + CouponSerializer, + ProductQuantitySerializer, +) +from crapi.user.serializers import UserSerializer +from utils.jwt import jwt_auth_required +from utils import messages +from crapi.shop.models import Order, Product, AppliedCoupon, Coupon +from crapi.user.models import UserDetails +from utils.logging import log_error +from django.core.exceptions import ObjectDoesNotExist +from rest_framework.pagination import LimitOffsetPagination + + +class ProductView(APIView, LimitOffsetPagination): + """ + Product Controller View + """ + + @jwt_auth_required + def get(self, request, user): + """ + products view for fetching the list of products + :param request: http request for the view + method allowed: GET + http request should be authorised by the jwt token of the user + :param user: User object of the requesting user + :returns Response object with + products list and 200 status if no error + message and corresponding status if error + """ + user_details = UserDetails.objects.get(user=user) + products = Product.objects.all().order_by("-id") + paginated = self.paginate_queryset(products, request, view=self) + serializer = ProductSerializer(paginated, many=True) + response_data = dict( + products=serializer.data, + credit=user_details.available_credit, + next_offset=( + self.offset + self.limit + if self.offset + self.limit < self.count + else None + ), + previous_offset=( + self.offset - self.limit if self.offset - self.limit >= 0 else None + ), + count=self.get_count(paginated), + ) + return Response(response_data, status=status.HTTP_200_OK) + + @jwt_auth_required + def post(self, request, user): + """ + products view for adding a new product + :param request: http request for the view + method allowed: POST + http request should be authorised by the jwt token of the user + mandatory fields for POST and PUT http methods: ['name', 'price', 'image_url'] + :param user: User object of the requesting user + :returns Response object with + products list and 200 status if no error + message and corresponding status if error + """ + user_request_body = request.data + serializer = ProductSerializer(data=user_request_body) + if not serializer.is_valid(): + log_error(request.path, request.data, 400, serializer.errors) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + + +class OrderControlView(APIView): + """ + Order Controller View + """ + + def get(self, request, order_id=None, user=None): + """ + order view for fetching a particular order + :param request: http request for the view + method allowed: GET + http request should be authorised by the jwt token of the user + :param order_id: + order_id of the order referring to\ + :param user: User object of the requesting user + :returns Response object with + order object and 200 status if no error + message and corresponding status if error + """ + order = Order.objects.get(id=order_id) + order_serializer = OrderSerializer(order) + user = order.user + # email user.email, number user.number + payment = {} + try: + user_dict = UserSerializer(user).data + user_details = UserDetails.objects.get(user=user) + user_dict["name"] = user_details.name + gateway_endpoint = settings.API_GATEWAY_URL + "/v1/payment" + gateway_credential = basic_auth( + settings.API_GATEWAY_USERNAME, settings.API_GATEWAY_PASSWORD + ) + logging.debug(gateway_endpoint) + data = {} + data["user"] = user_dict + data["order"] = order_serializer.data + data["amount"] = float(order.product.price) * int(order.quantity) + try: + payment_response = requests.post( + gateway_endpoint, + headers={ + "Authorization": gateway_credential, + "Content-Type": "application/json", + }, + json=data, + verify=False, + timeout=5, + ) + if payment_response.status_code == 200: + payment = payment_response.json() + else: + logging.error( + "Payment response error, {}: {}".format( + payment_response.status_code, payment_response.content + ) + ) + logging.debug("payment response: {}".format(payment)) + except Exception as e: + logging.error(e, exc_info=True) + except Exception as e: + logging.error(e, exc_info=True) + response_data = dict(order=order_serializer.data, payment=payment) + return Response(response_data, status=status.HTTP_200_OK) + + @jwt_auth_required + def post(self, request, order_id=None, user=None): + """ + order view for adding a new order + :param request: http request for the view + method allowed: POST + http request should be authorised by the jwt token of the user + mandatory fields: ['product_id', 'quantity'] + :param order_id: + order_id of the order referring to + mandatory for GET and PUT http methods + :param user: User object of the requesting user + :returns Response object with + order object and 200 status if no error + message and corresponding status if error + """ + request_data = request.data + serializer = ProductQuantitySerializer(data=request_data) + if not serializer.is_valid(): + log_error( + request.path, + request.data, + status.HTTP_400_BAD_REQUEST, + serializer.errors, + ) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + product = Product.objects.get(id=request_data["product_id"]) + user_details = UserDetails.objects.get(user=user) + if user_details.available_credit < product.price: + return Response( + {"message": messages.INSUFFICIENT_BALANCE}, + status=status.HTTP_400_BAD_REQUEST, + ) + user_details.available_credit -= float(product.price * request_data["quantity"]) + order = Order.objects.create( + user=user, + product=product, + quantity=request_data["quantity"], + created_on=timezone.now(), + transaction_id=uuid.uuid4(), + ) + user_details.save() + return Response( + { + "id": order.id, + "message": messages.ORDER_CREATED, + "credit": user_details.available_credit, + }, + status=status.HTTP_200_OK, + ) + + @jwt_auth_required + def put(self, request, order_id=None, user=None): + """ + order view for updating a particular order + :param request: http request for the view + method allowed: PUT + http request should be authorised by the jwt token of the user + mandatory fields for POST and PUT http methods: ['product_id', 'quantity'] + :param order_id: + order_id of the order referring to + mandatory for GET and PUT http methods + :param user: User object of the requesting user + :returns Response object with + order object and 200 status if no error + message and corresponding status if error + """ + request_data = request.data + order = Order.objects.get(id=order_id) + if user != order.user: + return Response( + {"message": messages.RESTRICTED}, status=status.HTTP_403_FORBIDDEN + ) + if "quantity" in request_data: + order.quantity = request_data["quantity"] + if "status" in request_data and not Order.STATUS_CHOICES.has_value( + request_data["status"] + ): + return Response( + {"message": messages.INVALID_STATUS}, status=status.HTTP_400_BAD_REQUEST + ) + user_details = UserDetails.objects.get(user=order.user) + if "status" in request_data and request_data["status"] != order.status: + order.status = request_data["status"] + if request_data["status"] == Order.STATUS_CHOICES.RETURNED.value: + user_details.available_credit += float( + order.quantity * order.product.price + ) + user_details.save() + order.save() + serializer = OrderSerializer(order) + response_data = dict(orders=serializer.data) + return Response(response_data, status=status.HTTP_200_OK) + + +class OrderDetailsView(APIView, LimitOffsetPagination): + """ + Get the details of the orders. + """ + + @jwt_auth_required + def get(self, request, user=None): + """ + returns all the order of the particular user + :param request: http request for the view + method allowed: GET + http request should be authorised by the jwt token of the user + :param user: User object of the requesting user + :returns Response object with + list of order object and 200 status if no error + message and corresponding status if error + """ + orders = Order.objects.filter(user=user).order_by("-id") + paginated = self.paginate_queryset(orders, request, view=self) + serializer = OrderSerializer(paginated, many=True) + response_data = dict( + orders=serializer.data, + next_offset=( + self.offset + self.limit + if self.offset + self.limit < self.count + else None + ), + previous_offset=( + self.offset - self.limit if self.offset - self.limit >= 0 else None + ), + count=self.get_count(paginated), + ) + return Response(response_data, status=status.HTTP_200_OK) + + +class ReturnOrder(APIView): + """ + Return Order View + """ + + @jwt_auth_required + def post(self, request, user=None): + """ + api for returning an order + :param request: http request for the view + method allowed: POST + http request should be authorised by the jwt token of the user + :param user: User object of the requesting user + :returns Response object with + message and 200 status if no error + message and corresponding status if error + """ + order = Order.objects.get(id=request.GET["order_id"]) + if user != order.user: + return Response( + {"message": messages.RESTRICTED}, status=status.HTTP_403_FORBIDDEN + ) + if order.status == Order.STATUS_CHOICES.RETURNED.value: + return Response( + {"message": messages.ORDER_ALREADY_RETURNED}, + status=status.HTTP_400_BAD_REQUEST, + ) + elif order.status == Order.STATUS_CHOICES.RETURN_PENDING.value: + return Response( + {"message": messages.ORDER_RETURNED_PENDING}, + status=status.HTTP_400_BAD_REQUEST, + ) + + qr_code_url = request.build_absolute_uri(reverse("shop-return-qr-code")) + order.status = Order.STATUS_CHOICES.RETURN_PENDING.value + order.save() + serializer = OrderSerializer(order) + return Response( + { + "message": messages.ORDER_RETURNING, + "qr_code_url": qr_code_url, + "order": serializer.data, + }, + status=status.HTTP_200_OK, + ) + + +class ReturnQRCodeView(APIView): + """ + QR code image view + """ + + def get(self, request): + """ + returns a qr code image + :param request: http request for the view + method allowed: GET + :return: FileResponse + """ + img = open("utils/return-qr-code.png", "rb") + return FileResponse(img) + + +class ApplyCouponView(APIView): + """ + Apply Coupon View to increase the available credit + """ + + @jwt_auth_required + def post(self, request, user=None): + """ + api for checking if coupon is already claimed + if claimed before: returns an error message + else: increases the user credit + :param request: http request for the view + method allowed: POST + http request should be authorised by the jwt token of the user + :param user: User object of the requesting user + :returns Response object with + message and 200 status if no error + message and corresponding status if error + """ + coupon_request_body = request.data + + serializer = CouponSerializer(data=coupon_request_body) + if not serializer.is_valid(): + log_error(request.path, request.data, 400, serializer.errors) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + row = None + with connection.cursor() as cursor: + try: + cursor.execute( + "SELECT coupon_code from applied_coupon WHERE user_id = " + + str(user.id) + + " AND coupon_code = '" + + coupon_request_body["coupon_code"] + + "'" + ) + row = cursor.fetchall() + except Exception as e: + log_error(request.path, request.data, 500, e) + return Response( + {"message": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + if row and row != None: + return Response( + { + "message": row[0][0] + " " + messages.COUPON_ALREADY_APPLIED, + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + try: + coupon = Coupon.objects.using("mongodb").get( + coupon_code=coupon_request_body["coupon_code"] + ) + except ObjectDoesNotExist as e: + log_error(request.path, request.data, 400, e) + return Response( + {"message": messages.COUPON_NOT_FOUND}, + status=status.HTTP_400_BAD_REQUEST, + ) + + AppliedCoupon.objects.create( + user=user, coupon_code=coupon_request_body["coupon_code"] + ) + user_details = UserDetails.objects.get(user=user) + user_details.available_credit += coupon_request_body["amount"] + user_details.save() + return Response( + { + "credit": user_details.available_credit, + "message": messages.COUPON_APPLIED, + }, + status=status.HTTP_200_OK, + )