diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..883824c4f --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,37 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose +{ + "name": "Cruddur Configuration", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + // Features to add to the dev container. More info: https://containers.dev/features. + "features": { + "ghcr.io/devcontainers/features/aws-cli:1": {} + }, + "remoteEnv": { + "AWS_CLI_AUTO_PROMPT": "on-partial" + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-azuretools.vscode-docker", + "ms-python.python", + "dracula-theme.theme-dracula" + ], + "settings": { + "workbench.colorThe": "Default Dark+ Experimental" + } + } + } + + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "devcontainer" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..a62d260e5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +frontend-react-js/build/* +node_modules +*.env +docker/**/* \ No newline at end of file diff --git a/.gitpod.yml b/.gitpod.yml index a0ba1af5e..ce1b359e1 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -1,4 +1,61 @@ +tasks: + - name: aws-cli + env: + AWS_CLI_AUTO_PROMPT: on-partial + before: | + cd /workspace + rm -rf aws awscliv2.zip + curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" + unzip awscliv2.zip + sudo ./aws/install + cd $THEIA_WORKSPACE_ROOT + bash bin/ecr/login + - name: postgres + before: | + curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc|sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/postgresql.gpg + echo "deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main" |sudo tee /etc/apt/sources.list.d/pgdg.list + sudo apt update + sudo apt install -y postgresql-client-13 libpq-dev + command: | + export GITPOD_IP=$(curl ifconfig.me) + source "$THEIA_WORKSPACE_ROOT/backend-flask/bin/rds/update-sg-rule" + - name: react-js + command: | + ruby $THEIA_WORKSPACE_ROOT/bin/frontend/generate-env + cd frontend-react-js + npm i + - name: backend-flask + command: | + ruby $THEIA_WORKSPACE_ROOT/bin/backend/generate-env + cd backend-flask + pip install -r requirements.txt + - name: fargate + command: | + curl "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_64bit/session-manager-plugin.deb" -o "session-manager-plugin.deb" + sudo dpkg -i session-manager-plugin.deb + cd backend-flask + - name: cdk + before: | + npm install aws-cdk -g + cd thumbing-serverless-cdk + npm i vscode: extensions: - - 42Crunch.vscode-openapi \ No newline at end of file + - 42Crunch.vscode-openapi + - cweijan.vscode-postgresql-client2 + - ms-python.python + + - rangav.vscode-thunder-client + +ports: + - name: frontend + port: 3000 + onOpen: open-browser + visibility: public + - name: backend + port: 4567 + visibility: public + - name: xray-daemon + port: 2000 + visibility: public diff --git a/README.md b/README.md index 93d2ffb9b..c14bdc973 100644 --- a/README.md +++ b/README.md @@ -17,17 +17,17 @@ At the start of the bootcamp you need to create a new Github Repository from thi The `/journal` directory contains -- [ ] [Week 0](journal/week0.md) -- [ ] [Week 1](journal/week1.md) -- [ ] [Week 2](journal/week2.md) -- [ ] [Week 3](journal/week3.md) -- [ ] [Week 4](journal/week4.md) -- [ ] [Week 5](journal/week5.md) -- [ ] [Week 6](journal/week6.md) -- [ ] [Week 7](journal/week7.md) -- [ ] [Week 8](journal/week8.md) -- [ ] [Week 9](journal/week9.md) -- [ ] [Week 10](journal/week10.md) -- [ ] [Week 11](journal/week11.md) -- [ ] [Week 12](journal/week12.md) -- [ ] [Week 13](journal/week13.md) \ No newline at end of file +- [x] [Week 0 - Billing and Architecture](journal/week0.md) +- [x] [Week 1 - App Containerization](journal/week1.md) +- [x] [Week 2 - Distributed Tracing](journal/week2.md) +- [x] [Week 3 - Decentralized Authentication](journal/week3.md) +- [x] [Week 4 - Postgres and RDS](journal/week4.md) +- [x] [Week 5 - DynamoDB and Serverless Caching](journal/week5.md) +- [ ] [Week 6 - Deploying Containers](journal/week6.md) +- [ ] [Week 7 - Solving CORS with a Load Balancer and Custom Domain](journal/week7.md) +- [ ] [Week 8 - Serverless Image Processing](journal/week8.md) +- [ ] [Week 9 - CI/CD with CodePipeline, CodeBuild and CodeDeploy](journal/week9.md) +- [ ] [Week 10 - CloudFormation Part 1](journal/week10.md) +- [ ] [Week 11 - CloudFormation Part 2](journal/week11.md) +- [ ] [Week 12 - Modern APIs](journal/week12.md) +- [ ] [Week 13 - (Secret Bonus Class)](journal/week13.md) diff --git a/aws/backup_files/docker-compose.yml b/aws/backup_files/docker-compose.yml new file mode 100644 index 000000000..82d762e59 --- /dev/null +++ b/aws/backup_files/docker-compose.yml @@ -0,0 +1,86 @@ +version: "3.8" +services: + backend-flask: + environment: + #AWS_ENDPOINT_URL: "http://dynamodb-local:8000" + #CONNECTION_URL: "postgresql://postgres:password@db:5432/cruddur" + CONNECTION_URL: "${PROD_CONNECTION_URL}" + PROD_CONNECTION_URL: "${PROD_CONNECTION_URL}" + #FRONTEND_URL: "https://${CODESPACE_NAME}-3000.${GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}" + #BACKEND_URL: "https://${CODESPACE_NAME}-4567.${GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}" + FRONTEND_URL: "https://3000-${GITPOD_WORKSPACE_ID}.${GITPOD_WORKSPACE_CLUSTER_HOST}" + BACKEND_URL: "https://4567-${GITPOD_WORKSPACE_ID}.${GITPOD_WORKSPACE_CLUSTER_HOST}" + OTEL_EXPORTER_OTLP_ENDPOINT: "https://api.honeycomb.io" + OTEL_EXPORTER_OTLP_HEADERS: "x-honeycomb-team=${HONEYCOMB_API_KEY}" + OTEL_SERVICE_NAME: "${HONEYCOMB_SERVICE_NAME}" + #AWS_XRAY_URL: "*${CODESPACE_NAME}-4567.${GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}*" + AWS_XRAY_URL: "*4567-${GITPOD_WORKSPACE_ID}.${GITPOD_WORKSPACE_CLUSTER_HOST}*" + AWS_XRAY_DAEMON_ADDRESS: "xray-daemon:2000" + AWS_DEFAULT_REGION: "${AWS_DEFAULT_REGION}" + AWS_ACCESS_KEY_ID: "${AWS_ACCESS_KEY_ID}" + AWS_SECRET_ACCESS_KEY: "${AWS_SECRET_ACCESS_KEY}" + ROLLBAR_ACCESS_TOKEN: "${ROLLBAR_ACCESS_TOKEN}" + AWS_COGNITO_USER_POOL_ID: "us-east-1_fjE3ZCqqX" + AWS_COGNITO_USER_POOL_CLIENT_ID: "6ff7h7vbdua1rvjctbi7lode3l" + build: ./backend-flask + ports: + - "4567:4567" + volumes: + - ./backend-flask:/backend-flask + frontend-react-js: + environment: + REACT_APP_BACKEND_URL: "https://4567-${GITPOD_WORKSPACE_ID}.${GITPOD_WORKSPACE_CLUSTER_HOST}" + #REACT_APP_BACKEND_URL: "https://${CODESPACE_NAME}-4567.${GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}" + REACT_APP_AWS_PROJECT_REGION: "${AWS_DEFAULT_REGION}" + REACT_APP_AWS_COGNITO_REGION: "${AWS_DEFAULT_REGION}" + REACT_APP_AWS_USER_POOLS_ID: "us-east-1_fjE3ZCqqX" + REACT_APP_CLIENT_ID: "6ff7h7vbdua1rvjctbi7lode3l" + REACT_APP_AWS_USER_POOLS_WEB_CLIENT_ID: "6ff7h7vbdua1rvjctbi7lode3l" + build: ./frontend-react-js + ports: + - "3000:3000" + volumes: + - ./frontend-react-js:/frontend-react-js + xray-daemon: + image: "amazon/aws-xray-daemon" + environment: + AWS_ACCESS_KEY_ID: "${AWS_ACCESS_KEY_ID}" + AWS_SECRET_ACCESS_KEY: "${AWS_SECRET_ACCESS_KEY}" + AWS_REGION: "us-east-1" + command: + - "xray -o -b xray-daemon:2000" + ports: + - 2000:2000/udp + db: + image: postgres:13-alpine + restart: always + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=password + ports: + - '5432:5432' + volumes: + - db:/var/lib/postgresql/data + dynamodb-local: + # https://stackoverflow.com/questions/67533058/persist-local-dynamodb-data-in-volumes-lack-permission-unable-to-open-databa + # We needed to add user:root to get this working. + user: root + command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data" + image: "amazon/dynamodb-local:latest" + container_name: dynamodb-local + ports: + - "8000:8000" + volumes: + - "./docker/dynamodb:/home/dynamodblocal/data" + working_dir: /home/dynamodblocal + +# the name flag is a hack to change the default prepend folder +# name when outputting the image names +networks: + internal-network: + driver: bridge + name: cruddur + +volumes: + db: + driver: local diff --git a/aws/json/alarm_config.json b/aws/json/alarm_config.json new file mode 100644 index 000000000..85af038e3 --- /dev/null +++ b/aws/json/alarm_config.json @@ -0,0 +1,35 @@ +{ + "AlarmName": "DailyEstimatedCharges", + "AlarmDescription": "This alarm would be triggered if the daily estimated charges exceeds 1$", + "ActionsEnabled": true, + "AlarmActions": [ + "arn:aws:sns:us-east-1:730331264766:billing-alarm" + ], + "EvaluationPeriods": 1, + "DatapointsToAlarm": 1, + "Threshold": 1, + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "TreatMissingData": "breaching", + "Metrics": [{ + "Id": "m1", + "MetricStat": { + "Metric": { + "Namespace": "AWS/Billing", + "MetricName": "EstimatedCharges", + "Dimensions": [{ + "Name": "Currency", + "Value": "USD" + }] + }, + "Period": 86400, + "Stat": "Maximum" + }, + "ReturnData": false + }, + { + "Id": "e1", + "Expression": "IF(RATE(m1)>0,RATE(m1)*86400,0)", + "Label": "DailyEstimatedCharges", + "ReturnData": true + }] + } \ No newline at end of file diff --git a/aws/json/budget-notifications-with-subscribers.json b/aws/json/budget-notifications-with-subscribers.json new file mode 100644 index 000000000..06abb1f9e --- /dev/null +++ b/aws/json/budget-notifications-with-subscribers.json @@ -0,0 +1,16 @@ +[ + { + "Notification": { + "ComparisonOperator": "GREATER_THAN", + "NotificationType": "ACTUAL", + "Threshold": 50, + "ThresholdType": "PERCENTAGE" + }, + "Subscribers": [ + { + "Address": "papemfall@gmail.com", + "SubscriptionType": "EMAIL" + } + ] + } + ] \ No newline at end of file diff --git a/aws/json/budget.json b/aws/json/budget.json new file mode 100644 index 000000000..848436f76 --- /dev/null +++ b/aws/json/budget.json @@ -0,0 +1,31 @@ +{ + "BudgetLimit": { + "Amount": "10", + "Unit": "USD" + }, + "BudgetName": "Example Tag Budget", + "BudgetType": "COST", + "CostFilters": { + "TagKeyValue": [ + "user:Key$value1", + "user:Key$value2" + ] + }, + "CostTypes": { + "IncludeCredit": true, + "IncludeDiscount": true, + "IncludeOtherSubscription": true, + "IncludeRecurring": true, + "IncludeRefund": true, + "IncludeSubscription": true, + "IncludeSupport": true, + "IncludeTax": true, + "IncludeUpfront": true, + "UseBlended": false + }, + "TimePeriod": { + "Start": 1477958399, + "End": 3706473600 + }, + "TimeUnit": "MONTHLY" + } \ No newline at end of file diff --git a/aws/json/service-backend-flask.json b/aws/json/service-backend-flask.json new file mode 100644 index 000000000..59c22ac4b --- /dev/null +++ b/aws/json/service-backend-flask.json @@ -0,0 +1,41 @@ +{ + "cluster": "cruddur", + "launchType": "FARGATE", + "desiredCount": 1, + "enableECSManagedTags": true, + "enableExecuteCommand": true, + "loadBalancers": [ + { + "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:730331264766:targetgroup/cruddur-backend-flask-tg/fb2fd7e553317afd", + "containerName": "backend-flask", + "containerPort": 4567 + } + ], + "networkConfiguration": { + "awsvpcConfiguration": { + "assignPublicIp": "ENABLED", + "securityGroups": [ + "sg-0abca869cdbf1a4f4" + ], + "subnets": [ + "subnet-089d721f396a65cf4", + "subnet-05095853de204f9f1", + "subnet-0aaf7ff438aeb86f8" + ] + } + }, + "propagateTags": "SERVICE", + "serviceName": "backend-flask", + "taskDefinition": "backend-flask", + "serviceConnectConfiguration": { + "enabled": true, + "namespace": "cruddur", + "services": [ + { + "portName": "backend-flask", + "discoveryName": "backend-flask", + "clientAliases": [{"port": 4567}] + } + ] + } + } \ No newline at end of file diff --git a/aws/json/service-frontend-react-js.json b/aws/json/service-frontend-react-js.json new file mode 100644 index 000000000..e5a364726 --- /dev/null +++ b/aws/json/service-frontend-react-js.json @@ -0,0 +1,41 @@ +{ + "cluster": "cruddur", + "launchType": "FARGATE", + "desiredCount": 1, + "enableECSManagedTags": true, + "enableExecuteCommand": true, + "loadBalancers": [ + { + "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:730331264766:targetgroup/cruddur-frontend-react-tg/0fca7d23fd3fc09e", + "containerName": "frontend-react-js", + "containerPort": 3000 + } + ], + "networkConfiguration": { + "awsvpcConfiguration": { + "assignPublicIp": "ENABLED", + "securityGroups": [ + "sg-0abca869cdbf1a4f4" + ], + "subnets": [ + "subnet-089d721f396a65cf4", + "subnet-05095853de204f9f1", + "subnet-0aaf7ff438aeb86f8" + ] + } + }, + "propagateTags": "SERVICE", + "serviceName": "frontend-react-js", + "taskDefinition": "frontend-react-js", + "serviceConnectConfiguration": { + "enabled": true, + "namespace": "cruddur", + "services": [ + { + "portName": "frontend-react-js", + "discoveryName": "frontend-react-js", + "clientAliases": [{"port": 3000}] + } + ] + } + } \ No newline at end of file diff --git a/aws/json/xray.json b/aws/json/xray.json new file mode 100644 index 000000000..c6ad212d6 --- /dev/null +++ b/aws/json/xray.json @@ -0,0 +1,15 @@ +{ + "SamplingRule": { + "RuleName": "Cruddur", + "ResourceARN": "*", + "Priority": 9000, + "FixedRate": 0.1, + "ReservoirSize": 5, + "ServiceName": "Cruddur", + "ServiceType": "*", + "Host": "*", + "HTTPMethod": "*", + "URLPath": "*", + "Version": 1 + } + } \ No newline at end of file diff --git a/aws/lambdas/cruddur-messaging-stream.py b/aws/lambdas/cruddur-messaging-stream.py new file mode 100644 index 000000000..82d8aff2c --- /dev/null +++ b/aws/lambdas/cruddur-messaging-stream.py @@ -0,0 +1,44 @@ +import json +import boto3 +from boto3.dynamodb.conditions import Key, Attr + +dynamodb = boto3.resource( + 'dynamodb', + region_name='us-east-1', + endpoint_url="http://dynamodb.us-east-1.amazonaws.com" +) + +def lambda_handler(event, context): + pk = event['Records'][0]['dynamodb']['Keys']['pk']['S'] + sk = event['Records'][0]['dynamodb']['Keys']['sk']['S'] + if pk.startswith('MSG#'): + group_uuid = pk.replace("MSG#","") + message = event['Records'][0]['dynamodb']['NewImage']['message']['S'] + print("GRUP ===>",group_uuid,message) + + table_name = 'cruddur-messages' + index_name = 'message-group-sk-index' + table = dynamodb.Table(table_name) + data = table.query( + IndexName=index_name, + KeyConditionExpression=Key('message_group_uuid').eq(group_uuid) + ) + print("RESP ===>",data['Items']) + + # recreate the message group rows with new SK value + for i in data['Items']: + delete_item = table.delete_item(Key={'pk': i['pk'], 'sk': i['sk']}) + print("DELETE ===>",delete_item) + + response = table.put_item( + Item={ + 'pk': i['pk'], + 'sk': sk, + 'message_group_uuid':i['message_group_uuid'], + 'message':message, + 'user_display_name': i['user_display_name'], + 'user_handle': i['user_handle'], + 'user_uuid': i['user_uuid'] + } + ) + print("CREATE ===>",response) \ No newline at end of file diff --git a/aws/lambdas/cruddur-post-confirmation.py b/aws/lambdas/cruddur-post-confirmation.py new file mode 100644 index 000000000..d8f498dab --- /dev/null +++ b/aws/lambdas/cruddur-post-confirmation.py @@ -0,0 +1,53 @@ +import json +import psycopg2 +import os + +def lambda_handler(event, context): + user = event['request']['userAttributes'] + + print('UserAttributes') + print(user) + + user_display_name=user['name'] + user_email=user['email'] + user_handle=user['preferred_username'] + user_cognito_id=user['sub'] + try: + conn = psycopg2.connect(os.getenv('CONNECTION_URL')) + cur = conn.cursor() + + + sql = f""" + INSERT INTO public.users ( + display_name, + email, + handle, + cognito_user_id + ) + VALUES( + %s, + %s, + %s, + %s + ) + """ + #cur.execute("INSERT INTO users (display_name, handle, cognito_user_id) VALUES(%s, %s, %s)", (user['name'], user['email'], user['sub'])) + params = [ + user_display_name, + user_email, + user_handle, + user_cognito_id + ] + cur.execute(sql,*params) + conn.commit() + + except (Exception, psycopg2.DatabaseError) as error: + print(error) + + finally: + if conn is not None: + cur.close() + conn.close() + print('Database connection closed.') + + return event \ No newline at end of file diff --git a/aws/lambdas/cruddur-upload-avatar/Gemfile b/aws/lambdas/cruddur-upload-avatar/Gemfile new file mode 100644 index 000000000..74a16f2cc --- /dev/null +++ b/aws/lambdas/cruddur-upload-avatar/Gemfile @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +# gem "rails" +gem "aws-sdk-s3" +gem "ox" +gem "jwt" \ No newline at end of file diff --git a/aws/lambdas/cruddur-upload-avatar/Gemfile.lock b/aws/lambdas/cruddur-upload-avatar/Gemfile.lock new file mode 100644 index 000000000..6e8e268f2 --- /dev/null +++ b/aws/lambdas/cruddur-upload-avatar/Gemfile.lock @@ -0,0 +1,33 @@ +GEM + remote: https://rubygems.org/ + specs: + aws-eventstream (1.2.0) + aws-partitions (1.772.0) + aws-sdk-core (3.173.1) + aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (~> 1, >= 1.651.0) + aws-sigv4 (~> 1.5) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.64.0) + aws-sdk-core (~> 3, >= 3.165.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.122.0) + aws-sdk-core (~> 3, >= 3.165.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.4) + aws-sigv4 (1.5.2) + aws-eventstream (~> 1, >= 1.0.2) + jmespath (1.6.2) + jwt (2.7.0) + ox (2.14.16) + +PLATFORMS + x86_64-linux + +DEPENDENCIES + aws-sdk-s3 + jwt + ox + +BUNDLED WITH + 2.4.9 diff --git a/aws/lambdas/cruddur-upload-avatar/function.rb b/aws/lambdas/cruddur-upload-avatar/function.rb new file mode 100644 index 000000000..6e7fc5cd2 --- /dev/null +++ b/aws/lambdas/cruddur-upload-avatar/function.rb @@ -0,0 +1,48 @@ +require 'aws-sdk-s3' +require 'json' +require 'jwt' + +def handler(event:, context:) + puts event + # return cors headers for preflight check + if event['routeKey'] == "OPTIONS /{proxy+}" + puts({step: 'preflight', message: 'preflight CORS check'}.to_json) + { + headers: { + "Access-Control-Allow-Headers": "*, Authorization", + "Access-Control-Allow-Origin": "https://3000-papicool-awsbootcampcru-gxorw1rfn3z.ws-eu98.gitpod.io", + "Access-Control-Allow-Methods": "OPTIONS,GET,POST" + }, + statusCode: 200 + } + else + token = event['headers']['authorization'].split(' ')[1] + puts({step: 'presignedurl', access_token: token}.to_json) + + body_hash = JSON.parse(event["body"]) + extension = body_hash["extension"] + + decoded_token = JWT.decode token, nil, false + cognito_user_uuid = decoded_token[0]['sub'] + + s3 = Aws::S3::Resource.new + bucket_name = ENV["UPLOADS_BUCKET_NAME"] + object_key = "#{cognito_user_uuid}.#{extension}" + + puts({object_key: object_key}.to_json) + + obj = s3.bucket(bucket_name).object(object_key) + url = obj.presigned_url(:put, expires_in: 60 * 5) + url # this is the data that will be returned + body = {url: url}.to_json + { + headers: { + "Access-Control-Allow-Headers": "*, Authorization", + "Access-Control-Allow-Origin": "https://3000-papicool-awsbootcampcru-gxorw1rfn3z.ws-eu98.gitpod.io", + "Access-Control-Allow-Methods": "OPTIONS,GET,POST" + }, + statusCode: 200, + body: body + } + end # if +end # def handler \ No newline at end of file diff --git a/aws/lambdas/cruddur-upload-avatar/lore.jpg b/aws/lambdas/cruddur-upload-avatar/lore.jpg new file mode 100644 index 000000000..a9b29307c Binary files /dev/null and b/aws/lambdas/cruddur-upload-avatar/lore.jpg differ diff --git a/aws/lambdas/lambda-authorizer/index.js b/aws/lambdas/lambda-authorizer/index.js new file mode 100644 index 000000000..dd36f5cfb --- /dev/null +++ b/aws/lambdas/lambda-authorizer/index.js @@ -0,0 +1,30 @@ +"use strict"; +const { CognitoJwtVerifier } = require("aws-jwt-verify"); +//const { assertStringEquals } = require("aws-jwt-verify/assert"); + +const jwtVerifier = CognitoJwtVerifier.create({ + userPoolId: process.env.USER_POOL_ID, + tokenUse: "access", + clientId: process.env.CLIENT_ID//, + //customJwtCheck: ({ payload }) => { + // assertStringEquals("e-mail", payload["email"], process.env.USER_EMAIL); + //}, +}); + +exports.handler = async (event) => { + console.log("request:", JSON.stringify(event, undefined, 2)); + + const jwt = event.headers.authorization; + try { + const payload = await jwtVerifier.verify(jwt); + console.log("Access allowed. JWT payload:", payload); + } catch (err) { + console.error("Access forbidden:", err); + return { + isAuthorized: false, + }; + } + return { + isAuthorized: true, + }; +}; \ No newline at end of file diff --git a/aws/lambdas/lambda-authorizer/package-lock.json b/aws/lambdas/lambda-authorizer/package-lock.json new file mode 100644 index 000000000..717e110c6 --- /dev/null +++ b/aws/lambdas/lambda-authorizer/package-lock.json @@ -0,0 +1,20 @@ +{ + "name": "lambda-authorizer", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "aws-jwt-verify": "^4.0.0" + } + }, + "node_modules/aws-jwt-verify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/aws-jwt-verify/-/aws-jwt-verify-4.0.0.tgz", + "integrity": "sha512-1kCv+Ub3jBaQ6HnIjfAXswjp7xD0LO4GxwbQZ/o9IoJpb8/ZBUhHu5GQ4k2O7jOVTS/KOz86uw4NV71V3s6V3g==", + "engines": { + "node": ">=14.0.0" + } + } + } +} diff --git a/aws/lambdas/lambda-authorizer/package.json b/aws/lambdas/lambda-authorizer/package.json new file mode 100644 index 000000000..0e964c1ae --- /dev/null +++ b/aws/lambdas/lambda-authorizer/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "aws-jwt-verify": "^4.0.0" + } +} diff --git a/aws/lambdas/process-images/example.json b/aws/lambdas/process-images/example.json new file mode 100644 index 000000000..add950e1a --- /dev/null +++ b/aws/lambdas/process-images/example.json @@ -0,0 +1,38 @@ +{ + "Records": [ + { + "eventVersion": "2.1", + "eventSource": "aws:s3", + "awsRegion": "us-east-1", + "eventTime": "2023-04-04T12:34:56.000Z", + "eventName": "ObjectCreated:Put", + "userIdentity": { + "principalId": "EXAMPLE" + }, + "requestParameters": { + "sourceIPAddress": "127.0.0.1" + }, + "responseElements": { + "x-amz-request-id": "EXAMPLE123456789", + "x-amz-id-2": "EXAMPLE123/abcdefghijklmno/123456789" + }, + "s3": { + "s3SchemaVersion": "1.0", + "configurationId": "EXAMPLEConfig", + "bucket": { + "name": "assets.cruddursn.net", + "ownerIdentity": { + "principalId": "EXAMPLE" + }, + "arn": "arn:aws:s3:::assets.cruddursn.net" + }, + "object": { + "key": "avatars/original/data.jpg", + "size": 1024, + "eTag": "EXAMPLEETAG", + "sequencer": "EXAMPLESEQUENCER" + } + } + } + ] + } \ No newline at end of file diff --git a/aws/lambdas/process-images/index.js b/aws/lambdas/process-images/index.js new file mode 100644 index 000000000..b501666fc --- /dev/null +++ b/aws/lambdas/process-images/index.js @@ -0,0 +1,32 @@ +const process = require('process'); +const {getClient, getOriginalImage, processImage, uploadProcessedImage} = require('./s3-image-processing.js'); +const path = require('path'); + +const bucketName = process.env.DEST_BUCKET_NAME +const folderInput = process.env.FOLDER_INPUT +const folderOutput = process.env.FOLDER_OUTPUT +const width = parseInt(process.env.PROCESS_WIDTH) +const height = parseInt(process.env.PROCESS_HEIGHT) + +client = getClient(); + +exports.handler = async (event) => { + console.log('event',event) + + const srcBucket = event.Records[0].s3.bucket.name; + const srcKey = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, ' ')); + console.log('srcBucket',srcBucket) + console.log('srcKey',srcKey) + + const dstBucket = bucketName; + + filename = path.parse(srcKey).name + const dstKey = `${folderOutput}/${filename}.jpg` + //const dstKey = srcKey.replace(folderInput,folderOutput) + console.log('dstBucket',dstBucket) + console.log('dstKey',dstKey) + + const originalImage = await getOriginalImage(client,srcBucket,srcKey) + const processedImage = await processImage(originalImage,width,height) + await uploadProcessedImage(dstBucket,dstKey,processedImage) +}; \ No newline at end of file diff --git a/aws/lambdas/process-images/package-lock.json b/aws/lambdas/process-images/package-lock.json new file mode 100644 index 000000000..a2124c374 --- /dev/null +++ b/aws/lambdas/process-images/package-lock.json @@ -0,0 +1,1852 @@ +{ + "name": "process-images", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "process-images", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@aws-sdk/client-s3": "^3.335.0", + "sharp": "^0.32.1" + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-3.0.0.tgz", + "integrity": "sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==", + "dependencies": { + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/crc32/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/crc32c": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-3.0.0.tgz", + "integrity": "sha512-ENNPPManmnVJ4BTXlOjAgD7URidbAznURqD0KvfREyc4o20DPYdEldU1f5cQ7Jbj0CJJSPaMIk/9ZshdB3210w==", + "dependencies": { + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/crc32c/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/ie11-detection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-3.0.0.tgz", + "integrity": "sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q==", + "dependencies": { + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/ie11-detection/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-3.0.0.tgz", + "integrity": "sha512-NJth5c997GLHs6nOYTzFKTbYdMNA6/1XlKVgnZoaZcQ7z7UJlOgj2JdbHE8tiYLS3fzXNCguct77SPGat2raSw==", + "dependencies": { + "@aws-crypto/ie11-detection": "^3.0.0", + "@aws-crypto/supports-web-crypto": "^3.0.0", + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-3.0.0.tgz", + "integrity": "sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ==", + "dependencies": { + "@aws-crypto/ie11-detection": "^3.0.0", + "@aws-crypto/sha256-js": "^3.0.0", + "@aws-crypto/supports-web-crypto": "^3.0.0", + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-3.0.0.tgz", + "integrity": "sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ==", + "dependencies": { + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/sha256-js/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-3.0.0.tgz", + "integrity": "sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg==", + "dependencies": { + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/supports-web-crypto/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/util": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-3.0.0.tgz", + "integrity": "sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w==", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/util/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-sdk/abort-controller": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/abort-controller/-/abort-controller-3.329.0.tgz", + "integrity": "sha512-hzrjPNQcJoSPe0oS20V5i98oiEZSM3mKNiR6P3xHTHTPI/F23lyjGZ+/CSkCmJbSWfGZ5sHZZcU6AWuS7xBdTw==", + "dependencies": { + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/chunked-blob-reader": { + "version": "3.310.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/chunked-blob-reader/-/chunked-blob-reader-3.310.0.tgz", + "integrity": "sha512-CrJS3exo4mWaLnWxfCH+w88Ou0IcAZSIkk4QbmxiHl/5Dq705OLoxf4385MVyExpqpeVJYOYQ2WaD8i/pQZ2fg==", + "dependencies": { + "tslib": "^2.5.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.335.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.335.0.tgz", + "integrity": "sha512-v6uyLvW6Ym/oLrBNbhsQvrVV76I9efUODHMISMVq8erHOgUiPsOyhZdfTlt7dx1x6gAmzZoP4CH/HFeC8mfoIg==", + "dependencies": { + "@aws-crypto/sha1-browser": "3.0.0", + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/client-sts": "3.335.0", + "@aws-sdk/config-resolver": "3.329.0", + "@aws-sdk/credential-provider-node": "3.335.0", + "@aws-sdk/eventstream-serde-browser": "3.329.0", + "@aws-sdk/eventstream-serde-config-resolver": "3.329.0", + "@aws-sdk/eventstream-serde-node": "3.329.0", + "@aws-sdk/fetch-http-handler": "3.329.0", + "@aws-sdk/hash-blob-browser": "3.329.0", + "@aws-sdk/hash-node": "3.329.0", + "@aws-sdk/hash-stream-node": "3.329.0", + "@aws-sdk/invalid-dependency": "3.329.0", + "@aws-sdk/md5-js": "3.329.0", + "@aws-sdk/middleware-bucket-endpoint": "3.329.0", + "@aws-sdk/middleware-content-length": "3.329.0", + "@aws-sdk/middleware-endpoint": "3.329.0", + "@aws-sdk/middleware-expect-continue": "3.329.0", + "@aws-sdk/middleware-flexible-checksums": "3.331.0", + "@aws-sdk/middleware-host-header": "3.329.0", + "@aws-sdk/middleware-location-constraint": "3.329.0", + "@aws-sdk/middleware-logger": "3.329.0", + "@aws-sdk/middleware-recursion-detection": "3.329.0", + "@aws-sdk/middleware-retry": "3.329.0", + "@aws-sdk/middleware-sdk-s3": "3.329.0", + "@aws-sdk/middleware-serde": "3.329.0", + "@aws-sdk/middleware-signing": "3.329.0", + "@aws-sdk/middleware-ssec": "3.329.0", + "@aws-sdk/middleware-stack": "3.329.0", + "@aws-sdk/middleware-user-agent": "3.332.0", + "@aws-sdk/node-config-provider": "3.329.0", + "@aws-sdk/node-http-handler": "3.329.0", + "@aws-sdk/signature-v4-multi-region": "3.329.0", + "@aws-sdk/smithy-client": "3.329.0", + "@aws-sdk/types": "3.329.0", + "@aws-sdk/url-parser": "3.329.0", + "@aws-sdk/util-base64": "3.310.0", + "@aws-sdk/util-body-length-browser": "3.310.0", + "@aws-sdk/util-body-length-node": "3.310.0", + "@aws-sdk/util-defaults-mode-browser": "3.329.0", + "@aws-sdk/util-defaults-mode-node": "3.329.0", + "@aws-sdk/util-endpoints": "3.332.0", + "@aws-sdk/util-retry": "3.329.0", + "@aws-sdk/util-stream-browser": "3.329.0", + "@aws-sdk/util-stream-node": "3.331.0", + "@aws-sdk/util-user-agent-browser": "3.329.0", + "@aws-sdk/util-user-agent-node": "3.329.0", + "@aws-sdk/util-utf8": "3.310.0", + "@aws-sdk/util-waiter": "3.329.0", + "@aws-sdk/xml-builder": "3.310.0", + "@smithy/protocol-http": "^1.0.1", + "@smithy/types": "^1.0.0", + "fast-xml-parser": "4.1.2", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.335.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.335.0.tgz", + "integrity": "sha512-tMvOq366QeMzcrRTDhMwuCFirntANX25qi4U32NDl//ny/7V6+7WK8Hf8lRAHvWnY9eT4RdNklXESo2yxlPyUg==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/config-resolver": "3.329.0", + "@aws-sdk/fetch-http-handler": "3.329.0", + "@aws-sdk/hash-node": "3.329.0", + "@aws-sdk/invalid-dependency": "3.329.0", + "@aws-sdk/middleware-content-length": "3.329.0", + "@aws-sdk/middleware-endpoint": "3.329.0", + "@aws-sdk/middleware-host-header": "3.329.0", + "@aws-sdk/middleware-logger": "3.329.0", + "@aws-sdk/middleware-recursion-detection": "3.329.0", + "@aws-sdk/middleware-retry": "3.329.0", + "@aws-sdk/middleware-serde": "3.329.0", + "@aws-sdk/middleware-stack": "3.329.0", + "@aws-sdk/middleware-user-agent": "3.332.0", + "@aws-sdk/node-config-provider": "3.329.0", + "@aws-sdk/node-http-handler": "3.329.0", + "@aws-sdk/smithy-client": "3.329.0", + "@aws-sdk/types": "3.329.0", + "@aws-sdk/url-parser": "3.329.0", + "@aws-sdk/util-base64": "3.310.0", + "@aws-sdk/util-body-length-browser": "3.310.0", + "@aws-sdk/util-body-length-node": "3.310.0", + "@aws-sdk/util-defaults-mode-browser": "3.329.0", + "@aws-sdk/util-defaults-mode-node": "3.329.0", + "@aws-sdk/util-endpoints": "3.332.0", + "@aws-sdk/util-retry": "3.329.0", + "@aws-sdk/util-user-agent-browser": "3.329.0", + "@aws-sdk/util-user-agent-node": "3.329.0", + "@aws-sdk/util-utf8": "3.310.0", + "@smithy/protocol-http": "^1.0.1", + "@smithy/types": "^1.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.335.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.335.0.tgz", + "integrity": "sha512-szaMq6tDznGy4EuidxPqhZKqEnfGJfoPWUpoFlhXsgZXinZY/vJlJ4G5l6nikhnS3omq3C3WPGJXMKF1ejVXKg==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/config-resolver": "3.329.0", + "@aws-sdk/fetch-http-handler": "3.329.0", + "@aws-sdk/hash-node": "3.329.0", + "@aws-sdk/invalid-dependency": "3.329.0", + "@aws-sdk/middleware-content-length": "3.329.0", + "@aws-sdk/middleware-endpoint": "3.329.0", + "@aws-sdk/middleware-host-header": "3.329.0", + "@aws-sdk/middleware-logger": "3.329.0", + "@aws-sdk/middleware-recursion-detection": "3.329.0", + "@aws-sdk/middleware-retry": "3.329.0", + "@aws-sdk/middleware-serde": "3.329.0", + "@aws-sdk/middleware-stack": "3.329.0", + "@aws-sdk/middleware-user-agent": "3.332.0", + "@aws-sdk/node-config-provider": "3.329.0", + "@aws-sdk/node-http-handler": "3.329.0", + "@aws-sdk/smithy-client": "3.329.0", + "@aws-sdk/types": "3.329.0", + "@aws-sdk/url-parser": "3.329.0", + "@aws-sdk/util-base64": "3.310.0", + "@aws-sdk/util-body-length-browser": "3.310.0", + "@aws-sdk/util-body-length-node": "3.310.0", + "@aws-sdk/util-defaults-mode-browser": "3.329.0", + "@aws-sdk/util-defaults-mode-node": "3.329.0", + "@aws-sdk/util-endpoints": "3.332.0", + "@aws-sdk/util-retry": "3.329.0", + "@aws-sdk/util-user-agent-browser": "3.329.0", + "@aws-sdk/util-user-agent-node": "3.329.0", + "@aws-sdk/util-utf8": "3.310.0", + "@smithy/protocol-http": "^1.0.1", + "@smithy/types": "^1.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-sts": { + "version": "3.335.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.335.0.tgz", + "integrity": "sha512-W+LW1b/3auvGg3EmFeJiraMyH/nxX7qIEBEPPWlJKphGSJAt0l08o8glL2O8s+o2oYWCB2DmgdWyOt1D6YRldQ==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/config-resolver": "3.329.0", + "@aws-sdk/credential-provider-node": "3.335.0", + "@aws-sdk/fetch-http-handler": "3.329.0", + "@aws-sdk/hash-node": "3.329.0", + "@aws-sdk/invalid-dependency": "3.329.0", + "@aws-sdk/middleware-content-length": "3.329.0", + "@aws-sdk/middleware-endpoint": "3.329.0", + "@aws-sdk/middleware-host-header": "3.329.0", + "@aws-sdk/middleware-logger": "3.329.0", + "@aws-sdk/middleware-recursion-detection": "3.329.0", + "@aws-sdk/middleware-retry": "3.329.0", + "@aws-sdk/middleware-sdk-sts": "3.329.0", + "@aws-sdk/middleware-serde": "3.329.0", + "@aws-sdk/middleware-signing": "3.329.0", + "@aws-sdk/middleware-stack": "3.329.0", + "@aws-sdk/middleware-user-agent": "3.332.0", + "@aws-sdk/node-config-provider": "3.329.0", + "@aws-sdk/node-http-handler": "3.329.0", + "@aws-sdk/smithy-client": "3.329.0", + "@aws-sdk/types": "3.329.0", + "@aws-sdk/url-parser": "3.329.0", + "@aws-sdk/util-base64": "3.310.0", + "@aws-sdk/util-body-length-browser": "3.310.0", + "@aws-sdk/util-body-length-node": "3.310.0", + "@aws-sdk/util-defaults-mode-browser": "3.329.0", + "@aws-sdk/util-defaults-mode-node": "3.329.0", + "@aws-sdk/util-endpoints": "3.332.0", + "@aws-sdk/util-retry": "3.329.0", + "@aws-sdk/util-user-agent-browser": "3.329.0", + "@aws-sdk/util-user-agent-node": "3.329.0", + "@aws-sdk/util-utf8": "3.310.0", + "@smithy/protocol-http": "^1.0.1", + "@smithy/types": "^1.0.0", + "fast-xml-parser": "4.1.2", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/config-resolver": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/config-resolver/-/config-resolver-3.329.0.tgz", + "integrity": "sha512-Oj6eiT3q+Jn685yvUrfRi8PhB3fb81hasJqdrsEivA8IP8qAgnVUTJzXsh8O2UX8UM2MF6A1gTgToSgneJuw2Q==", + "dependencies": { + "@aws-sdk/types": "3.329.0", + "@aws-sdk/util-config-provider": "3.310.0", + "@aws-sdk/util-middleware": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.329.0.tgz", + "integrity": "sha512-B4orC9hMt9hG82vAR0TAnQqjk6cFDbO2S14RdzUj2n2NPlGWW4Blkv3NTo86K0lq011VRhtqaLcuTwn5EJD5Sg==", + "dependencies": { + "@aws-sdk/property-provider": "3.329.0", + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-imds": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.329.0.tgz", + "integrity": "sha512-ggPlnd7QROPTid0CwT01TYYGvstRRTpzTGsQ/B31wkh30IrRXE81W3S4xrOYuqQD3u0RnflSxnvhs+EayJEYjg==", + "dependencies": { + "@aws-sdk/node-config-provider": "3.329.0", + "@aws-sdk/property-provider": "3.329.0", + "@aws-sdk/types": "3.329.0", + "@aws-sdk/url-parser": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.335.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.335.0.tgz", + "integrity": "sha512-3AsKlpAnddLYGEZkfT8ZsAB+1WySSzbLA2eoJTW80nKWVUnvYV6gq/sNXEY43i7T2rOXmblJHbTuMAWA1ruMFg==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.329.0", + "@aws-sdk/credential-provider-imds": "3.329.0", + "@aws-sdk/credential-provider-process": "3.329.0", + "@aws-sdk/credential-provider-sso": "3.335.0", + "@aws-sdk/credential-provider-web-identity": "3.329.0", + "@aws-sdk/property-provider": "3.329.0", + "@aws-sdk/shared-ini-file-loader": "3.329.0", + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.335.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.335.0.tgz", + "integrity": "sha512-aIelF8GBTbXuVntpeEdnbcajYtkO01OfSmXb08JxvtQ0tPCWY6SbLpNHUAIfBW1OVkm5E7SX+Hc1tawxq9IKAA==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.329.0", + "@aws-sdk/credential-provider-imds": "3.329.0", + "@aws-sdk/credential-provider-ini": "3.335.0", + "@aws-sdk/credential-provider-process": "3.329.0", + "@aws-sdk/credential-provider-sso": "3.335.0", + "@aws-sdk/credential-provider-web-identity": "3.329.0", + "@aws-sdk/property-provider": "3.329.0", + "@aws-sdk/shared-ini-file-loader": "3.329.0", + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.329.0.tgz", + "integrity": "sha512-5oO220qoFc2pMdZDQa6XN/mVhp669I3+LqMbbscGtX/UgLJPSOb7YzPld9Wjv12L5rf+sD3G1PF3LZXO0vKLFA==", + "dependencies": { + "@aws-sdk/property-provider": "3.329.0", + "@aws-sdk/shared-ini-file-loader": "3.329.0", + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.335.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.335.0.tgz", + "integrity": "sha512-omEF3m9Vy18QfuGuGx/48MaiKDOdvMZKZI9FKyQxFIwfqRyhmF2jzQ7070FD/E9YakscOZ0hSeYEPJ7nkJa8ww==", + "dependencies": { + "@aws-sdk/client-sso": "3.335.0", + "@aws-sdk/property-provider": "3.329.0", + "@aws-sdk/shared-ini-file-loader": "3.329.0", + "@aws-sdk/token-providers": "3.335.0", + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.329.0.tgz", + "integrity": "sha512-lcEibZD7AlutCacpQ6DyNUqElZJDq+ylaIo5a8MH9jGh7Pg2WpDg0Sy+B6FbGCkVn4eIjdHxeX54JM245nhESg==", + "dependencies": { + "@aws-sdk/property-provider": "3.329.0", + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/eventstream-codec": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-codec/-/eventstream-codec-3.329.0.tgz", + "integrity": "sha512-1r+6MNfye0za35FNLxMR5V9zpKY1lyzwySyu7o7aj8lnStBaCcjOEe7iHboP/z3DH73KJbxR++O2N+UC/XHFrg==", + "dependencies": { + "@aws-crypto/crc32": "3.0.0", + "@aws-sdk/types": "3.329.0", + "@aws-sdk/util-hex-encoding": "3.310.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@aws-sdk/eventstream-serde-browser": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-serde-browser/-/eventstream-serde-browser-3.329.0.tgz", + "integrity": "sha512-oWFSn4o6sxlbFF0AIuDJYf7N0fkiOyWvYgRW3VTX9FSbd66f/KnDspdxIasaDPDUzJl5YRMwUvQbPWw8y9ZQfQ==", + "dependencies": { + "@aws-sdk/eventstream-serde-universal": "3.329.0", + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/eventstream-serde-config-resolver": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-3.329.0.tgz", + "integrity": "sha512-iQguqvTtxWXAIniaWmmAO0Qy8080fqnS309p9jbYzz7KaT90sNSCX+CxGFHPy5F0QY36uklDdHn1d1fwWTZciA==", + "dependencies": { + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/eventstream-serde-node": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-serde-node/-/eventstream-serde-node-3.329.0.tgz", + "integrity": "sha512-+DFia0wdZiHpdOKjBcl1baZjtzPKf4U4MvOpsUpC6CeW1kSy0hoikKzJstNvRb1qxrTSamElT4gKkMHxxVhPBQ==", + "dependencies": { + "@aws-sdk/eventstream-serde-universal": "3.329.0", + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/eventstream-serde-universal": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-serde-universal/-/eventstream-serde-universal-3.329.0.tgz", + "integrity": "sha512-n9UzW6HKAhVD5wuz3FMC1ew3VI/vUvRSPXGUpKReMiR2z+YyjmuW8UM4nn7q6i7A/I4QHBt1TC/ax/J2yupgPg==", + "dependencies": { + "@aws-sdk/eventstream-codec": "3.329.0", + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/fetch-http-handler": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.329.0.tgz", + "integrity": "sha512-9jfIeJhYCcTX4ScXOueRTB3S/tVce0bRsKxKDP0PnTxnGYOwKXoM9lAPmiYItzYmQ/+QzjTI8xfkA9Usz2SK/Q==", + "dependencies": { + "@aws-sdk/protocol-http": "3.329.0", + "@aws-sdk/querystring-builder": "3.329.0", + "@aws-sdk/types": "3.329.0", + "@aws-sdk/util-base64": "3.310.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@aws-sdk/hash-blob-browser": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/hash-blob-browser/-/hash-blob-browser-3.329.0.tgz", + "integrity": "sha512-F5HwXYYSpJtUJqmCRKbz/xwDdOyxKpu69TlfsliECLvAQiQGMh2GO1wGm7grolgTROVVqLYRKk2TSJl/WBg1pw==", + "dependencies": { + "@aws-sdk/chunked-blob-reader": "3.310.0", + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@aws-sdk/hash-node": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/hash-node/-/hash-node-3.329.0.tgz", + "integrity": "sha512-6RmnWXNWpi7yAs0oRDQlkMn2wfXOStr/8kTCgiAiqrk1KopGSBkC2veKiKRSfv02FTd1yV/ISqYNIRqW1VLyxg==", + "dependencies": { + "@aws-sdk/types": "3.329.0", + "@aws-sdk/util-buffer-from": "3.310.0", + "@aws-sdk/util-utf8": "3.310.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/hash-stream-node": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/hash-stream-node/-/hash-stream-node-3.329.0.tgz", + "integrity": "sha512-blSZcb/hJyw3c1bH2Hc1aRoRgruNhRK/qc2svq5kXQFW+qBI5O4fwJayKSdo62/Wh2ejR/N06teYQ9haQLVJEA==", + "dependencies": { + "@aws-sdk/types": "3.329.0", + "@aws-sdk/util-utf8": "3.310.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/invalid-dependency": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/invalid-dependency/-/invalid-dependency-3.329.0.tgz", + "integrity": "sha512-UXynGusDxN/HxLma5ByJ7u+XnuMd47NbHOjJgYsaAjb1CVZT7hEPXOB+mcZ+Ku7To5SCOKu2QbRn7m4bGespBg==", + "dependencies": { + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@aws-sdk/is-array-buffer": { + "version": "3.310.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/is-array-buffer/-/is-array-buffer-3.310.0.tgz", + "integrity": "sha512-urnbcCR+h9NWUnmOtet/s4ghvzsidFmspfhYaHAmSRdy9yDjdjBJMFjjsn85A1ODUktztm+cVncXjQ38WCMjMQ==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/md5-js": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/md5-js/-/md5-js-3.329.0.tgz", + "integrity": "sha512-newSeHd+CO2hNmXhQOrUk5Y1hH7BsJ5J4IldcqHKY93UwWqvQNiepRowSa2bV5EuS1qx3kfXhD66PFNRprrIlQ==", + "dependencies": { + "@aws-sdk/types": "3.329.0", + "@aws-sdk/util-utf8": "3.310.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.329.0.tgz", + "integrity": "sha512-h3/JdK+FmJ/nxLcd8QciJYLy0B4QRsYqqxSffXJ7DYlDjEhUgvVpfGdVgAYHrTtOP8rHSG/K7l7iY7QqTaZpuw==", + "dependencies": { + "@aws-sdk/protocol-http": "3.329.0", + "@aws-sdk/types": "3.329.0", + "@aws-sdk/util-arn-parser": "3.310.0", + "@aws-sdk/util-config-provider": "3.310.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-content-length": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-content-length/-/middleware-content-length-3.329.0.tgz", + "integrity": "sha512-7kCd+CvY/4KbyXB0uyL7jCwPjMi2yERMALFdEH9dsUciwmxIQT6eSc4aF6wImC4UrbafaqmXvvHErABKMVBTKA==", + "dependencies": { + "@aws-sdk/protocol-http": "3.329.0", + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-endpoint": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-endpoint/-/middleware-endpoint-3.329.0.tgz", + "integrity": "sha512-hdJRoNdCM0BT4W+rrtee+kfFRgGPGXQDgtbIQlf/FuuuYz2sdef7/SYWr0mxuncnVBW5WkYSPP8h6q07whSKbg==", + "dependencies": { + "@aws-sdk/middleware-serde": "3.329.0", + "@aws-sdk/types": "3.329.0", + "@aws-sdk/url-parser": "3.329.0", + "@aws-sdk/util-middleware": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.329.0.tgz", + "integrity": "sha512-E/Jp2KijdR/BwF4s899xcSN4/bbHqYznwmBRL5PiHI+HImA6aZ11qTP8kPt5U5p0l2j5iTmW3FpMnByQKJP5Dw==", + "dependencies": { + "@aws-sdk/protocol-http": "3.329.0", + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.331.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.331.0.tgz", + "integrity": "sha512-rdRa4yvyqSQ/HDCh4p1Glv8Y/uRNuIwmOG4nDuL6/GYK1BQdpUpbgrhsszPormku10SnbAdsaWGmVhy3qlUSCQ==", + "dependencies": { + "@aws-crypto/crc32": "3.0.0", + "@aws-crypto/crc32c": "3.0.0", + "@aws-sdk/is-array-buffer": "3.310.0", + "@aws-sdk/protocol-http": "3.329.0", + "@aws-sdk/types": "3.329.0", + "@aws-sdk/util-utf8": "3.310.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.329.0.tgz", + "integrity": "sha512-JrHeUdTIpTCfXDo9JpbAbZTS1x4mt63CCytJRq0mpWp+FlP9hjckBcNxWdR/wSKEzP9pDRnTri638BOwWH7O8w==", + "dependencies": { + "@aws-sdk/protocol-http": "3.329.0", + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.329.0.tgz", + "integrity": "sha512-iUTkyXyhchqoEPkdMZSkHhRQmXe0El1+r9oOw8y9JN6IY0T1bnaqUlerGXzb/tQUeENk9OXYuvDHExegHjEWug==", + "dependencies": { + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.329.0.tgz", + "integrity": "sha512-lKeeTXsYC1NiwmxrXsZepcwNXPoQxTNNbeD1qaCELPGK2cJlrGoeAP2YRWzpwO2kNZWrDLaGAPT/EUEhqw+d1w==", + "dependencies": { + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.329.0.tgz", + "integrity": "sha512-0/TYOJwrj1Z8s+Y7thibD23hggBq/K/01NwPk32CwWG/G+1vWozs5DefknEl++w0vuV+39pkY4KHI8m/+wOCpg==", + "dependencies": { + "@aws-sdk/protocol-http": "3.329.0", + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-retry": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-retry/-/middleware-retry-3.329.0.tgz", + "integrity": "sha512-cB3D7GlhHUcHGOlygOYxD9cPhwsTYEAMcohK38An8+RHNp6VQEWezzLFCmHVKUSeCQ+wkjZfPA40jOG0rbjSgQ==", + "dependencies": { + "@aws-sdk/protocol-http": "3.329.0", + "@aws-sdk/service-error-classification": "3.329.0", + "@aws-sdk/types": "3.329.0", + "@aws-sdk/util-middleware": "3.329.0", + "@aws-sdk/util-retry": "3.329.0", + "tslib": "^2.5.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.329.0.tgz", + "integrity": "sha512-Uo8dLXLDpOb3BnLVl0mkTPiVXlNzNGOXOVtpihvYhF2Z+hGFJW1Ro3aUDbVEsFHu753r2Lss4dLiq1fzREeBKA==", + "dependencies": { + "@aws-sdk/protocol-http": "3.329.0", + "@aws-sdk/types": "3.329.0", + "@aws-sdk/util-arn-parser": "3.310.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-sts": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.329.0.tgz", + "integrity": "sha512-bqtZuhkH8pANb2Gb4FEM1p27o+BoDBmVhEWm8sWH+APsyOor3jc6eUG2GxkfoO6D5tGNIuyCC/GuvW9XDIe4Kg==", + "dependencies": { + "@aws-sdk/middleware-signing": "3.329.0", + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-serde": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-serde/-/middleware-serde-3.329.0.tgz", + "integrity": "sha512-tvM9NdPuRPCozPjTGNOeYZeLlyx3BcEyajrkRorCRf1YzG/mXdB6I1stote7i4q1doFtYTz0sYL8bqW3LUPn9A==", + "dependencies": { + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-signing": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.329.0.tgz", + "integrity": "sha512-bL1nI+EUcF5B1ipwDXxiKL+Uw02Mbt/TNX54PbzunBGZIyO6DZG/H+M3U296bYbvPlwlZhp26O830g6K7VEWsA==", + "dependencies": { + "@aws-sdk/property-provider": "3.329.0", + "@aws-sdk/protocol-http": "3.329.0", + "@aws-sdk/signature-v4": "3.329.0", + "@aws-sdk/types": "3.329.0", + "@aws-sdk/util-middleware": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.329.0.tgz", + "integrity": "sha512-XtDA/P2Sf79scu4a7tG77QC3VLtAGq/pit73x+qwctnI4gBgZlQ+FpE15d89ulntd7rIaD4v6tVU0bAg/L3PIQ==", + "dependencies": { + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-stack": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-stack/-/middleware-stack-3.329.0.tgz", + "integrity": "sha512-2huFLhJ45td2nuiIOjpc9JKJbFNn5CYmw9U8YDITTcydpteRN62CzCpeqroDvF89VOLWxh0ZFtuLCGUr7liSWQ==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.332.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.332.0.tgz", + "integrity": "sha512-rSL1xP4QmcMOsunN1p5ZDR9GT3vvoSCnYa4iPvMSjP8Jx7l4ff/aVctwfZkMs/up12+68Jqwj4TvtaCvCFXdUA==", + "dependencies": { + "@aws-sdk/protocol-http": "3.329.0", + "@aws-sdk/types": "3.329.0", + "@aws-sdk/util-endpoints": "3.332.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/node-config-provider": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/node-config-provider/-/node-config-provider-3.329.0.tgz", + "integrity": "sha512-hg9rGNlkzh8aeR/sQbijrkFx2BIO53j4Z6qDxPNWwSGpl05jri1VHxHx2HZMwgbY6Zy/DSguETN/BL8vdFqyLg==", + "dependencies": { + "@aws-sdk/property-provider": "3.329.0", + "@aws-sdk/shared-ini-file-loader": "3.329.0", + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/node-http-handler": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/node-http-handler/-/node-http-handler-3.329.0.tgz", + "integrity": "sha512-OrjaHjU2ZTPfoHa5DruRvTIbeHH/cc0wvh4ml+FwDpWaPaBpOhLiluhZ3anqX1l5QjrXNiQnL8FxSM5OV/zVCA==", + "dependencies": { + "@aws-sdk/abort-controller": "3.329.0", + "@aws-sdk/protocol-http": "3.329.0", + "@aws-sdk/querystring-builder": "3.329.0", + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/property-provider": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/property-provider/-/property-provider-3.329.0.tgz", + "integrity": "sha512-1cHLTV6yyMGaMSWWDW/p4vTkJ1cc5BOEO+A0eHuAcoSOk+LDe9IKhUG3/ZOvvYKQYcqIj5jjGSni/noXNCl/qw==", + "dependencies": { + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/protocol-http": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/protocol-http/-/protocol-http-3.329.0.tgz", + "integrity": "sha512-0rLEHY6QTHTUUcVxzGbPUSmCKlXWplxT/fcYRh0bcc5MBK4naKfcQft1O6Ajp8uqs/9YPZ7XCVCn90pDeJfeaw==", + "dependencies": { + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/querystring-builder": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/querystring-builder/-/querystring-builder-3.329.0.tgz", + "integrity": "sha512-UWgMKkS5trliaDJG4nPv3onu8Y0aBuwRo7RdIgggguOiU8pU6pq1I113nH2FBNWy+Me1bwf+bcviJh0pCo6bEg==", + "dependencies": { + "@aws-sdk/types": "3.329.0", + "@aws-sdk/util-uri-escape": "3.310.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/querystring-parser": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/querystring-parser/-/querystring-parser-3.329.0.tgz", + "integrity": "sha512-9mkK+FB7snJ2G7H3CqtprDwYIRhzm6jEezffCwUWrC+lbqHBbErbhE9IeU/MKxILmf0RbC2riXEY1MHGspjRrQ==", + "dependencies": { + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/service-error-classification": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/service-error-classification/-/service-error-classification-3.329.0.tgz", + "integrity": "sha512-TSNr0flOcCLe71aPp7MjblKNGsmxpTU4xR5772MDX9Cz9GUTNZCPFtvrcqd+wzEPP/AC7XwNXe8KjoXooZImUQ==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/shared-ini-file-loader": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.329.0.tgz", + "integrity": "sha512-e0hyd75fbjMd4aCoRwpP2/HR+0oScwogErVArIkq3F42c/hyNCQP3sph4JImuXIjuo6HNnpKpf20CEPPhNna8A==", + "dependencies": { + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4/-/signature-v4-3.329.0.tgz", + "integrity": "sha512-9EnLoyOD5nFtCRAp+QRllDgQASCfY7jLHVhwht7jzwE80wE65Z9Ym5Z/mwTd4IyTz/xXfCvcE2VwClsBt0Ybdw==", + "dependencies": { + "@aws-sdk/is-array-buffer": "3.310.0", + "@aws-sdk/types": "3.329.0", + "@aws-sdk/util-hex-encoding": "3.310.0", + "@aws-sdk/util-middleware": "3.329.0", + "@aws-sdk/util-uri-escape": "3.310.0", + "@aws-sdk/util-utf8": "3.310.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.329.0.tgz", + "integrity": "sha512-SiK1ez8Ns61ulDm0MJsTOSGNJNOMNoPgfA9i+Uu/VMCBkotZASuxrcSWW8seQnLEynWLerjUF9CYpCQuCqKn9w==", + "dependencies": { + "@aws-sdk/protocol-http": "3.329.0", + "@aws-sdk/signature-v4": "3.329.0", + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@aws-sdk/signature-v4-crt": "^3.118.0" + }, + "peerDependenciesMeta": { + "@aws-sdk/signature-v4-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/smithy-client": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/smithy-client/-/smithy-client-3.329.0.tgz", + "integrity": "sha512-7E0fGpBKxwFqHHAOqNbgNsHSEmCZLuvmU9yvG9DXKVzrS4P48O/PfOro123WpcFZs3STyOVgH8wjUPftHAVKmg==", + "dependencies": { + "@aws-sdk/middleware-stack": "3.329.0", + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.335.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.335.0.tgz", + "integrity": "sha512-2Hu62xH4/6V+N5JWsPuvxCCmaf/QUnxtz48ClpxzBKM/whrTTkLku8W2fh2MmnzGzAHtT+N97jkIsy2B+onqIg==", + "dependencies": { + "@aws-sdk/client-sso-oidc": "3.335.0", + "@aws-sdk/property-provider": "3.329.0", + "@aws-sdk/shared-ini-file-loader": "3.329.0", + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.329.0.tgz", + "integrity": "sha512-wFBW4yciDfzQBSFmWNaEvHShnSGLMxSu9Lls6EUf6xDMavxSB36bsrVRX6CyAo/W0NeIIyEOW1LclGPgJV1okg==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/url-parser": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/url-parser/-/url-parser-3.329.0.tgz", + "integrity": "sha512-/VcfL7vNJKJGSjYYHVQF3bYCDFs4fSzB7j5qeVDwRdWr870gE7O1Dar+sLWBRKFF3AX+4VzplqzUfpu9t44JVA==", + "dependencies": { + "@aws-sdk/querystring-parser": "3.329.0", + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.310.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.310.0.tgz", + "integrity": "sha512-jL8509owp/xB9+Or0pvn3Fe+b94qfklc2yPowZZIFAkFcCSIdkIglz18cPDWnYAcy9JGewpMS1COXKIUhZkJsA==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-base64": { + "version": "3.310.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-base64/-/util-base64-3.310.0.tgz", + "integrity": "sha512-v3+HBKQvqgdzcbL+pFswlx5HQsd9L6ZTlyPVL2LS9nNXnCcR3XgGz9jRskikRUuUvUXtkSG1J88GAOnJ/apTPg==", + "dependencies": { + "@aws-sdk/util-buffer-from": "3.310.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-body-length-browser": { + "version": "3.310.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-browser/-/util-body-length-browser-3.310.0.tgz", + "integrity": "sha512-sxsC3lPBGfpHtNTUoGXMQXLwjmR0zVpx0rSvzTPAuoVILVsp5AU/w5FphNPxD5OVIjNbZv9KsKTuvNTiZjDp9g==", + "dependencies": { + "tslib": "^2.5.0" + } + }, + "node_modules/@aws-sdk/util-body-length-node": { + "version": "3.310.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-node/-/util-body-length-node-3.310.0.tgz", + "integrity": "sha512-2tqGXdyKhyA6w4zz7UPoS8Ip+7sayOg9BwHNidiGm2ikbDxm1YrCfYXvCBdwaJxa4hJfRVz+aL9e+d3GqPI9pQ==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-buffer-from": { + "version": "3.310.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-buffer-from/-/util-buffer-from-3.310.0.tgz", + "integrity": "sha512-i6LVeXFtGih5Zs8enLrt+ExXY92QV25jtEnTKHsmlFqFAuL3VBeod6boeMXkN2p9lbSVVQ1sAOOYZOHYbYkntw==", + "dependencies": { + "@aws-sdk/is-array-buffer": "3.310.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-config-provider": { + "version": "3.310.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-config-provider/-/util-config-provider-3.310.0.tgz", + "integrity": "sha512-xIBaYo8dwiojCw8vnUcIL4Z5tyfb1v3yjqyJKJWV/dqKUFOOS0U591plmXbM+M/QkXyML3ypon1f8+BoaDExrg==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-defaults-mode-browser": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-browser/-/util-defaults-mode-browser-3.329.0.tgz", + "integrity": "sha512-2iSiy/pzX3OXMhtSxtAzOiEFr3viQEFnYOTeZuiheuyS+cea2L79F6SlZ1110b/nOIU/UOrxxtz83HVad8YFMQ==", + "dependencies": { + "@aws-sdk/property-provider": "3.329.0", + "@aws-sdk/types": "3.329.0", + "bowser": "^2.11.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@aws-sdk/util-defaults-mode-node": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-node/-/util-defaults-mode-node-3.329.0.tgz", + "integrity": "sha512-7A6C7YKjkZtmKtH29isYEtOCbhd7IcXPP8lftN8WAWlLOiZE4gV7PHveagUj7QserJzgRKGwwTQbBj53n18HYg==", + "dependencies": { + "@aws-sdk/config-resolver": "3.329.0", + "@aws-sdk/credential-provider-imds": "3.329.0", + "@aws-sdk/node-config-provider": "3.329.0", + "@aws-sdk/property-provider": "3.329.0", + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.332.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.332.0.tgz", + "integrity": "sha512-nQx7AiOroMU2hj6h+umWOSZ+WECwxupaxFUK/PPKGW6NY/VdQE6LluYnXOtF5awlr8w1nPksT0Lq05PZutMDLA==", + "dependencies": { + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-hex-encoding": { + "version": "3.310.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.310.0.tgz", + "integrity": "sha512-sVN7mcCCDSJ67pI1ZMtk84SKGqyix6/0A1Ab163YKn+lFBQRMKexleZzpYzNGxYzmQS6VanP/cfU7NiLQOaSfA==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.310.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.310.0.tgz", + "integrity": "sha512-qo2t/vBTnoXpjKxlsC2e1gBrRm80M3bId27r0BRB2VniSSe7bL1mmzM+/HFtujm0iAxtPM+aLEflLJlJeDPg0w==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-middleware": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-middleware/-/util-middleware-3.329.0.tgz", + "integrity": "sha512-RhBOBaxzkTUghi4MSqr8S5qeeBCjgJ0XPJ6jIYkVkj1saCmqkuZCgl3zFaYdyhdxxPV6nflkFer+1HUoqT+Fqw==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-retry": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-retry/-/util-retry-3.329.0.tgz", + "integrity": "sha512-+3VQ9HZLinysnmryUs9Xjt1YVh4TYYHLt30ilu4iUnIHFQoamdzIbRCWseSVFPCxGroen9M9qmAleAsytHEKuA==", + "dependencies": { + "@aws-sdk/service-error-classification": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@aws-sdk/util-stream-browser": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-stream-browser/-/util-stream-browser-3.329.0.tgz", + "integrity": "sha512-UF1fJNfgrdJLMxn8ZlfPkYdv7hoLvVgSk3GHgxYA4OQs5zKCzeZgVrbxtE147LxWwJbxi3Qf04vnaEHwzVESpg==", + "dependencies": { + "@aws-sdk/fetch-http-handler": "3.329.0", + "@aws-sdk/types": "3.329.0", + "@aws-sdk/util-base64": "3.310.0", + "@aws-sdk/util-hex-encoding": "3.310.0", + "@aws-sdk/util-utf8": "3.310.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@aws-sdk/util-stream-node": { + "version": "3.331.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-stream-node/-/util-stream-node-3.331.0.tgz", + "integrity": "sha512-5YUatdh4vgkv7VFY+lSkF+b+6EFkiHvy+dlucfGoJEOcEzuA/NBZYebWbcJ5TiR6z3cQdA23OTyZz3ZofZY1hw==", + "dependencies": { + "@aws-sdk/node-http-handler": "3.329.0", + "@aws-sdk/types": "3.329.0", + "@aws-sdk/util-buffer-from": "3.310.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-uri-escape": { + "version": "3.310.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-uri-escape/-/util-uri-escape-3.310.0.tgz", + "integrity": "sha512-drzt+aB2qo2LgtDoiy/3sVG8w63cgLkqFIa2NFlGpUgHFWTXkqtbgf4L5QdjRGKWhmZsnqkbtL7vkSWEcYDJ4Q==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.329.0.tgz", + "integrity": "sha512-8hLSmMCl8aw2++0Zuba8ELq8FkK6/VNyx470St201IpMn2GMbQMDl/rLolRKiTgji6wc+T3pOTidkJkz8/cIXA==", + "dependencies": { + "@aws-sdk/types": "3.329.0", + "bowser": "^2.11.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.329.0.tgz", + "integrity": "sha512-C50Zaeodc0+psEP+L4WpElrH8epuLWJPVN4hDOTORcM0cSoU2o025Ost9mbcU7UdoHNxF9vitLnzORGN9SHolg==", + "dependencies": { + "@aws-sdk/node-config-provider": "3.329.0", + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/util-utf8": { + "version": "3.310.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8/-/util-utf8-3.310.0.tgz", + "integrity": "sha512-DnLfFT8uCO22uOJc0pt0DsSNau1GTisngBCDw8jQuWT5CqogMJu4b/uXmwEqfj8B3GX6Xsz8zOd6JpRlPftQoA==", + "dependencies": { + "@aws-sdk/util-buffer-from": "3.310.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-utf8-browser": { + "version": "3.259.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz", + "integrity": "sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==", + "dependencies": { + "tslib": "^2.3.1" + } + }, + "node_modules/@aws-sdk/util-waiter": { + "version": "3.329.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-waiter/-/util-waiter-3.329.0.tgz", + "integrity": "sha512-MIGs7snNL0ZV55zo1BDVPlrmbinUGV3260hp6HrW4zUbpYVoeIOGeewtrwAsF6FJ+vpZCxljPBB0X2jYR7Q7ZQ==", + "dependencies": { + "@aws-sdk/abort-controller": "3.329.0", + "@aws-sdk/types": "3.329.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.310.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.310.0.tgz", + "integrity": "sha512-TqELu4mOuSIKQCqj63fGVs86Yh+vBx5nHRpWKNUNhB2nPTpfbziTs5c1X358be3peVWA4wPxW7Nt53KIg1tnNw==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-1.0.1.tgz", + "integrity": "sha512-9OrEn0WfOVtBNYJUjUAn9AOiJ4lzERCJJ/JeZs8E6yajTGxBaFRxUnNBHiNqoDJVg076hY36UmEnPx7xXrvUSg==", + "dependencies": { + "@smithy/types": "^1.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-1.0.0.tgz", + "integrity": "sha512-kc1m5wPBHQCTixwuaOh9vnak/iJm21DrSf9UK6yDE5S3mQQ4u11pqAUiKWnlrZnYkeLfAI9UEHj9OaMT1v5Umg==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bowser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", + "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==" + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-xml-parser": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.1.2.tgz", + "integrity": "sha512-CDYeykkle1LiA/uqQyNwYpFbyF6Axec6YapmpUP+/RHWIoR1zKjocdvNaTsxCxZzQ6v9MLXaSYm9Qq0thv0DHg==", + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + }, + "funding": { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + }, + "node_modules/node-abi": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.40.0.tgz", + "integrity": "sha512-zNy02qivjjRosswoYmPi8hIKJRr8MpQyeKT6qlcq/OnOgA3Rhoae+IYOqsM9V5+JnHWmxKnWOT2GxvtqdtOCXA==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/semver": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", + "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.1.tgz", + "integrity": "sha512-kQTFtj7ldpUqSe8kDxoGLZc1rnMFU0AO2pqbX6pLy3b7Oj8ivJIdoKNwxHVQG2HN6XpHPJqCSM2nsma2gOXvOg==", + "hasInstallScript": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.1", + "node-addon-api": "^6.1.0", + "prebuild-install": "^7.1.1", + "semver": "^7.5.0", + "simple-get": "^4.0.1", + "tar-fs": "^2.1.1", + "tunnel-agent": "^0.6.0" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" + }, + "node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.2.tgz", + "integrity": "sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA==" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/aws/lambdas/process-images/package.json b/aws/lambdas/process-images/package.json new file mode 100644 index 000000000..29c07eb43 --- /dev/null +++ b/aws/lambdas/process-images/package.json @@ -0,0 +1,16 @@ +{ + "name": "process-images", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@aws-sdk/client-s3": "^3.335.0", + "sharp": "^0.32.1" + } +} diff --git a/aws/lambdas/process-images/s3-image-processing.js b/aws/lambdas/process-images/s3-image-processing.js new file mode 100644 index 000000000..04fe6fb01 --- /dev/null +++ b/aws/lambdas/process-images/s3-image-processing.js @@ -0,0 +1,55 @@ +const sharp = require('sharp'); +const { S3Client, PutObjectCommand, GetObjectCommand } = require("@aws-sdk/client-s3"); + +function getClient(){ + const client = new S3Client(); + return client; +} + +async function getOriginalImage(client,srcBucket,srcKey){ + console.log('get==') + const params = { + Bucket: srcBucket, + Key: srcKey + }; + console.log('params',params) + const command = new GetObjectCommand(params); + const response = await client.send(command); + + const chunks = []; + for await (const chunk of response.Body) { + chunks.push(chunk); + } + const buffer = Buffer.concat(chunks); + return buffer; +} + +async function processImage(image,width,height){ + const processedImage = await sharp(image) + .resize(width, height) + .jpeg() + .toBuffer(); + return processedImage; +} + +async function uploadProcessedImage(dstBucket,dstKey,image){ + console.log('upload==') + const params = { + Bucket: dstBucket, + Key: dstKey, + Body: image, + ContentType: 'image/jpeg' + }; + console.log('params',params) + const command = new PutObjectCommand(params); + const response = await client.send(command); + console.log('repsonse',response); + return response; +} + +module.exports = { + getClient: getClient, + getOriginalImage: getOriginalImage, + processImage: processImage, + uploadProcessedImage: uploadProcessedImage +} \ No newline at end of file diff --git a/aws/lambdas/process-images/test.js b/aws/lambdas/process-images/test.js new file mode 100644 index 000000000..ba9c0628b --- /dev/null +++ b/aws/lambdas/process-images/test.js @@ -0,0 +1,18 @@ +const {getClient, getOriginalImage, processImage, uploadProcessedImage} = require('./s3-image-processing.js') + +async function main(){ + client = getClient() + const srcBucket = 'assets.cruddursn.net' + const srcKey = 'avatar/original/data.jpg' + const dstBucket = 'assets.cruddursn.net' + const dstKey = 'avatar/processed/data.png' + const width = 256 + const height = 256 + + const originalImage = await getOriginalImage(client,srcBucket,srcKey) + console.log(originalImage) + const processedImage = await processImage(originalImage,width,height) + await uploadProcessedImage(dstBucket,dstKey,processedImage) +} + +main() \ No newline at end of file diff --git a/aws/policies/cruddur-message-stream-policy.json b/aws/policies/cruddur-message-stream-policy.json new file mode 100644 index 000000000..d77425fbf --- /dev/null +++ b/aws/policies/cruddur-message-stream-policy.json @@ -0,0 +1,18 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "VisualEditor0", + "Effect": "Allow", + "Action": [ + "dynamodb:PutItem", + "dynamodb:DeleteItem", + "dynamodb:Query" + ], + "Resource": [ + "arn:aws:dynamodb:us-east-1:730331264766:table/cruddur-messages", + "arn:aws:dynamodb:us-east-1:730331264766:table/cruddur-messages/index/message-group-sk-index" + ] + } + ] + } \ No newline at end of file diff --git a/aws/policies/s3-upload-presigned-url-policy b/aws/policies/s3-upload-presigned-url-policy new file mode 100644 index 000000000..023ad706c --- /dev/null +++ b/aws/policies/s3-upload-presigned-url-policy @@ -0,0 +1,14 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "VisualEditor0", + "Effect": "Allow", + "Action": [ + "s3:PutObject", + "s3:PutObjectAcl" + ], + "Resource": "arn:aws:s3:::cruddursn-upload-avatars/*" + } + ] +} \ No newline at end of file diff --git a/aws/policies/service-assume-role-execution-policy.json b/aws/policies/service-assume-role-execution-policy.json new file mode 100644 index 000000000..31bb427af --- /dev/null +++ b/aws/policies/service-assume-role-execution-policy.json @@ -0,0 +1,10 @@ +{ + "Version":"2012-10-17", + "Statement":[{ + "Action":["sts:AssumeRole"], + "Effect":"Allow", + "Principal":{ + "Service":["ecs-tasks.amazonaws.com"] + }}] + } + \ No newline at end of file diff --git a/aws/policies/service-execution-policy.json b/aws/policies/service-execution-policy.json new file mode 100644 index 000000000..5326ee4cd --- /dev/null +++ b/aws/policies/service-execution-policy.json @@ -0,0 +1,11 @@ +{ + "Version":"2012-10-17", + "Statement":[{ + "Effect": "Allow", + "Action": [ + "ssm:GetParameters", + "ssm:GetParameter" + ], + "Resource": "arn:aws:ssm:us-east-1:730331264766:parameter/cruddur/backend-flask/*" + }] + } \ No newline at end of file diff --git a/aws/s3/cors.json b/aws/s3/cors.json new file mode 100644 index 000000000..82caef7dc --- /dev/null +++ b/aws/s3/cors.json @@ -0,0 +1,19 @@ +[ + { + "AllowedHeaders": [ + "*" + ], + "AllowedMethods": [ + "PUT" + ], + "AllowedOrigins": [ + "https://*.gitpod.io" + ], + "ExposeHeaders": [ + "x-amz-server-side-encryption", + "x-amz-request-id", + "x-amz-id-2" + ], + "MaxAgeSeconds": 30000 + } +] \ No newline at end of file diff --git a/aws/task-definitions/backend-flask.json b/aws/task-definitions/backend-flask.json new file mode 100644 index 000000000..aa27c19a8 --- /dev/null +++ b/aws/task-definitions/backend-flask.json @@ -0,0 +1,73 @@ +{ + "family": "backend-flask", + "executionRoleArn": "arn:aws:iam::730331264766:role/CruddurServiceExecutionRole", + "taskRoleArn": "arn:aws:iam::730331264766:role/CruddurTaskRole", + "networkMode": "awsvpc", + "cpu": "256", + "memory": "512", + "requiresCompatibilities": [ + "FARGATE" + ], + "containerDefinitions": [ + { + "name": "xray", + "image": "public.ecr.aws/xray/aws-xray-daemon" , + "essential": true, + "user": "1337", + "portMappings": [ + { + "name": "xray", + "containerPort": 2000, + "protocol": "udp" + } + ] + }, + { + "name": "backend-flask", + "image": "730331264766.dkr.ecr.us-east-1.amazonaws.com/backend-flask", + "essential": true, + "healthCheck": { + "command": [ + "CMD-SHELL", + "python /backend-flask/bin/flask/health-check" + ], + "interval": 30, + "timeout": 5, + "retries": 3, + "startPeriod": 60 + }, + "portMappings": [ + { + "name": "backend-flask", + "containerPort": 4567, + "protocol": "tcp", + "appProtocol": "http" + } + ], + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "cruddur", + "awslogs-region": "us-east-1", + "awslogs-stream-prefix": "backend-flask" + } + }, + "environment": [ + {"name": "OTEL_SERVICE_NAME", "value": "backend-flask"}, + {"name": "OTEL_EXPORTER_OTLP_ENDPOINT", "value": "https://api.honeycomb.io"}, + {"name": "AWS_COGNITO_USER_POOL_ID", "value": "us-east-1_fjE3ZCqqX"}, + {"name": "AWS_COGNITO_USER_POOL_CLIENT_ID", "value": "6ff7h7vbdua1rvjctbi7lode3l"}, + {"name": "FRONTEND_URL", "value": "https://cruddursn.net"}, + {"name": "BACKEND_URL", "value": "https://api.cruddursn.net"}, + {"name": "AWS_DEFAULT_REGION", "value": "us-east-1"} + ], + "secrets": [ + {"name": "AWS_ACCESS_KEY_ID" , "valueFrom": "arn:aws:ssm:us-east-1:730331264766:parameter/cruddur/backend-flask/AWS_ACCESS_KEY_ID"}, + {"name": "AWS_SECRET_ACCESS_KEY", "valueFrom": "arn:aws:ssm:us-east-1:730331264766:parameter/cruddur/backend-flask/AWS_SECRET_ACCESS_KEY"}, + {"name": "CONNECTION_URL" , "valueFrom": "arn:aws:ssm:us-east-1:730331264766:parameter/cruddur/backend-flask/CONNECTION_URL" }, + {"name": "ROLLBAR_ACCESS_TOKEN" , "valueFrom": "arn:aws:ssm:us-east-1:730331264766:parameter/cruddur/backend-flask/ROLLBAR_ACCESS_TOKEN" }, + {"name": "OTEL_EXPORTER_OTLP_HEADERS" , "valueFrom": "arn:aws:ssm:us-east-1:730331264766:parameter/cruddur/backend-flask/OTEL_EXPORTER_OTLP_HEADERS" } + ] + } + ] + } \ No newline at end of file diff --git a/aws/task-definitions/frontend-react-js.json b/aws/task-definitions/frontend-react-js.json new file mode 100644 index 000000000..4c1342d02 --- /dev/null +++ b/aws/task-definitions/frontend-react-js.json @@ -0,0 +1,44 @@ +{ + "family": "frontend-react-js", + "executionRoleArn": "arn:aws:iam::730331264766:role/CruddurServiceExecutionRole", + "taskRoleArn": "arn:aws:iam::730331264766:role/CruddurTaskRole", + "networkMode": "awsvpc", + "cpu": "256", + "memory": "512", + "requiresCompatibilities": [ + "FARGATE" + ], + "containerDefinitions": [ + { + "name": "frontend-react-js", + "image": "730331264766.dkr.ecr.us-east-1.amazonaws.com/frontend-react-js", + "essential": true, + "healthCheck": { + "command": [ + "CMD-SHELL", + "curl -f http://localhost:3000 || exit 1" + ], + "interval": 30, + "timeout": 5, + "retries": 3 + }, + "portMappings": [ + { + "name": "frontend-react-js", + "containerPort": 3000, + "protocol": "tcp", + "appProtocol": "http" + } + ], + + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "cruddur", + "awslogs-region": "us-east-1", + "awslogs-stream-prefix": "frontend-react-js" + } + } + } + ] +} \ No newline at end of file diff --git a/backend-flask/Dockerfile b/backend-flask/Dockerfile new file mode 100644 index 000000000..e8f12a07d --- /dev/null +++ b/backend-flask/Dockerfile @@ -0,0 +1,34 @@ +FROM 730331264766.dkr.ecr.us-east-1.amazonaws.com/cruddur-python:3.10-slim-buster +## Step 1: +# inside container +# Create a working directory +WORKDIR /backend-flask + +## Step 2: +# Outside -> inside container +# Copy source code to working directory +COPY requirements.txt requirements.txt +## Step 3: +# inside the container +# Install packages from requirements.txt +RUN pip3 install -r requirements.txt + +# Outside -> inside container +# first (.) mean /backend-flask Outside container +# second (.) mean /backend-flask Inside container +# +COPY . . + +# set env variable inside container +#ENV FLASK_ENV=development +#ENV FLASK_DEBUG=1 + +## Step 4: +# Expose port 80 +EXPOSE ${PORT} + +ENV PYTHONUNBUFFERED=1 +## Step 5: +# Run app at container launch +# python3 -m flask run --host=0.0.0.0 --port 4567 +CMD [ "python3", "-m" , "flask", "run", "--host=0.0.0.0", "--port=4567", "--debug"] diff --git a/backend-flask/Dockerfile.prod b/backend-flask/Dockerfile.prod new file mode 100644 index 000000000..3f5f0d49f --- /dev/null +++ b/backend-flask/Dockerfile.prod @@ -0,0 +1,38 @@ +FROM 730331264766.dkr.ecr.us-east-1.amazonaws.com/cruddur-python:3.10-slim-buster + +# [TODO] For debugging, don't leave these in +#RUN apt-get update -y +#RUN apt-get install iputils-ping -y + +## Step 1: +# inside container +# Create a working directory +WORKDIR /backend-flask + +## Step 2: +# Outside -> inside container +# Copy source code to working directory +COPY requirements.txt requirements.txt +## Step 3: +# inside the container +# Install packages from requirements.txt +RUN pip3 install -r requirements.txt + +# Outside -> inside container +# first (.) mean /backend-flask Outside container +# second (.) mean /backend-flask Inside container +# +COPY . . + +# set env variable inside container +#ENV FLASK_ENV=development +#ENV FLASK_DEBUG=1 + +## Step 4: +# Expose port 80 +EXPOSE ${PORT} + +## Step 5: +# Run app at container launch +# python3 -m flask run --host=0.0.0.0 --port 4567 +CMD [ "python3", "-m" , "flask", "run", "--host=0.0.0.0", "--port=4567", "--debug", "--no-debugger", "--no-reload"] diff --git a/backend-flask/app.py b/backend-flask/app.py index b8fe3d363..61e0b3c3e 100644 --- a/backend-flask/app.py +++ b/backend-flask/app.py @@ -2,8 +2,11 @@ from flask import request from flask_cors import CORS, cross_origin import os +import sys +from services.users_short import * from services.home_activities import * +from services.notifications_activities import * from services.user_activities import * from services.create_activity import * from services.create_reply import * @@ -12,60 +15,229 @@ from services.messages import * from services.create_message import * from services.show_activity import * +from services.update_profile import * + +from lib.cognito_jwt_token import CognitoJwtToken, extract_access_token, TokenVerifyError + +# HoneyComb --------- +from opentelemetry import trace +from opentelemetry.instrumentation.flask import FlaskInstrumentor +from opentelemetry.instrumentation.requests import RequestsInstrumentor +from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor +from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor + +# X-RAY ---------- +from aws_xray_sdk.core import xray_recorder +from aws_xray_sdk.ext.flask.middleware import XRayMiddleware + +# CloudWatch Logs ---- +import watchtower +import logging + +# Rollbar ------ +from time import strftime +import os +import rollbar +import rollbar.contrib.flask +from flask import got_request_exception + +# Configuring Logger to Use CloudWatch +# LOGGER = logging.getLogger(__name__) +# LOGGER.setLevel(logging.DEBUG) +# console_handler = logging.StreamHandler() +# cw_handler = watchtower.CloudWatchLogHandler(log_group='cruddur') +# LOGGER.addHandler(console_handler) +# LOGGER.addHandler(cw_handler) +# LOGGER.info("test log") + +# HoneyComb --------- +# Initialize tracing and an exporter that can send data to Honeycomb +provider = TracerProvider() +processor = BatchSpanProcessor(OTLPSpanExporter()) +provider.add_span_processor(processor) + +# X-RAY ---------- +#xray_url = osgetenv("AWS_XRAY_URL") +#xray_recorder.configure(service='backend-flask', dynamic_naming=xray_url) + +# OTEL ---------- +# Show this in the logs within the backend-flask app (STDOUT) +#simple_processor = SimpleSpanProcessor(ConsoleSpanExporter()) +#provider.add_span_processor(simple_processor) + +trace.set_tracer_provider(provider) +tracer = trace.get_tracer(__name__) app = Flask(__name__) + +cognito_jwt_token = CognitoJwtToken( + user_pool_id=os.getenv("AWS_COGNITO_USER_POOL_ID"), + user_pool_client_id=os.getenv("AWS_COGNITO_USER_POOL_CLIENT_ID"), + region=os.getenv("AWS_DEFAULT_REGION") +) + +# X-RAY ---------- +#XRayMiddleware(app, xray_recorder) + +# HoneyComb --------- +# Initialize automatic instrumentation with Flask +FlaskInstrumentor().instrument_app(app) +RequestsInstrumentor().instrument() + + frontend = os.getenv('FRONTEND_URL') backend = os.getenv('BACKEND_URL') origins = [frontend, backend] cors = CORS( app, resources={r"/api/*": {"origins": origins}}, - expose_headers="location,link", - allow_headers="content-type,if-modified-since", + headers=['Content-Type', 'Authorization'], + expose_headers='Authorization', methods="OPTIONS,GET,HEAD,POST" ) +# CloudWatch Logs ----- +#@app.after_request +#def after_request(response): +# timestamp = strftime('[%Y-%b-%d %H:%M]') +# LOGGER.error('%s %s %s %s %s %s', timestamp, request.remote_addr, request.method, request.scheme, request.full_path, response.status) +# return response + +# Rollbar ---------- +rollbar_access_token = os.getenv('ROLLBAR_ACCESS_TOKEN') +@app.before_first_request +def init_rollbar(): + """init rollbar module""" + rollbar.init( + # access token + rollbar_access_token, + # environment name + 'production', + # server root directory, makes tracebacks prettier + root=os.path.dirname(os.path.realpath(__file__)), + # flask already sets up logging + allow_logging_basic_config=False) + + # send exceptions from `app` to rollbar, using flask's signal system. + got_request_exception.connect(rollbar.contrib.flask.report_exception, app) + +@app.route('/api/health-check') +def health_check(): + return {'success': True, 'ver': 1}, 200 + +#@app.route('/rollbar/test') +#def rollbar_test(): +# rollbar.report_message('Hello World!', 'warning') +# return "Hello World!" + @app.route("/api/message_groups", methods=['GET']) def data_message_groups(): - user_handle = 'andrewbrown' - model = MessageGroups.run(user_handle=user_handle) - if model['errors'] is not None: - return model['errors'], 422 - else: - return model['data'], 200 + access_token = extract_access_token(request.headers) + try: + claims = cognito_jwt_token.verify(access_token) + # authenicatied request + app.logger.debug("authenicated") + app.logger.debug(claims) + cognito_user_id = claims['sub'] + model = MessageGroups.run(cognito_user_id=cognito_user_id) + if model['errors'] is not None: + return model['errors'], 422 + else: + return model['data'], 200 + except TokenVerifyError as e: + # unauthenicatied request + app.logger.debug(e) + return {}, 401 -@app.route("/api/messages/@", methods=['GET']) -def data_messages(handle): - user_sender_handle = 'andrewbrown' - user_receiver_handle = request.args.get('user_reciever_handle') - model = Messages.run(user_sender_handle=user_sender_handle, user_receiver_handle=user_receiver_handle) - if model['errors'] is not None: - return model['errors'], 422 - else: - return model['data'], 200 - return +@app.route("/api/messages/", methods=['GET']) +def data_messages(message_group_uuid): + access_token = extract_access_token(request.headers) + try: + claims = cognito_jwt_token.verify(access_token) + # authenicatied request + app.logger.debug("authenicated") + app.logger.debug(claims) + cognito_user_id = claims['sub'] + model = Messages.run( + cognito_user_id=cognito_user_id, + message_group_uuid=message_group_uuid + ) + if model['errors'] is not None: + return model['errors'], 422 + else: + return model['data'], 200 + except TokenVerifyError as e: + # unauthenicatied request + app.logger.debug(e) + return {}, 401 @app.route("/api/messages", methods=['POST','OPTIONS']) @cross_origin() def data_create_message(): - user_sender_handle = 'andrewbrown' - user_receiver_handle = request.json['user_receiver_handle'] + message_group_uuid = request.json.get('message_group_uuid',None) + user_receiver_handle = request.json.get('handle',None) message = request.json['message'] + access_token = extract_access_token(request.headers) + try: + claims = cognito_jwt_token.verify(access_token) + # authenicatied request + app.logger.debug("authenicated") + app.logger.debug(claims) + cognito_user_id = claims['sub'] + if message_group_uuid == None: + # Create for the first time + model = CreateMessage.run( + mode="create", + message=message, + cognito_user_id=cognito_user_id, + user_receiver_handle=user_receiver_handle + ) + else: + # Push onto existing Message Group + model = CreateMessage.run( + mode="update", + message=message, + message_group_uuid=message_group_uuid, + cognito_user_id=cognito_user_id + ) + if model['errors'] is not None: + return model['errors'], 422 + else: + return model['data'], 200 + except TokenVerifyError as e: + # unauthenicatied request + app.logger.debug(e) + return {}, 401 - model = CreateMessage.run(message=message,user_sender_handle=user_sender_handle,user_receiver_handle=user_receiver_handle) - if model['errors'] is not None: - return model['errors'], 422 - else: - return model['data'], 200 - return @app.route("/api/activities/home", methods=['GET']) +#@xray_recorder.capture('activities_home') def data_home(): - data = HomeActivities.run() + access_token = extract_access_token(request.headers) + try: + claims = cognito_jwt_token.verify(access_token) + # authenicatied request + app.logger.debug("authenicated") + app.logger.debug(claims) + app.logger.debug(claims['username']) + data = HomeActivities.run(cognito_user_id=claims['username']) + except TokenVerifyError as e: + # unauthenicatied request + app.logger.debug(e) + app.logger.debug("unauthenicated") + data = HomeActivities.run() + return data, 200 + +@app.route("/api/activities/notifications", methods=['GET']) +def data_notifications(): + data = NotificationsActivities.run() return data, 200 @app.route("/api/activities/@", methods=['GET']) +#@xray_recorder.capture('activities_users') def data_handle(handle): model = UserActivities.run(handle) if model['errors'] is not None: @@ -97,6 +269,7 @@ def data_activities(): return @app.route("/api/activities/", methods=['GET']) +@xray_recorder.capture('activities_show') def data_show_activity(activity_uuid): data = ShowActivity.run(activity_uuid=activity_uuid) return data, 200 @@ -113,5 +286,33 @@ def data_activities_reply(activity_uuid): return model['data'], 200 return +@app.route("/api/users/@/short", methods=['GET']) +def data_users_short(handle): + data = UsersShort.run(handle) + return data, 200 + +@app.route("/api/profile/update", methods=['POST','OPTIONS']) +@cross_origin() +def data_update_profile(): + bio = request.json.get('bio',None) + display_name = request.json.get('display_name',None) + access_token = extract_access_token(request.headers) + try: + claims = cognito_jwt_token.verify(access_token) + cognito_user_id = claims['sub'] + model = UpdateProfile.run( + cognito_user_id=cognito_user_id, + bio=bio, + display_name=display_name + ) + if model['errors'] is not None: + return model['errors'], 422 + else: + return model['data'], 200 + except TokenVerifyError as e: + # unauthenicatied request + app.logger.debug(e) + return {}, 401 + if __name__ == "__main__": app.run(debug=True) \ No newline at end of file diff --git a/backend-flask/bin/backend/build b/backend-flask/bin/backend/build new file mode 100755 index 000000000..d4df59d95 --- /dev/null +++ b/backend-flask/bin/backend/build @@ -0,0 +1,12 @@ +#! /usr/bin/bash + +ABS_PATH=$(readlink -f "$0") +BACKEND_PATH=$(dirname $ABS_PATH) +BIN_PATH=$(dirname $BACKEND_PATH) +PROJECT_PATH=$(dirname $BIN_PATH) +BACKEND_FLASK_PATH="$PROJECT_PATH/backend-flask" + +docker build \ +-f "$BACKEND_FLASK_PATH/Dockerfile.prod" \ +-t backend-flask-prod \ +"$BACKEND_FLASK_PATH/." \ No newline at end of file diff --git a/backend-flask/bin/backend/connect b/backend-flask/bin/backend/connect new file mode 100755 index 000000000..85f69f67f --- /dev/null +++ b/backend-flask/bin/backend/connect @@ -0,0 +1,19 @@ +#! /usr/bin/bash +if [ -z "$1" ]; then + echo "No TASK_ID argument supplied eg ./bin/ecs/connect-to-backend-flask 99b2f8953616495e99545e5a6066fbb5d" + exit 1 +fi +TASK_ID=$1 + +CONTAINER_NAME=backend-flask + +echo "TASK ID : $TASK_ID" +echo "Container Name: $CONTAINER_NAME" + +aws ecs execute-command \ +--region $AWS_DEFAULT_REGION \ +--cluster cruddur \ +--task $TASK_ID \ +--container $CONTAINER_NAME \ +--command "/bin/bash" \ +--interactive \ No newline at end of file diff --git a/backend-flask/bin/backend/deploy b/backend-flask/bin/backend/deploy new file mode 100755 index 000000000..a5622d673 --- /dev/null +++ b/backend-flask/bin/backend/deploy @@ -0,0 +1,26 @@ +#! /usr/bin/bash + +CLUSTER_NAME="cruddur" +SERVICE_NAME="backend-flask" +TASK_DEFINTION_FAMILY="backend-flask" + + +LATEST_TASK_DEFINITION_ARN=$(aws ecs describe-task-definition \ +--task-definition $TASK_DEFINTION_FAMILY \ +--query 'taskDefinition.taskDefinitionArn' \ +--output text) + +echo "TASK DEF ARN:" +echo $LATEST_TASK_DEFINITION_ARN + +aws ecs update-service \ +--cluster $CLUSTER_NAME \ +--service $SERVICE_NAME \ +--task-definition $LATEST_TASK_DEFINITION_ARN \ +--force-new-deployment + +#aws ecs describe-services \ +#--cluster $CLUSTER_NAME \ +#--service $SERVICE_NAME \ +#--query 'services[0].deployments' \ +#--output table \ No newline at end of file diff --git a/backend-flask/bin/backend/push b/backend-flask/bin/backend/push new file mode 100755 index 000000000..a6359a5b5 --- /dev/null +++ b/backend-flask/bin/backend/push @@ -0,0 +1,7 @@ +#! /usr/bin/bash + +ECR_BACKEND_FLASK_URL="$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/backend-flask" +echo $ECR_BACKEND_FLASK_URL + +docker tag backend-flask-prod:latest $ECR_BACKEND_FLASK_URL:latest +docker push $ECR_BACKEND_FLASK_URL:latest \ No newline at end of file diff --git a/backend-flask/bin/backend/register b/backend-flask/bin/backend/register new file mode 100755 index 000000000..d9e12d585 --- /dev/null +++ b/backend-flask/bin/backend/register @@ -0,0 +1,12 @@ +#! /usr/bin/bash + +ABS_PATH=$(readlink -f "$0") +FRONTEND_PATH=$(dirname $ABS_PATH) +BIN_PATH=$(dirname $FRONTEND_PATH) +PROJECT_PATH=$(dirname $BIN_PATH) +TASK_DEF_PATH="$PROJECT_PATH/aws/task-definitions/backend-flask.json" + +echo $TASK_DEF_PATH + +aws ecs register-task-definition \ +--cli-input-json "file://$TASK_DEF_PATH" \ No newline at end of file diff --git a/backend-flask/bin/backend/run b/backend-flask/bin/backend/run new file mode 100755 index 000000000..689ceca5e --- /dev/null +++ b/backend-flask/bin/backend/run @@ -0,0 +1,29 @@ +#! /usr/bin/bash + +ABS_PATH=$(readlink -f "$0") +BACKEND_PATH=$(dirname $ABS_PATH) +BIN_PATH=$(dirname $BACKEND_PATH) +PROJECT_PATH=$(dirname $BIN_PATH) +ENVFILE_PATH="$PROJECT_PATH/backend-flask.env" + +docker run --rm \ + --env-file $ENVFILE_PATH \ + --network cruddur-net \ + --publish 4567:4567 \ + -it backend-flask-prod + +# --env AWS_ENDPOINT_URL="http://dynamodb-local:8000" \ +# --env CONNECTION_URL="postgresql://postgres:password@db:5432/cruddur" \ +# --env FRONTEND_URL="https://3000-omenking-awsbootcampcru-fz5zc9avjbe.ws-us93.gitpod.io" \ +# --env BACKEND_URL="https://4567-omenking-awsbootcampcru-fz5zc9avjbe.ws-us93.gitpod.io" \ +# --env OTEL_SERVICE_NAME='backend-flask' \ +# --env OTEL_EXPORTER_OTLP_ENDPOINT="https://api.honeycomb.io" \ +# --env OTEL_EXPORTER_OTLP_HEADERS="x-honeycomb-team=SqeqeKIB5xVY7SO9ZnG7EO" \ +# --env AWS_XRAY_URL="*4567-omenking-awsbootcampcru-fz5zc9avjbe.ws-us93.gitpod.io*" \ +# --env AWS_XRAY_DAEMON_ADDRESS="xray-daemon:2000" \ +# --env AWS_DEFAULT_REGION="ca-central-1" \ +# --env AWS_ACCESS_KEY_ID="AKIAVUO25ZPVJYSOSBY2" \ +# --env AWS_SECRET_ACCESS_KEY="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" \ +# --env ROLLBAR_ACCESS_TOKEN="ca8cde9275fe48c69c6eaa7cf504cdf0" \ +# --env AWS_COGNITO_USER_POOL_ID="ca-central-1_CQ4wDfnwc" \ +# --env AWS_COGNITO_USER_POOL_CLIENT_ID="5b6ro31g97urk767adrbrdj1g5" \ \ No newline at end of file diff --git a/backend-flask/bin/cognito/list-users b/backend-flask/bin/cognito/list-users new file mode 100755 index 000000000..7c7015851 --- /dev/null +++ b/backend-flask/bin/cognito/list-users @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +import boto3 +import os +import json + +userpool_id = os.getenv("AWS_COGNITO_USER_POOL_ID") +client = boto3.client('cognito-idp') +params = { + 'UserPoolId': userpool_id, + 'AttributesToGet': [ + 'preferred_username', + 'sub' + ] +} +response = client.list_users(**params) +users = response['Users'] + +print(json.dumps(users, sort_keys=True, indent=2, default=str)) + +dict_users = {} +for user in users: + attrs = user['Attributes'] + sub = next((a for a in attrs if a["Name"] == 'sub'), None) + handle = next((a for a in attrs if a["Name"] == 'preferred_username'), None) + dict_users[handle['Value']] = sub['Value'] + +#print(dict_users) +print(json.dumps(dict_users, sort_keys=True, indent=2, default=str)) diff --git a/backend-flask/bin/db/connect b/backend-flask/bin/db/connect new file mode 100755 index 000000000..86fe8afb0 --- /dev/null +++ b/backend-flask/bin/db/connect @@ -0,0 +1,9 @@ +#! /usr/bin/bash +if [ "$1" = "prod" ]; then + echo "Running in production mode" + URL=$PROD_CONNECTION_URL +else + URL=$CONNECTION_URL +fi + +psql $URL \ No newline at end of file diff --git a/backend-flask/bin/db/create b/backend-flask/bin/db/create new file mode 100755 index 000000000..1aeb56ce6 --- /dev/null +++ b/backend-flask/bin/db/create @@ -0,0 +1,16 @@ +#! /usr/bin/bash + +CYAN='\033[1;36m' +NO_COLOR='\033[0m' +LABEL="db-create" +printf "${CYAN}== ${LABEL}${NO_COLOR}\n" + +if [ "$1" = "prod" ]; then + echo "Running in production mode" + NO_DB_CONNECTION_URL=$(sed 's/\/cruddur//g' <<<"$PROD_CONNECTION_URL") + psql $NO_DB_CONNECTION_URL -c "create database cruddur;" +else + echo "Running in development mode" + NO_DB_CONNECTION_URL=$(sed 's/\/cruddur//g' <<<"$CONNECTION_URL") + psql $NO_DB_CONNECTION_URL -c "create database cruddur;" +fi \ No newline at end of file diff --git a/backend-flask/bin/db/drop b/backend-flask/bin/db/drop new file mode 100755 index 000000000..b74e2753b --- /dev/null +++ b/backend-flask/bin/db/drop @@ -0,0 +1,15 @@ +#! /usr/bin/bash + +CYAN='\033[1;36m' +NO_COLOR='\033[0m' +LABEL="db-drop" +printf "${CYAN}== ${LABEL}${NO_COLOR}\n" + +if [ "$1" = "prod" ]; then + echo "Running in production mode" + NO_DB_CONNECTION_URL=$(sed 's/\/cruddur//g' <<<"$PROD_CONNECTION_URL") + psql $NO_DB_CONNECTION_URL -c "drop database cruddur;" +else + NO_DB_CONNECTION_URL=$(sed 's/\/cruddur//g' <<<"$CONNECTION_URL") + psql $NO_DB_CONNECTION_URL -c "drop database IF EXISTS cruddur;" +fi diff --git a/backend-flask/bin/db/kill-all b/backend-flask/bin/db/kill-all new file mode 100644 index 000000000..7ea8f6049 --- /dev/null +++ b/backend-flask/bin/db/kill-all @@ -0,0 +1,17 @@ +#! /usr/bin/bash + +CYAN='\033[1;36m' +NO_COLOR='\033[0m' +LABEL="db-kill-all" +printf "${CYAN}== ${LABEL}${NO_COLOR}\n" + +ABS_PATH=$(readlink -f "$0") +DB_PATH=$(dirname $ABS_PATH) +BIN_PATH=$(dirname $DB_PATH) +PROJECT_PATH=$(dirname $BIN_PATH) +BACKEND_FLASK_PATH="$PROJECT_PATH/backend-flask" +kill_path="$BACKEND_FLASK_PATH/db/kill-all-connections.sql" +echo $kill_path + +psql $CONNECTION_URL cruddur < $kill_path + diff --git a/backend-flask/bin/db/schema-load b/backend-flask/bin/db/schema-load new file mode 100755 index 000000000..445c4424a --- /dev/null +++ b/backend-flask/bin/db/schema-load @@ -0,0 +1,18 @@ +#! /usr/bin/bash + +CYAN='\033[1;36m' +NO_COLOR='\033[0m' +LABEL="db-schema-load" +printf "${CYAN}== ${LABEL}${NO_COLOR}\n" + +schema_path="$(realpath .)/db/schema.sql" +echo $schema_path + +if [ "$1" = "prod" ]; then + echo "Running in production mode" + URL=$PROD_CONNECTION_URL +else + URL=$CONNECTION_URL +fi + +psql $URL cruddur < $schema_path \ No newline at end of file diff --git a/backend-flask/bin/db/seed b/backend-flask/bin/db/seed new file mode 100755 index 000000000..08552df31 --- /dev/null +++ b/backend-flask/bin/db/seed @@ -0,0 +1,18 @@ +#! /usr/bin/bash + +CYAN='\033[1;36m' +NO_COLOR='\033[0m' +LABEL="db-seed" +printf "${CYAN}== ${LABEL}${NO_COLOR}\n" + +seed_path="$(realpath .)/db/seed.sql" +echo $seed_path + +if [ "$1" = "prod" ]; then + echo "Running in production mode" + URL=$PROD_CONNECTION_URL +else + URL=$CONNECTION_URL +fi + +psql $URL cruddur < $seed_path \ No newline at end of file diff --git a/backend-flask/bin/db/setup b/backend-flask/bin/db/setup new file mode 100755 index 000000000..955b4731c --- /dev/null +++ b/backend-flask/bin/db/setup @@ -0,0 +1,26 @@ +#! /usr/bin/bash +-e # stop if it fails at any point + +CYAN='\033[1;36m' +NO_COLOR='\033[0m' +LABEL="db/setup" +printf "${CYAN}==== ${LABEL}${NO_COLOR}\n" + +bin_path="$(realpath .)/bin" + +if [ "$1" = "prod" ]; then + echo "Running in production mode" + source "$bin_path/db/drop" prod + source "$bin_path/db/create" prod + source "$bin_path/db/schema-load" prod + source "$bin_path/db/seed" prod + python "$bin_path/db/update_cognito_user_ids" +else + source "$bin_path/db/drop" + source "$bin_path/db/create" + source "$bin_path/db/schema-load" + source "$bin_path/db/seed" + python "$bin_path/db/update_cognito_user_ids" +fi + + diff --git a/backend-flask/bin/db/test b/backend-flask/bin/db/test new file mode 100755 index 000000000..559e00d64 --- /dev/null +++ b/backend-flask/bin/db/test @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 + +import psycopg +import os +import sys + +connection_url = os.getenv("PROD_CONNECTION_URL") + +conn = None +try: + print('attempting connection') + conn = psycopg.connect(connection_url) + print("Connection successful!") +except psycopg.Error as e: + print("Unable to connect to the database:", e) +finally: + conn.close() \ No newline at end of file diff --git a/backend-flask/bin/db/update_cognito_user_ids b/backend-flask/bin/db/update_cognito_user_ids new file mode 100755 index 000000000..ff802a41a --- /dev/null +++ b/backend-flask/bin/db/update_cognito_user_ids @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 + +import boto3 +import os +import sys + +current_path = os.path.dirname(os.path.abspath(__file__)) +parent_path = os.path.abspath(os.path.join(current_path, '..', '..')) +sys.path.append(parent_path) +from lib.db import db + +def update_users_with_cognito_user_id(handle,sub): + sql = """ + UPDATE public.users + SET cognito_user_id = %(sub)s + WHERE + users.handle = %(handle)s; + """ + db.query_commit(sql,{ + 'handle' : handle, + 'sub' : sub + }) + +def get_cognito_user_ids(): + userpool_id = os.getenv("AWS_COGNITO_USER_POOL_ID") + client = boto3.client('cognito-idp') + params = { + 'UserPoolId': userpool_id, + 'AttributesToGet': [ + 'preferred_username', + 'sub' + ] + } + response = client.list_users(**params) + users = response['Users'] + dict_users = {} + for user in users: + attrs = user['Attributes'] + sub = next((a for a in attrs if a["Name"] == 'sub'), None) + handle = next((a for a in attrs if a["Name"] == 'preferred_username'), None) + dict_users[handle['Value']] = sub['Value'] + return dict_users + + +users = get_cognito_user_ids() + +for handle, sub in users.items(): + print('----',handle,sub) + update_users_with_cognito_user_id( + handle=handle, + sub=sub + ) \ No newline at end of file diff --git a/backend-flask/bin/ddb/delete-table b/backend-flask/bin/ddb/delete-table new file mode 100755 index 000000000..5924b442d --- /dev/null +++ b/backend-flask/bin/ddb/delete-table @@ -0,0 +1,10 @@ +#! /usr/bin/bash +set -e # stop if it fails at any point + +if [ "$1" = "prod" ]; then + ENDPOINT_URL="" +else + ENDPOINT_URL="--endpoint-url=http://localhost:8000" +fi + +aws dynamodb delete-table $ENDPOINT_URL --table-name 'cruddur-messages' \ No newline at end of file diff --git a/backend-flask/bin/ddb/drop b/backend-flask/bin/ddb/drop new file mode 100755 index 000000000..1fa61a8fc --- /dev/null +++ b/backend-flask/bin/ddb/drop @@ -0,0 +1,23 @@ +#! /usr/bin/bash +set -e # stop if it fails at any point + +# ./bin/ddb/drop cruddur-messages prod + +if [ -z "$1" ]; then + echo "No TABLE_NAME supplied eg ./bin/ddb/drop cruddur-messages prod" + exit 1 +fi + +TABLE_NAME=$1 + +if [ "$1" = "prod" ]; then + ENDPOINT_URL="" +else + ENDPOINT_URL="--endpoint-url=http://localhost:8000" +fi + + +echo "deleting table: $TABLE_NAME ..." + +aws dynamodb delete-table $ENDPOINT_URL \ +--table-name $TABLE_NAME \ No newline at end of file diff --git a/backend-flask/bin/ddb/list-tables b/backend-flask/bin/ddb/list-tables new file mode 100755 index 000000000..733692950 --- /dev/null +++ b/backend-flask/bin/ddb/list-tables @@ -0,0 +1,12 @@ +#! /usr/bin/bash +set -e # stop if it fails at any point + +if [ "$1" = "prod" ]; then + ENDPOINT_URL="" +else + ENDPOINT_URL="--endpoint-url=http://localhost:8000" +fi + +aws dynamodb list-tables $ENDPOINT_URL \ +--query TableNames \ +--output table \ No newline at end of file diff --git a/backend-flask/bin/ddb/patterns/get-conversation b/backend-flask/bin/ddb/patterns/get-conversation new file mode 100755 index 000000000..6d1d214ce --- /dev/null +++ b/backend-flask/bin/ddb/patterns/get-conversation @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 + +import boto3 +import sys +import json +import datetime + +attrs = { + 'endpoint_url': 'http://localhost:8000' +} + +if len(sys.argv) == 2: + if "prod" in sys.argv[1]: + attrs = {} + +dynamodb = boto3.client('dynamodb',**attrs) +table_name = 'cruddur-messages' + +message_group_uuid = "5ae290ed-55d1-47a0-bc6d-fe2bc2700399" + +# define the query parameters +current_year = datetime.datetime.now().year +query_params = { + 'TableName': table_name, + 'ScanIndexForward': False, + 'Limit': 20, + 'ReturnConsumedCapacity': 'TOTAL', + 'KeyConditionExpression': 'pk = :pk AND begins_with(sk,:year)', + #'KeyConditionExpression': 'pk = :pk AND sk BETWEEN :start_date AND :end_date', + 'ExpressionAttributeValues': { + ':year': {'S': str(current_year) }, + #":start_date": { "S": "2023-03-01T00:00:00.000000+00:00" }, + #":end_date": { "S": "2023-03-19T23:59:59.999999+00:00" }, + ':pk': {'S': f"MSG#{message_group_uuid}"} + } +} + + +# query the table +response = dynamodb.query(**query_params) + +# print the items returned by the query +print(json.dumps(response, sort_keys=True, indent=2)) + +# print the consumed capacity +print(json.dumps(response['ConsumedCapacity'], sort_keys=True, indent=2)) + +items = response['Items'] +items.reverse() + +for item in items: + sender_handle = item['user_handle']['S'] + message = item['message']['S'] + timestamp = item['sk']['S'] + dt_object = datetime.datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S.%f%z') + formatted_datetime = dt_object.strftime('%Y-%m-%d %I:%M %p') + print(f'{sender_handle: <12}{formatted_datetime: <22}{message[:40]}...') \ No newline at end of file diff --git a/backend-flask/bin/ddb/patterns/list-conversations b/backend-flask/bin/ddb/patterns/list-conversations new file mode 100755 index 000000000..6737f26b8 --- /dev/null +++ b/backend-flask/bin/ddb/patterns/list-conversations @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 + +import boto3 +import sys +import json +import os +import datetime + +current_path = os.path.dirname(os.path.abspath(__file__)) +parent_path = os.path.abspath(os.path.join(current_path, '..', '..', '..')) +sys.path.append(parent_path) +from lib.db import db + +attrs = { + 'endpoint_url': 'http://localhost:8000' +} + +if len(sys.argv) == 2: + if "prod" in sys.argv[1]: + attrs = {} + +dynamodb = boto3.client('dynamodb',**attrs) +table_name = 'cruddur-messages' + +def get_my_user_uuid(): + sql = """ + SELECT + users.uuid + FROM users + WHERE + users.handle =%(handle)s + """ + uuid = db.query_value(sql,{ + 'handle': 'papamfall' + }) + return uuid + +my_user_uuid = get_my_user_uuid() +print(f"my-uuid: {my_user_uuid}") + +current_year = datetime.datetime.now().year +# define the query parameters +query_params = { + 'TableName': table_name, + 'KeyConditionExpression': 'pk = :pk AND begins_with(sk,:year)', + 'ScanIndexForward': False, + 'ExpressionAttributeValues': { + ':year': {'S': str(current_year) }, + ':pk': {'S': f"GRP#{my_user_uuid}"} + }, + 'ReturnConsumedCapacity': 'TOTAL' +} + +# query the table +response = dynamodb.query(**query_params) + +# print the items returned by the query +print(json.dumps(response, sort_keys=True, indent=2)) \ No newline at end of file diff --git a/backend-flask/bin/ddb/scan b/backend-flask/bin/ddb/scan new file mode 100755 index 000000000..59385f4c7 --- /dev/null +++ b/backend-flask/bin/ddb/scan @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 + +import boto3 +import sys +attrs = { + 'endpoint_url': 'http://localhost:8000' +} + +if len(sys.argv) == 2: + if "prod" in sys.argv[1]: + attrs = {} + +ddb = boto3.resource('dynamodb', **attrs) + +table_name = 'cruddur-messages' + + +table = ddb.Table(table_name) +response = table.scan() + +items = response['Items'] +for item in items: + print(item) \ No newline at end of file diff --git a/backend-flask/bin/ddb/schema-load b/backend-flask/bin/ddb/schema-load new file mode 100755 index 000000000..bb9668232 --- /dev/null +++ b/backend-flask/bin/ddb/schema-load @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 + +import boto3 +import sys + +attrs = { + 'endpoint_url': 'http://localhost:8000' +} + +if len(sys.argv) == 2: + if "prod" in sys.argv[1]: + attrs = {} + +dynamodb = boto3.client('dynamodb', **attrs) +table_name = 'cruddur-messages' + +# define the schema for the table +table_schema = { + 'AttributeDefinitions': [ + { + 'AttributeName': 'message_group_uuid', + 'AttributeType': 'S' + },{ + 'AttributeName': 'pk', + 'AttributeType': 'S' + },{ + 'AttributeName': 'sk', + 'AttributeType': 'S' + } + ], + 'KeySchema': [{ + 'AttributeName': 'pk', + 'KeyType': 'HASH' + },{ + 'AttributeName': 'sk', + 'KeyType': 'RANGE' + } + ], + 'BillingMode': 'PROVISIONED', + 'ProvisionedThroughput': { + 'ReadCapacityUnits': 5, + 'WriteCapacityUnits': 5 + }, + 'GlobalSecondaryIndexes':[{ + 'IndexName':'message-group-sk-index', + 'KeySchema':[{ + 'AttributeName': 'message_group_uuid', + 'KeyType': 'HASH' + },{ + 'AttributeName': 'sk', + 'KeyType': 'RANGE' + }], + 'Projection': { + 'ProjectionType': 'ALL' + }, + 'ProvisionedThroughput': { + 'ReadCapacityUnits': 5, + 'WriteCapacityUnits': 5 + }, + }] +} + +# create the table +response = dynamodb.create_table( + TableName=table_name, + **table_schema +) + +# print the response +print(response) \ No newline at end of file diff --git a/backend-flask/bin/ddb/seed b/backend-flask/bin/ddb/seed new file mode 100755 index 000000000..3464f422f --- /dev/null +++ b/backend-flask/bin/ddb/seed @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 + +import boto3 +import os +import sys +from datetime import datetime, timedelta, timezone +import uuid + +current_path = os.path.dirname(os.path.abspath(__file__)) +parent_path = os.path.abspath(os.path.join(current_path, '..', '..')) +sys.path.append(parent_path) +from lib.db import db + +attrs = { + 'endpoint_url': 'http://localhost:8000' +} +# unset endpoint url for use with production database +if len(sys.argv) == 2: + if "prod" in sys.argv[1]: + attrs = {} +ddb = boto3.client('dynamodb',**attrs) + +def get_user_uuids(): + sql = """ + SELECT + users.uuid, + users.display_name, + users.handle + FROM users + WHERE + users.handle IN( + %(my_handle)s, + %(other_handle)s + ) + """ + users = db.query_array_json(sql,{ + 'my_handle': 'papamfall', + 'other_handle': 'sokhnangom' + }) + my_user = next((item for item in users if item["handle"] == 'papamfall'), None) + other_user = next((item for item in users if item["handle"] == 'sokhnangom'), None) + results = { + 'my_user': my_user, + 'other_user': other_user + } + print('get_user_uuids') + print(results) + return results + +def create_message_group(client,message_group_uuid, my_user_uuid, last_message_at=None, message=None, other_user_uuid=None, other_user_display_name=None, other_user_handle=None): + table_name = 'cruddur-messages' + record = { + 'pk': {'S': f"GRP#{my_user_uuid}"}, + 'sk': {'S': last_message_at}, + 'message_group_uuid': {'S': message_group_uuid}, + 'message': {'S': message}, + 'user_uuid': {'S': other_user_uuid}, + 'user_display_name': {'S': other_user_display_name}, + 'user_handle': {'S': other_user_handle} + } + + response = client.put_item( + TableName=table_name, + Item=record + ) + print(response) + +def create_message(client,message_group_uuid, created_at, message, my_user_uuid, my_user_display_name, my_user_handle): + table_name = 'cruddur-messages' + record = { + 'pk': {'S': f"MSG#{message_group_uuid}"}, + 'sk': {'S': created_at }, + 'message_uuid': { 'S': str(uuid.uuid4()) }, + 'message': {'S': message}, + 'user_uuid': {'S': my_user_uuid}, + 'user_display_name': {'S': my_user_display_name}, + 'user_handle': {'S': my_user_handle} + } + # insert the record into the table + response = client.put_item( + TableName=table_name, + Item=record + ) + # print the response + print(response) + +message_group_uuid = "5ae290ed-55d1-47a0-bc6d-fe2bc2700399" +now = datetime.now(timezone.utc).astimezone() +users = get_user_uuids() + +create_message_group( + client=ddb, + message_group_uuid=message_group_uuid, + my_user_uuid=users['my_user']['uuid'], + other_user_uuid=users['other_user']['uuid'], + other_user_handle=users['other_user']['handle'], + other_user_display_name=users['other_user']['display_name'], + last_message_at=now.isoformat(), + message="this is a filler message" +) + +create_message_group( + client=ddb, + message_group_uuid=message_group_uuid, + my_user_uuid=users['other_user']['uuid'], + other_user_uuid=users['my_user']['uuid'], + other_user_handle=users['my_user']['handle'], + other_user_display_name=users['my_user']['display_name'], + last_message_at=now.isoformat(), + message="this is a filler message" +) + +conversation = """ +Person 1: Have you ever watched Babylon 5? It's one of my favorite TV shows! +Person 2: Yes, I have! I love it too. What's your favorite season? +Person 1: I think my favorite season has to be season 3. So many great episodes, like "Severed Dreams" and "War Without End." +Person 2: Yeah, season 3 was amazing! I also loved season 4, especially with the Shadow War heating up and the introduction of the White Star. +Person 1: Agreed, season 4 was really great as well. I was so glad they got to wrap up the storylines with the Shadows and the Vorlons in that season. +Person 2: Definitely. What about your favorite character? Mine is probably Londo Mollari. +Person 1: Londo is great! My favorite character is probably G'Kar. I loved his character development throughout the series. +Person 2: G'Kar was definitely a standout character. I also really liked Delenn's character arc and how she grew throughout the series. +Person 1: Delenn was amazing too, especially with her role in the Minbari Civil War and her relationship with Sheridan. Speaking of which, what did you think of the Sheridan character? +Person 2: I thought Sheridan was a great protagonist. He was a strong leader and had a lot of integrity. And his relationship with Delenn was so well-done. +Person 1: I totally agree! I also really liked the dynamic between Garibaldi and Bester. Those two had some great scenes together. +Person 2: Yes! Their interactions were always so intense and intriguing. And speaking of intense scenes, what did you think of the episode "Intersections in Real Time"? +Person 1: Oh man, that episode was intense. It was so well-done, but I could barely watch it. It was just too much. +Person 2: Yeah, it was definitely hard to watch. But it was also one of the best episodes of the series in my opinion. +Person 1: Absolutely. Babylon 5 had so many great episodes like that. Do you have a favorite standalone episode? +Person 2: Hmm, that's a tough one. I really loved "The Coming of Shadows" in season 2, but "A Voice in the Wilderness" in season 1 was also great. What about you? +Person 1: I think my favorite standalone episode might be "The Long Twilight Struggle" in season 2. It had some great moments with G'Kar and Londo. +Person 2: Yes, "The Long Twilight Struggle" was definitely a standout episode. Babylon 5 really had so many great episodes and moments throughout its run. +Person 1: Definitely. It's a shame it ended after only five seasons, but I'm glad we got the closure we did with the series finale. +Person 2: Yeah, the series finale was really well-done. It tied up a lot of loose ends and left us with a great sense of closure. +Person 1: It really did. Overall, Babylon 5 is just such a great show with fantastic characters, writing, and world-building. +Person 2: Agreed. It's one of my favorite sci-fi shows of all time and I'm always happy to revisit it. +Person 1: Same here. I think one of the things that makes Babylon 5 so special is its emphasis on politics and diplomacy. It's not just a show about space battles and aliens, but about the complex relationships between different species and their political maneuvering. +Person 2: Yes, that's definitely one of the show's strengths. And it's not just about big-picture politics, but also about personal relationships and the choices characters make. +Person 1: Exactly. I love how Babylon 5 explores themes of redemption, forgiveness, and sacrifice. Characters like G'Kar and Londo have such compelling arcs that are driven by their choices and actions. +Person 2: Yes, the character development in Babylon 5 is really top-notch. Even minor characters like Vir and Franklin get their moments to shine and grow over the course of the series. +Person 1: I couldn't agree more. And the way the show handles its themes is so nuanced and thought-provoking. For example, the idea of "the one" and how it's used by different characters in different ways. +Person 2: Yes, that's a really interesting theme to explore. And it's not just a one-dimensional concept, but something that's explored in different contexts and with different characters. +Person 1: And the show also does a great job of balancing humor and drama. There are so many funny moments in the show, but it never detracts from the serious themes and the high stakes. +Person 2: Absolutely. The humor is always organic and never feels forced. And the show isn't afraid to go dark when it needs to, like in "Intersections in Real Time" or the episode "Sleeping in Light." +Person 1: Yeah, those episodes are definitely tough to watch, but they're also some of the most powerful and memorable episodes of the series. And it's not just the writing that's great, but also the acting and the production values. +Person 2: Yes, the acting is fantastic across the board. From Bruce Boxleitner's performance as Sheridan to Peter Jurasik's portrayal of Londo, every actor brings their A-game. And the production design and special effects are really impressive for a TV show from the 90s. +Person 1: Definitely. Babylon 5 was really ahead of its time in terms of its visuals and special effects. And the fact that it was all done on a TV budget makes it even more impressive. +Person 2: Yeah, it's amazing what they were able to accomplish with the limited resources they had. It just goes to show how talented the people behind the show were. +Person 1: Agreed. It's no wonder that Babylon 5 has such a devoted fanbase, even all these years later. It's just such a well-crafted and timeless show. +Person 2: Absolutely. I'm glad we can still appreciate it and talk about it all these years later. It really is a show that stands the test of time. +Person 1: One thing I really appreciate about Babylon 5 is how it handles diversity and representation. It has a really diverse cast of characters from different species and backgrounds, and it doesn't shy away from exploring issues of prejudice and discrimination. +Person 2: Yes, that's a great point. The show was really ahead of its time in terms of its diverse cast and the way it tackled issues of race, gender, and sexuality. And it did so in a way that felt natural and integrated into the story. +Person 1: Definitely. It's great to see a show that's not afraid to tackle these issues head-on and address them in a thoughtful and nuanced way. And it's not just about representation, but also about exploring different cultures and ways of life. +Person 2: Yes, the show does a great job of world-building and creating distinct cultures for each of the species. And it's not just about their physical appearance, but also about their customs, beliefs, and values. +Person 1: Absolutely. It's one of the things that sets Babylon 5 apart from other sci-fi shows. The attention to detail and the thought that went into creating this universe is really impressive. +Person 2: And it's not just the aliens that are well-developed, but also the human characters. The show explores the different factions and political ideologies within EarthGov, as well as the different cultures and traditions on Earth. +Person 1: Yes, that's another great aspect of the show. It's not just about the conflicts between different species, but also about the internal struggles within humanity. And it's all tied together by the overarching plot of the Shadow War and the fate of the galaxy. +Person 2: Definitely. The show does a great job of balancing the episodic stories with the larger arc, so that every episode feels important and contributes to the overall narrative. +Person 1: And the show is also great at building up tension and suspense. The slow burn of the Shadow War and the mystery of the Vorlons and the Shadows kept me on the edge of my seat throughout the series. +Person 2: Yes, the show is really good at building up anticipation and delivering satisfying payoffs. Whether it's the resolution of a character arc or the climax of a season-long plotline, Babylon 5 always delivers. +Person 1: Agreed. It's just such a well-crafted and satisfying show, with so many memorable moments and characters. I'm really glad we got to talk about it today. +Person 2: Me too. It's always great to geek out about Babylon 5 with someone who appreciates it as much as I do! +Person 1: Yeah, it's always fun to discuss our favorite moments and characters from the show. And there are so many great moments to choose from! +Person 2: Definitely. I think one of the most memorable moments for me was the "goodbye" scene between G'Kar and Londo in the episode "Objects at Rest." It was such a poignant and emotional moment, and it really showed how far their characters had come. +Person 1: Yes, that was a really powerful scene. It was great to see these two former enemies come together and find common ground. And it was a great way to wrap up their character arcs. +Person 2: Another memorable moment for me was the speech that Sheridan gives in "Severed Dreams." It's such an iconic moment in the show, and it really encapsulates the themes of the series. +Person 1: Yes, that speech is definitely one of the highlights of the series. It's so well-written and well-delivered, and it really captures the sense of hope and defiance that the show is all about. +Person 2: And speaking of great speeches, what did you think of the "Ivanova is always right" speech from "Moments of Transition"? +Person 1: Oh man, that speech gives me chills every time I watch it. It's such a powerful moment for Ivanova, and it really shows her strength and determination as a leader. +Person 2: Yes, that speech is definitely a standout moment for Ivanova's character. And it's just one example of the great writing and character development in the show. +Person 1: Absolutely. It's a testament to the talent of the writers and actors that they were able to create such rich and complex characters with so much depth and nuance. +Person 2: And it's not just the main characters that are well-developed, but also the supporting characters like Marcus, Zack, and Lyta. They all have their own stories and struggles, and they all contribute to the larger narrative in meaningful ways. +Person 1: Definitely. Babylon 5 is just such a well-rounded and satisfying show in every way. It's no wonder that it's still beloved by fans all these years later. +Person 2: Agreed. It's a show that has stood the test of time, and it will always hold a special place in my heart as one of my favorite TV shows of all time. +Person 1: One of the most interesting ethical dilemmas presented in Babylon 5 is the treatment of the Narn by the Centauri. What do you think about that storyline? +Person 2: Yeah, it's definitely a difficult issue to grapple with. On the one hand, the Centauri were portrayed as the aggressors, and their treatment of the Narn was brutal and unjust. But on the other hand, the show also presented some nuance to the situation, with characters like Londo and Vir struggling with their own complicity in the conflict. +Person 1: Exactly. I think one of the strengths of the show is its willingness to explore complex ethical issues like this. It's not just about good guys versus bad guys, but about the shades of grey in between. +Person 2: Yeah, and it raises interesting questions about power and oppression. The Centauri had more advanced technology and military might than the Narn, which allowed them to dominate and subjugate the Narn people. But at the same time, there were also political and economic factors at play that contributed to the conflict. +Person 1: And it's not just about the actions of the Centauri government, but also about the actions of individual characters. Londo, for example, was initially portrayed as a somewhat sympathetic character, but as the series progressed, we saw how his choices and actions contributed to the suffering of the Narn people. +Person 2: Yes, and that raises interesting questions about personal responsibility and accountability. Can an individual be held responsible for the actions of their government or their society? And if so, to what extent? +Person 1: That's a really good point. And it's also interesting to consider the role of empathy and compassion in situations like this. Characters like G'Kar and Delenn showed compassion towards the Narn people and fought against their oppression, while others like Londo and Cartagia were more indifferent or even sadistic in their treatment of the Narn. +Person 2: Yeah, and that raises the question of whether empathy and compassion are innate traits, or whether they can be cultivated through education and exposure to different cultures and perspectives. +Person 1: Definitely. And it's also worth considering the role of forgiveness and reconciliation. The Narn and Centauri eventually came to a sort of reconciliation in the aftermath of the Shadow War, but it was a difficult and painful process that required a lot of sacrifice and forgiveness on both sides. +Person 2: Yes, and that raises the question of whether forgiveness is always possible or appropriate in situations of oppression and injustice. Can the victims of such oppression ever truly forgive their oppressors, or is that too much to ask? +Person 1: It's a tough question to answer. I think the show presents a hopeful message in the end, with characters like G'Kar and Londo finding a measure of redemption and reconciliation. But it's also clear that the scars of the conflict run deep and that healing takes time and effort. +Person 2: Yeah, that's a good point. Ultimately, I think the show's treatment of the Narn-Centauri conflict raises more questions than it answers, which is a testament to its complexity and nuance. It's a difficult issue to grapple with, but one that's worth exploring and discussing. +Person 1: Let's switch gears a bit and talk about the character of Natasha Alexander. What did you think about her role in the series? +Person 2: I thought Natasha Alexander was a really interesting character. She was a tough and competent security officer, but she also had a vulnerable side and a complicated past. +Person 1: Yeah, I agree. I think she added a lot of depth to the show and was a great foil to characters like Garibaldi and Zack. +Person 2: And I also appreciated the way the show handled her relationship with Garibaldi. It was clear that they had a history and a lot of unresolved tension, but the show never made it too melodramatic or over-the-top. +Person 1: That's a good point. I think the show did a good job of balancing the personal drama with the larger political and sci-fi elements. And it was refreshing to see a female character who was just as tough and competent as the male characters. +Person 2: Definitely. I think Natasha Alexander was a great example of a well-written and well-rounded female character. She wasn't just there to be eye candy or a love interest, but had her own story and agency. +Person 1: However, I did feel like the show could have done more with her character. She was introduced fairly late in the series, and didn't have as much screen time as some of the other characters. +Person 2: That's true. I think the show had a lot of characters to juggle, and sometimes that meant some characters got sidelined or didn't get as much development as they deserved. +Person 1: And I also thought that her storyline with Garibaldi could have been developed a bit more. They had a lot of history and tension between them, but it felt like it was resolved too quickly and neatly. +Person 2: I can see where you're coming from, but I also appreciated the way the show didn't drag out the drama unnecessarily. It was clear that they both had feelings for each other, but they also had to focus on their jobs and the larger conflicts at play. +Person 1: I can see that perspective as well. Overall, I think Natasha Alexander was a great addition to the show and added a lot of value to the series. It's a shame we didn't get to see more of her. +Person 2: Agreed. But at least the show was able to give her a satisfying arc and resolution in the end. And that's a testament to the show's strength as a whole. +Person 1: One thing that really stands out about Babylon 5 is the quality of the special effects. What did you think about the show's use of CGI and other visual effects? +Person 2: I thought the special effects in Babylon 5 were really impressive, especially for a show that aired in the 90s. The use of CGI to create the spaceships and other sci-fi elements was really innovative for its time. +Person 1: Yes, I was really blown away by the level of detail and realism in the effects. The ships looked so sleek and futuristic, and the space battles were really intense and exciting. +Person 2: And I also appreciated the way the show integrated the visual effects with the live-action footage. It never felt like the effects were taking over or overshadowing the characters or the story. +Person 1: Absolutely. The show had a great balance of practical effects and CGI, which helped to ground the sci-fi elements in a more tangible and realistic world. +Person 2: And it's also worth noting the way the show's use of visual effects evolved over the course of the series. The effects in the first season were a bit rough around the edges, but by the end of the series, they had really refined and perfected the look and feel of the show. +Person 1: Yes, I agree. And it's impressive how they were able to accomplish all of this on a TV budget. The fact that the show was able to create such a rich and immersive sci-fi universe with limited resources is a testament to the talent and creativity of the production team. +Person 2: Definitely. And it's one of the reasons why the show has aged so well. Even today, the visual effects still hold up and look impressive, which is a rarity for a show that's almost 30 years old. +Person 1: Agreed. And it's also worth noting the way the show's use of visual effects influenced other sci-fi shows that came after it. Babylon 5 really set the bar for what was possible in terms of sci-fi visuals on TV. +Person 2: Yes, it definitely had a big impact on the genre as a whole. And it's a great example of how innovative and groundbreaking sci-fi can be when it's done right. +Person 1: Another character I wanted to discuss is Zathras. What did you think of his character? +Person 2: Zathras was a really unique and memorable character. He was quirky and eccentric, but also had a lot of heart and sincerity. +Person 1: Yes, I thought he was a great addition to the show. He added some much-needed comic relief, but also had some important moments of character development. +Person 2: And I appreciated the way the show used him as a sort of plot device, with his knowledge of time and space being instrumental in the resolution of some of the show's major storylines. +Person 1: Definitely. It was a great way to integrate a seemingly minor character into the larger narrative. And it was also interesting to see the different versions of Zathras from different points in time. +Person 2: Yeah, that was a clever storytelling device that really added to the sci-fi elements of the show. And it was also a great showcase for actor Tim Choate, who played the character with so much charm and energy. +Person 1: I also thought that Zathras was a great example of the show's commitment to creating memorable and unique characters. Even characters that only appeared in a few episodes, like Zathras or Bester, were given distinct personalities and backstories. +Person 2: Yes, that's a good point. Babylon 5 was really great at creating a diverse and interesting cast of characters, with each one feeling like a fully-realized and distinct individual. +Person 1: And Zathras was just one example of that. He was a small but important part of the show's legacy, and he's still remembered fondly by fans today. +Person 2: Definitely. I think his character is a great example of the show's ability to balance humor and heart, and to create memorable and beloved characters that fans will cherish for years to come. +""" + + +lines = conversation.lstrip('\n').rstrip('\n').split('\n') +for i in range(len(lines)): + if lines[i].startswith('Person 1: '): + key = 'my_user' + message = lines[i].replace('Person 1: ', '') + elif lines[i].startswith('Person 2: '): + key = 'other_user' + message = lines[i].replace('Person 2: ', '') + else: + print(lines[i]) + raise 'invalid line' + + created_at = (now + timedelta(minutes=i)).isoformat() + create_message( + client=ddb, + message_group_uuid=message_group_uuid, + created_at=created_at, + message=message, + my_user_uuid=users[key]['uuid'], + my_user_display_name=users[key]['display_name'], + my_user_handle=users[key]['handle'] + ) \ No newline at end of file diff --git a/backend-flask/bin/ecr/login b/backend-flask/bin/ecr/login new file mode 100755 index 000000000..2cd176b31 --- /dev/null +++ b/backend-flask/bin/ecr/login @@ -0,0 +1,3 @@ +#! /usr/bin/bash + +aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin "$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com" \ No newline at end of file diff --git a/backend-flask/bin/ecs/list-tasks b/backend-flask/bin/ecs/list-tasks new file mode 100644 index 000000000..1fb8fa544 --- /dev/null +++ b/backend-flask/bin/ecs/list-tasks @@ -0,0 +1,4 @@ + +#! /usr/bin/bash + +aws ecs list-tasks --cluster cruddur --desired-status STOPPED \ No newline at end of file diff --git a/backend-flask/bin/flask/health-check b/backend-flask/bin/flask/health-check new file mode 100755 index 000000000..b644245b8 --- /dev/null +++ b/backend-flask/bin/flask/health-check @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 + +import urllib.request + +try: + response = urllib.request.urlopen('http://localhost:4567/api/health-check') + if response.getcode() == 200: + print("[OK] Flask server is running") + exit(0) + else: + print("[BAD] Flask server is not running") + exit(1) +except Exception as e: + print(e) + exit(1) \ No newline at end of file diff --git a/backend-flask/bin/frontend/build b/backend-flask/bin/frontend/build new file mode 100644 index 000000000..4be9935a9 --- /dev/null +++ b/backend-flask/bin/frontend/build @@ -0,0 +1,17 @@ +#! /usr/bin/bash + +ABS_PATH=$(readlink -f "$0") +FRONTEND_PATH=$(dirname $ABS_PATH) +BIN_PATH=$(dirname $FRONTEND_PATH) +PROJECT_PATH=$(dirname $BIN_PATH) +FRONTEND_REACT_JS_PATH="$PROJECT_PATH/frontend-react-js" + +docker build \ +--build-arg REACT_APP_BACKEND_URL="https://api.cruddursn.net" \ +--build-arg REACT_APP_AWS_PROJECT_REGION="$AWS_DEFAULT_REGION" \ +--build-arg REACT_APP_AWS_COGNITO_REGION="$AWS_DEFAULT_REGION" \ +--build-arg REACT_APP_AWS_USER_POOLS_ID="us-east-1_fjE3ZCqqX" \ +--build-arg REACT_APP_CLIENT_ID="6ff7h7vbdua1rvjctbi7lode3l" \ +-t frontend-react-js \ +-f "$FRONTEND_REACT_JS_PATH/Dockerfile.prod" \ +"$FRONTEND_REACT_JS_PATH/." \ No newline at end of file diff --git a/backend-flask/bin/frontend/connect b/backend-flask/bin/frontend/connect new file mode 100644 index 000000000..9f4f2de80 --- /dev/null +++ b/backend-flask/bin/frontend/connect @@ -0,0 +1,19 @@ +#! /usr/bin/bash +if [ -z "$1" ]; then + echo "No TASK_ID argument supplied eg ./bin/ecs/connect-to-frontend-react-js 99b2f8953616495e99545e5a6066fbb5d" + exit 1 +fi +TASK_ID=$1 + +CONTAINER_NAME=frontend-react-js + +echo "TASK ID : $TASK_ID" +echo "Container Name: $CONTAINER_NAME" + +aws ecs execute-command \ +--region $AWS_DEFAULT_REGION \ +--cluster cruddur \ +--task $TASK_ID \ +--container $CONTAINER_NAME \ +--command "/bin/sh" \ +--interactive \ No newline at end of file diff --git a/backend-flask/bin/frontend/deploy b/backend-flask/bin/frontend/deploy new file mode 100644 index 000000000..9c17b50a9 --- /dev/null +++ b/backend-flask/bin/frontend/deploy @@ -0,0 +1,19 @@ +#! /usr/bin/bash + +CLUSTER_NAME="cruddur" +SERVICE_NAME="frontend-react-js" +TASK_DEFINTION_FAMILY="frontend-react-js" + +LATEST_TASK_DEFINITION_ARN=$(aws ecs describe-task-definition \ +--task-definition $TASK_DEFINTION_FAMILY \ +--query 'taskDefinition.taskDefinitionArn' \ +--output text) + +echo "TASK DEF ARN:" +echo $LATEST_TASK_DEFINITION_ARN + +aws ecs update-service \ +--cluster $CLUSTER_NAME \ +--service $SERVICE_NAME \ +--task-definition $LATEST_TASK_DEFINITION_ARN \ +--force-new-deployment \ No newline at end of file diff --git a/backend-flask/bin/frontend/push b/backend-flask/bin/frontend/push new file mode 100644 index 000000000..68156f04a --- /dev/null +++ b/backend-flask/bin/frontend/push @@ -0,0 +1,5 @@ +ECR_FRONTEND_REACT_URL="$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/frontend-react-js" +echo $ECR_FRONTEND_REACT_URL + +docker tag frontend-react-js:latest $ECR_FRONTEND_REACT_URL:latest +docker push $ECR_FRONTEND_REACT_URL:latest \ No newline at end of file diff --git a/backend-flask/bin/rds/update-sg-rule b/backend-flask/bin/rds/update-sg-rule new file mode 100755 index 000000000..1e6e45294 --- /dev/null +++ b/backend-flask/bin/rds/update-sg-rule @@ -0,0 +1,10 @@ +#! /usr/bin/bash + +CYAN='\033[1;36m' +NO_COLOR='\033[0m' +LABEL="rds-update-sg-rule" +printf "${CYAN}==== ${LABEL}${NO_COLOR}\n" + +aws ec2 modify-security-group-rules \ + --group-id $DB_SG_ID \ + --security-group-rules "SecurityGroupRuleId=$DB_SG_RULE_ID,SecurityGroupRule={Description=GITPOD,IpProtocol=tcp,FromPort=5432,ToPort=5432,CidrIpv4=$GITPOD_IP/32}" \ No newline at end of file diff --git a/backend-flask/db/kill-all-connections.sql b/backend-flask/db/kill-all-connections.sql new file mode 100644 index 000000000..8ec3e023c --- /dev/null +++ b/backend-flask/db/kill-all-connections.sql @@ -0,0 +1,7 @@ +SELECT pg_terminate_backend(pid) +FROM pg_stat_activity +WHERE +-- don't kill my own connection! +pid <> pg_backend_pid() +-- don't kill the connections to other databases +AND datname = 'cruddur'; \ No newline at end of file diff --git a/backend-flask/db/migrations/16849551021582754_add_bio_column.py b/backend-flask/db/migrations/16849551021582754_add_bio_column.py new file mode 100644 index 000000000..1418a8eef --- /dev/null +++ b/backend-flask/db/migrations/16849551021582754_add_bio_column.py @@ -0,0 +1,23 @@ +from lib.db import db + +class AddBioColumnMigration: + def migrate_sql(): + data = """ + ALTER TABLE public.users ADD COLUMN bio text; + """ + return data + def rollback_sql(): + data = """ + ALTER TABLE public.users DROP COLUMN bio; + """ + return data + + def migrate(): + db.query_commit(AddBioColumnMigration.migrate_sql(),{ + }) + + def rollback(): + db.query_commit(AddBioColumnMigration.rollback_sql(),{ + }) + +migration = AddBioColumnMigration \ No newline at end of file diff --git a/backend-flask/db/schema.sql b/backend-flask/db/schema.sql new file mode 100644 index 000000000..d3864f2d2 --- /dev/null +++ b/backend-flask/db/schema.sql @@ -0,0 +1,35 @@ +-- https://www.postgresql.org/docs/current/uuid-ossp.html +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- forcefully drop our tables if they already exist +DROP TABLE IF EXISTS public.users cascade; +DROP TABLE IF EXISTS public.activities; + +CREATE TABLE IF NOT EXISTS public.schema_information ( + id INTEGER UNIQUE , + last_successful_run text +); + +INSERT INTO public.schema_information (id, last_successful_run) VALUES(1, '0') on conflict (id) do nothing; + + +CREATE TABLE public.users ( + uuid UUID default uuid_generate_v4() primary key, + display_name text, + handle text, + email text, + cognito_user_id text, + created_at timestamp default current_timestamp NOT NULL +); + +CREATE TABLE public.activities ( + uuid UUID default uuid_generate_v4() primary key, + user_uuid UUID REFERENCES public.users(uuid) NOT NULL, + message text NOT NULL, + replies_count integer default 0, + reposts_count integer default 0, + likes_count integer default 0, + reply_to_activity_uuid integer, + expires_at timestamp, + created_at timestamp default current_timestamp NOT NULL +); \ No newline at end of file diff --git a/backend-flask/db/seed.sql b/backend-flask/db/seed.sql new file mode 100644 index 000000000..22de49175 --- /dev/null +++ b/backend-flask/db/seed.sql @@ -0,0 +1,16 @@ +-- this file was manually created +INSERT INTO public.users (display_name, handle, email, cognito_user_id) +VALUES + ('Papa Moussa FALL', 'papamfall' ,'papemfall@gmail.com','MOCK'), + ('Sokhna Ngom', 'sokhnangom', 'sokhnangom2001@gmail.com' ,'MOCK'); + + + -- ('pmf', 'pmf' ,'papemfall@gmail.com','MOCK'); + +INSERT INTO public.activities (user_uuid, message, expires_at) +VALUES + ( + (SELECT uuid from public.users WHERE users.handle = 'papamfall' LIMIT 1), + 'This was imported as seed data!', + current_timestamp + interval '10 day' + ) \ No newline at end of file diff --git a/backend-flask/db/sql/activities/create_activities.sql b/backend-flask/db/sql/activities/create_activities.sql new file mode 100644 index 000000000..b6558a9ac --- /dev/null +++ b/backend-flask/db/sql/activities/create_activities.sql @@ -0,0 +1,10 @@ + INSERT INTO public.activities( + user_uuid, + message, + expires_at + ) + VALUES( + (SELECT uuid from public.users WHERE users.handle = %(handle)s LIMIT 1), + %(message)s, + %(expires_at)s + ) RETURNING uuid; \ No newline at end of file diff --git a/backend-flask/db/sql/activities/home.sql b/backend-flask/db/sql/activities/home.sql new file mode 100644 index 000000000..178142531 --- /dev/null +++ b/backend-flask/db/sql/activities/home.sql @@ -0,0 +1,14 @@ + SELECT + activities.uuid, + users.display_name, + users.handle, + activities.message, + activities.replies_count, + activities.reposts_count, + activities.likes_count, + activities.reply_to_activity_uuid, + activities.expires_at, + activities.created_at + FROM public.activities + LEFT JOIN public.users ON users.uuid = activities.user_uuid + ORDER BY activities.created_at DESC \ No newline at end of file diff --git a/backend-flask/db/sql/activities/object.sql b/backend-flask/db/sql/activities/object.sql new file mode 100644 index 000000000..e0948ad1d --- /dev/null +++ b/backend-flask/db/sql/activities/object.sql @@ -0,0 +1,11 @@ +SELECT + activities.uuid, + users.display_name, + users.handle, + activities.message, + activities.created_at, + activities.expires_at +FROM public.activities +INNER JOIN public.users ON users.uuid = activities.user_uuid +WHERE + activities.uuid = %(uuid)s \ No newline at end of file diff --git a/backend-flask/db/sql/users/create_message_users.sql b/backend-flask/db/sql/users/create_message_users.sql new file mode 100755 index 000000000..9b5233842 --- /dev/null +++ b/backend-flask/db/sql/users/create_message_users.sql @@ -0,0 +1,17 @@ +SELECT + users.uuid, + users.display_name, + users.handle, + CASE users.cognito_user_id = %(cognito_user_id)s + WHEN TRUE THEN + 'sender' + WHEN FALSE THEN + 'recv' + ELSE + 'other' + END as kind +FROM public.users +WHERE + users.cognito_user_id = %(cognito_user_id)s + OR + users.handle = %(user_receiver_handle)s \ No newline at end of file diff --git a/backend-flask/db/sql/users/short.sql b/backend-flask/db/sql/users/short.sql new file mode 100755 index 000000000..2ed049430 --- /dev/null +++ b/backend-flask/db/sql/users/short.sql @@ -0,0 +1,7 @@ +SELECT + users.uuid, + users.handle, + users.display_name +FROM public.users +WHERE + users.handle = %(handle)s \ No newline at end of file diff --git a/backend-flask/db/sql/users/show.sql b/backend-flask/db/sql/users/show.sql new file mode 100644 index 000000000..720bc46fb --- /dev/null +++ b/backend-flask/db/sql/users/show.sql @@ -0,0 +1,33 @@ +SELECT + (SELECT COALESCE(row_to_json(object_row),'{}'::json) FROM ( + SELECT + users.uuid, + users.cognito_user_id as cognito_user_uuid, + users.handle, + users.display_name, + users.bio, + ( + SELECT + count(true) + FROM public.activities + WHERE + activities.user_uuid = users.uuid + ) as cruds_count + ) object_row) as profile, + (SELECT COALESCE(array_to_json(array_agg(row_to_json(array_row))),'[]'::json) FROM ( + SELECT + activities.uuid, + users.display_name, + users.handle, + activities.message, + activities.created_at, + activities.expires_at + FROM public.activities + WHERE + activities.user_uuid = users.uuid + ORDER BY activities.created_at DESC + LIMIT 40 + ) array_row) as activities +FROM public.users +WHERE + users.handle = %(handle)s \ No newline at end of file diff --git a/backend-flask/db/sql/users/update.sql b/backend-flask/db/sql/users/update.sql new file mode 100644 index 000000000..3489468fc --- /dev/null +++ b/backend-flask/db/sql/users/update.sql @@ -0,0 +1,7 @@ +UPDATE public.users +SET + bio = %(bio)s, + display_name= %(display_name)s +WHERE + users.cognito_user_id = %(cognito_user_id)s +RETURNING handle; \ No newline at end of file diff --git a/backend-flask/db/sql/users/uuid_from_cognito_user_id.sql b/backend-flask/db/sql/users/uuid_from_cognito_user_id.sql new file mode 100644 index 000000000..d63945a85 --- /dev/null +++ b/backend-flask/db/sql/users/uuid_from_cognito_user_id.sql @@ -0,0 +1,6 @@ +SELECT + users.uuid +FROM public.users +WHERE + users.cognito_user_id = %(cognito_user_id)s +LIMIT 1 \ No newline at end of file diff --git a/backend-flask/lib/cognito_jwt_token.py b/backend-flask/lib/cognito_jwt_token.py new file mode 100644 index 000000000..06ccbeb9a --- /dev/null +++ b/backend-flask/lib/cognito_jwt_token.py @@ -0,0 +1,117 @@ +import time +import requests +from jose import jwk, jwt +from jose.exceptions import JOSEError +from jose.utils import base64url_decode +from flask_awscognito.exceptions import FlaskAWSCognitoError, TokenVerifyError +HTTP_HEADER = "Authorization" + +class FlaskAWSCognitoError(Exception): + pass + + +class TokenVerifyError(Exception): + pass + + +def extract_access_token(request_headers): + access_token = None + auth_header = request_headers.get(HTTP_HEADER) + if auth_header and " " in auth_header: + _, access_token = auth_header.split() + return access_token + +class CognitoJwtToken: + def __init__(self, user_pool_id, user_pool_client_id, region, request_client=None): + self.region = region + if not self.region: + raise FlaskAWSCognitoError("No AWS region provided") + self.user_pool_id = user_pool_id + self.user_pool_client_id = user_pool_client_id + self.claims = None + if not request_client: + self.request_client = requests.get + else: + self.request_client = request_client + self._load_jwk_keys() + + def _load_jwk_keys(self): + keys_url = f"https://cognito-idp.{self.region}.amazonaws.com/{self.user_pool_id}/.well-known/jwks.json" + try: + response = self.request_client(keys_url) + self.jwk_keys = response.json()["keys"] + except requests.exceptions.RequestException as e: + raise FlaskAWSCognitoError(str(e)) from e + + @staticmethod + def _extract_headers(token): + try: + headers = jwt.get_unverified_headers(token) + return headers + except JOSEError as e: + raise TokenVerifyError(str(e)) from e + + def _find_pkey(self, headers): + kid = headers["kid"] + # search for the kid in the downloaded public keys + key_index = -1 + for i in range(len(self.jwk_keys)): + if kid == self.jwk_keys[i]["kid"]: + key_index = i + break + if key_index == -1: + raise TokenVerifyError("Public key not found in jwks.json") + return self.jwk_keys[key_index] + + @staticmethod + def _verify_signature(token, pkey_data): + try: + # construct the public key + public_key = jwk.construct(pkey_data) + except JOSEError as e: + raise TokenVerifyError(str(e)) from e + # get the last two sections of the token, + # message and signature (encoded in base64) + message, encoded_signature = str(token).rsplit(".", 1) + # decode the signature + decoded_signature = base64url_decode(encoded_signature.encode("utf-8")) + # verify the signature + if not public_key.verify(message.encode("utf8"), decoded_signature): + raise TokenVerifyError("Signature verification failed") + + @staticmethod + def _extract_claims(token): + try: + claims = jwt.get_unverified_claims(token) + return claims + except JOSEError as e: + raise TokenVerifyError(str(e)) from e + + @staticmethod + def _check_expiration(claims, current_time): + if not current_time: + current_time = time.time() + if current_time > claims["exp"]: + raise TokenVerifyError("Token is expired") # probably another exception + + def _check_audience(self, claims): + # and the Audience (use claims['client_id'] if verifying an access token) + audience = claims["aud"] if "aud" in claims else claims["client_id"] + if audience != self.user_pool_client_id: + raise TokenVerifyError("Token was not issued for this audience") + + def verify(self, token, current_time=None): + """ https://github.com/awslabs/aws-support-tools/blob/master/Cognito/decode-verify-jwt/decode-verify-jwt.py """ + if not token: + raise TokenVerifyError("No token provided") + + headers = self._extract_headers(token) + pkey_data = self._find_pkey(headers) + self._verify_signature(token, pkey_data) + + claims = self._extract_claims(token) + self._check_expiration(claims, current_time) + #self._check_audience(claims) + + self.claims = claims + return claims \ No newline at end of file diff --git a/backend-flask/lib/db.py b/backend-flask/lib/db.py new file mode 100644 index 000000000..7294620d3 --- /dev/null +++ b/backend-flask/lib/db.py @@ -0,0 +1,144 @@ +from psycopg_pool import ConnectionPool +import os, sys, re +from flask import current_app as app + +class Db: + def __init__(self): + self.init_pool() + + def template(self,*args): + + pathing = list((app.root_path,'db','sql',) + args) + pathing[-1] = pathing[-1] + ".sql" + + template_path = os.path.join(*pathing) + + green = '\033[92m' + no_color = '\033[0m' + print("\n") + print(f'{green} Load SQL Template: {template_path} {no_color}') + + with open(template_path, 'r') as f: + template_content = f.read() + return template_content + + def init_pool(self): + connection_url = os.getenv("CONNECTION_URL") + self.pool = ConnectionPool(connection_url) + # when we want to commit data such as an insert + # be sure to check for RETURNING in all uppercases + def query_commit(self,sql,params={},verbose=True): + if verbose: + self.print_sql('commit with returning',sql,params) + + pattern = r"\bRETURNING\b" + is_returning_id = re.search(pattern, sql) + if is_returning_id: + print("Found a match") + else: + print("No match found") + + try: + with self.pool.connection() as conn: + with conn.cursor() as cur: + #conn = self.pool.connection() + #cur = conn.cursor() + cur.execute(sql, params) + if is_returning_id: + returning_id = cur.fetchone()[0] + conn.commit() + if is_returning_id: + return returning_id + except Exception as error: + self.print_sql_err(error) + #conn.rollback + + def print_sql(self,title,sql,params={}): + cyan = '\033[96m' + no_color = '\033[0m' + print(f'{cyan} SQL STATEMENT-[{title}]------{no_color}') + print(sql,params) + + def print_params(self,params): + blue = '\033[94m' + no_color = '\033[0m' + print(f'{blue} SQL Params:{no_color}') + for key, value in params.items(): + print(key, ":", value) + + def query_value(self,sql,params={},verbose=True): + if verbose: + self.print_sql('value',sql,params) + with self.pool.connection() as conn: + with conn.cursor() as cur: + cur.execute(sql,params) + json = cur.fetchone() + if json == None: + return None + else: + return json[0] + + def query_wrap_object(self,template): + sql = f""" + (SELECT COALESCE(row_to_json(object_row),'{{}}'::json) FROM ( + {template} + ) object_row); + """ + return sql + + def query_wrap_array(self,template): + sql = f""" + (SELECT COALESCE(array_to_json(array_agg(row_to_json(array_row))),'[]'::json) FROM ( + {template} + ) array_row); + """ + return sql + + #When we want to return a json object + def query_array_json(self, sql, params={},verbose=True): + if verbose: + self.print_sql('array',sql,params) + + wrapped_sql = self.query_wrap_array(sql) + with self.pool.connection() as conn: + with conn.cursor() as cur: + cur.execute(wrapped_sql, params) + # this will return a tuple + # the first field being the data + json = cur.fetchone() + return json[0] + + + #When we want to return an array of json objects + def query_object_json(self,sql,params={},verbose=True): + if verbose: + self.print_sql('json',sql,params) + self.print_params(params) + wrapped_sql = self.query_wrap_object(sql) + with self.pool.connection() as conn: + with conn.cursor() as cur: + cur.execute(wrapped_sql,params) + # this will return a tuple + # the first field being the data + json = cur.fetchone() + if json == None: + "{}" + else: + return json[0] + + def print_sql_err(self,err): + # get details about the exception + err_type, err_obj, traceback = sys.exc_info() + + # get the line number when exception occured + line_num = traceback.tb_lineno + + # print the connect() error + print ("\npsycopg ERROR:", err, "on line number:", line_num) + print ("psycopg traceback:", traceback, "-- type:", err_type) + + # print the pgcode and pgerror exceptions + #print ("pgerror:", err.pgerror) + #print ("pgcode:", err.pgcode, "\n") + +db = Db() \ No newline at end of file diff --git a/backend-flask/lib/ddb.py b/backend-flask/lib/ddb.py new file mode 100644 index 000000000..19234931c --- /dev/null +++ b/backend-flask/lib/ddb.py @@ -0,0 +1,173 @@ +import boto3 +import botocore.exceptions +import sys +from datetime import datetime, timedelta, timezone +import uuid +import os + +class Ddb: + def client(): + endpoint_url = os.getenv("AWS_ENDPOINT_URL") + if endpoint_url: + attrs = { 'endpoint_url': endpoint_url } + else: + attrs = {} + dynamodb = boto3.client('dynamodb',**attrs) + return dynamodb + + def list_message_groups(client,my_user_uuid): + current_year = datetime.now().year + table_name = 'cruddur-messages' + query_params = { + 'TableName': table_name, + 'KeyConditionExpression': 'pk = :pk AND begins_with(sk,:year)', + 'ScanIndexForward': False, + 'Limit': 20, + 'ExpressionAttributeValues': { + ':year': {'S': str(current_year) }, + ':pk': {'S': f"GRP#{my_user_uuid}"} + } + } + print('query-params') + print(query_params) + print('client') + print(client) + + # query the table + response = client.query(**query_params) + items = response['Items'] + + results = [] + for item in items: + last_sent_at = item['sk']['S'] + results.append({ + 'uuid': item['message_group_uuid']['S'], + 'display_name': item['user_display_name']['S'], + 'handle': item['user_handle']['S'], + 'message': item['message']['S'], + 'created_at': last_sent_at + }) + return results + def list_messages(client,message_group_uuid): + current_year = datetime.now().year + table_name = 'cruddur-messages' + query_params = { + 'TableName': table_name, + 'KeyConditionExpression': 'pk = :pk AND begins_with(sk,:year)', + 'ScanIndexForward': False, + 'Limit': 40, + 'ExpressionAttributeValues': { + ':year': {'S': str(current_year) }, + ':pk': {'S': f"MSG#{message_group_uuid}"} + } + } + + response = client.query(**query_params) + items = response['Items'] + items.reverse() + + results = [] + for item in items: + created_at = item['sk']['S'] + results.append({ + 'uuid': item['message_uuid']['S'], + 'display_name': item['user_display_name']['S'], + 'handle': item['user_handle']['S'], + 'message': item['message']['S'], + 'created_at': created_at + }) + print("EEE") + print(results) + return results + def create_message(client,message_group_uuid, message, my_user_uuid, my_user_display_name, my_user_handle): + now = datetime.now(timezone.utc).astimezone().isoformat() + created_at = now + message_uuid = str(uuid.uuid4()) + + record = { + 'pk': {'S': f"MSG#{message_group_uuid}"}, + 'sk': {'S': created_at }, + 'message': {'S': message}, + 'message_uuid': {'S': message_uuid}, + 'user_uuid': {'S': my_user_uuid}, + 'user_display_name': {'S': my_user_display_name}, + 'user_handle': {'S': my_user_handle} + } + # insert the record into the table + table_name = 'cruddur-messages' + response = client.put_item( + TableName=table_name, + Item=record + ) + # print the response + print(response) + + return { + 'message_group_uuid': message_group_uuid, + 'uuid': my_user_uuid, + 'display_name': my_user_display_name, + 'handle': my_user_handle, + 'message': message, + 'created_at': created_at + } + def create_message_group(client, message,my_user_uuid, my_user_display_name, my_user_handle, other_user_uuid, other_user_display_name, other_user_handle): + print('== create_message_group.1') + table_name = 'cruddur-messages' + + message_group_uuid = str(uuid.uuid4()) + message_uuid = str(uuid.uuid4()) + now = datetime.now(timezone.utc).astimezone().isoformat() + last_message_at = now + created_at = now + print('== create_message_group.2') + + my_message_group = { + 'pk': {'S': f"GRP#{my_user_uuid}"}, + 'sk': {'S': last_message_at}, + 'message_group_uuid': {'S': message_group_uuid}, + 'message': {'S': message}, + 'user_uuid': {'S': other_user_uuid}, + 'user_display_name': {'S': other_user_display_name}, + 'user_handle': {'S': other_user_handle} + } + + print('== create_message_group.3') + other_message_group = { + 'pk': {'S': f"GRP#{other_user_uuid}"}, + 'sk': {'S': last_message_at}, + 'message_group_uuid': {'S': message_group_uuid}, + 'message': {'S': message}, + 'user_uuid': {'S': my_user_uuid}, + 'user_display_name': {'S': my_user_display_name}, + 'user_handle': {'S': my_user_handle} + } + + print('== create_message_group.4') + message = { + 'pk': {'S': f"MSG#{message_group_uuid}"}, + 'sk': {'S': created_at }, + 'message': {'S': message}, + 'message_uuid': {'S': message_uuid}, + 'user_uuid': {'S': my_user_uuid}, + 'user_display_name': {'S': my_user_display_name}, + 'user_handle': {'S': my_user_handle} + } + + items = { + table_name: [ + {'PutRequest': {'Item': my_message_group}}, + {'PutRequest': {'Item': other_message_group}}, + {'PutRequest': {'Item': message}} + ] + } + + try: + print('== create_message_group.try') + # Begin the transaction + response = client.batch_write_item(RequestItems=items) + return { + 'message_group_uuid': message_group_uuid + } + except botocore.exceptions.ClientError as e: + print('== create_message_group.error') + print(e) \ No newline at end of file diff --git a/backend-flask/openapi-3.0.yml b/backend-flask/openapi-3.0.yml index 27f4c6658..db7597d39 100644 --- a/backend-flask/openapi-3.0.yml +++ b/backend-flask/openapi-3.0.yml @@ -148,6 +148,21 @@ paths: type: array items: $ref: '#/components/schemas/Message' + /api/activities/notifications: + get: + description: 'Return a feed of activity for all my followers' + tags: + - activities + parameters: [] + responses: + '200': + description: Returns an array of activities + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Activity' components: schemas: @@ -159,10 +174,10 @@ components: example: 0056a369-4618-43a4-ad88-e7c371bf5582 display_name: type: string - example: "Andrew Brown" + example: "Papa Moussa FALL" handle: type: string - example: "andrewbrown" + example: "papamfall" message: type: string example: "Who likes pineapple on their pizza?" @@ -192,7 +207,7 @@ components: example: 0056a369-4618-43a4-ad88-e7c371bf5582 handle: type: string - example: "andrewbrown" + example: "papamfall" message: type: string example: "Who likes pineapple on their pizza?" @@ -217,10 +232,10 @@ components: example: 0056a369-4618-43a4-ad88-e7c371bf5582 display_name: type: string - example: "Andrew Brown" + example: "Papa Moussa FALL" handle: type: string - example: "andrewbrown" + example: "papamfall" message: type: string example: "Who likes pineapple on their pizza?" diff --git a/backend-flask/requirements.txt b/backend-flask/requirements.txt index 70114e3f1..7be32eeb4 100644 --- a/backend-flask/requirements.txt +++ b/backend-flask/requirements.txt @@ -1,2 +1,15 @@ -flask -flask-cors \ No newline at end of file +flask==2.2.4 +flask-cors +opentelemetry-api +opentelemetry-sdk +opentelemetry-exporter-otlp-proto-http +opentelemetry-instrumentation-flask +opentelemetry-instrumentation-requests +aws-xray-sdk +watchtower +blinker +rollbar +flask-awscognito +psycopg[binary] +psycopg[pool] +boto3 \ No newline at end of file diff --git a/backend-flask/run_backend_app.sh b/backend-flask/run_backend_app.sh new file mode 100644 index 000000000..a80012dac --- /dev/null +++ b/backend-flask/run_backend_app.sh @@ -0,0 +1 @@ +python3 -m flask run --host=0.0.0.0 --port 4567 \ No newline at end of file diff --git a/backend-flask/services/create_activity.py b/backend-flask/services/create_activity.py index 2c7ab33b2..6c4a130dc 100644 --- a/backend-flask/services/create_activity.py +++ b/backend-flask/services/create_activity.py @@ -1,5 +1,6 @@ -import uuid from datetime import datetime, timedelta, timezone +#from lib.db import pool, print_sql_err, query_commit +from lib.db import db class CreateActivity: def run(message, user_handle, ttl): model = { @@ -40,12 +41,29 @@ def run(message, user_handle, ttl): 'message': message } else: - model['data'] = { - 'uuid': uuid.uuid4(), - 'display_name': 'Andrew Brown', - 'handle': user_handle, - 'message': message, - 'created_at': now.isoformat(), - 'expires_at': (now + ttl_offset).isoformat() - } - return model \ No newline at end of file + expires_at = (now + ttl_offset) + uuid = CreateActivity.create_activity(user_handle,message,expires_at) + + object_json = CreateActivity.query_object_activity(uuid) + model['data'] = object_json + + return model + + def create_activity(handle,message, expires_at): + sql = db.template('activities','create_activities') + + uuid = db.query_commit(sql,{ + 'handle': handle, + 'message': message, + 'expires_at': expires_at + } + ) + return uuid + + def query_object_activity(uuid): + sql = db.template('activities','object') + + return db.query_object_json(sql,{ + 'uuid': uuid + } + ) diff --git a/backend-flask/services/create_message.py b/backend-flask/services/create_message.py index 83bd218ed..553b50460 100644 --- a/backend-flask/services/create_message.py +++ b/backend-flask/services/create_message.py @@ -1,16 +1,28 @@ -import uuid from datetime import datetime, timedelta, timezone + +from lib.db import db +from lib.ddb import Ddb +#from lib.momento import MomentoCounter + class CreateMessage: - def run(message, user_sender_handle, user_receiver_handle): + # mode indicates if we want to create a new message_group or using an existing one + def run(mode, message, cognito_user_id, message_group_uuid=None, user_receiver_handle=None): model = { 'errors': None, 'data': None } - if user_sender_handle == None or len(user_sender_handle) < 1: - model['errors'] = ['user_sender_handle_blank'] - if user_receiver_handle == None or len(user_receiver_handle) < 1: - model['errors'] = ['user_reciever_handle_blank'] + if (mode == "update"): + if message_group_uuid == None or len(message_group_uuid) < 1: + model['errors'] = ['message_group_uuid_blank'] + + + if cognito_user_id == None or len(cognito_user_id) < 1: + model['errors'] = ['cognito_user_id_blank'] + + if (mode == "create"): + if user_receiver_handle == None or len(user_receiver_handle) < 1: + model['errors'] = ['user_reciever_handle_blank'] if message == None or len(message) < 1: model['errors'] = ['message_blank'] @@ -25,12 +37,49 @@ def run(message, user_sender_handle, user_receiver_handle): 'message': message } else: - now = datetime.now(timezone.utc).astimezone() - model['data'] = { - 'uuid': uuid.uuid4(), - 'display_name': 'Andrew Brown', - 'handle': user_sender_handle, - 'message': message, - 'created_at': now.isoformat() - } + sql = db.template('users','create_message_users') + + if user_receiver_handle == None: + rev_handle = '' + else: + rev_handle = user_receiver_handle + users = db.query_array_json(sql,{ + 'cognito_user_id': cognito_user_id, + 'user_receiver_handle': rev_handle + }) + print("USERS =-=-=-=-==") + print(users) + + my_user = next((item for item in users if item["kind"] == 'sender'), None) + other_user = next((item for item in users if item["kind"] == 'recv') , None) + + print("USERS=[my-user]==") + print(my_user) + print("USERS=[other-user]==") + print(other_user) + + ddb = Ddb.client() + + if (mode == "update"): + data = Ddb.create_message( + client=ddb, + message_group_uuid=message_group_uuid, + message=message, + my_user_uuid=my_user['uuid'], + my_user_display_name=my_user['display_name'], + my_user_handle=my_user['handle'] + ) + elif (mode == "create"): + data = Ddb.create_message_group( + client=ddb, + message=message, + my_user_uuid=my_user['uuid'], + my_user_display_name=my_user['display_name'], + my_user_handle=my_user['handle'], + other_user_uuid=other_user['uuid'], + other_user_display_name=other_user['display_name'], + other_user_handle=other_user['handle'] + ) + #MomentoCounter.incr(f"msgs/{user_handle}") + model['data'] = data return model \ No newline at end of file diff --git a/backend-flask/services/create_reply.py b/backend-flask/services/create_reply.py index cdeb53bcd..d1d9846ee 100644 --- a/backend-flask/services/create_reply.py +++ b/backend-flask/services/create_reply.py @@ -21,7 +21,7 @@ def run(message, user_handle, activity_uuid): if model['errors']: # return what we provided model['data'] = { - 'display_name': 'Andrew Brown', + 'display_name': 'Papa Moussa FALL', 'handle': user_sender_handle, 'message': message, 'reply_to_activity_uuid': activity_uuid @@ -30,7 +30,7 @@ def run(message, user_handle, activity_uuid): now = datetime.now(timezone.utc).astimezone() model['data'] = { 'uuid': uuid.uuid4(), - 'display_name': 'Andrew Brown', + 'display_name': 'Papa Moussa FALL', 'handle': user_handle, 'message': message, 'created_at': now.isoformat(), diff --git a/backend-flask/services/home_activities.py b/backend-flask/services/home_activities.py index 038e7db56..014f419f8 100644 --- a/backend-flask/services/home_activities.py +++ b/backend-flask/services/home_activities.py @@ -1,44 +1,17 @@ from datetime import datetime, timedelta, timezone +from lib.db import db +# Honeycomb +from opentelemetry import trace +tracer = trace.get_tracer("home.activities") + +# CloudWatch +import logging + class HomeActivities: - def run(): - now = datetime.now(timezone.utc).astimezone() - results = [{ - 'uuid': '68f126b0-1ceb-4a33-88be-d90fa7109eee', - 'handle': 'Andrew Brown', - 'message': 'Cloud is fun!', - 'created_at': (now - timedelta(days=2)).isoformat(), - 'expires_at': (now + timedelta(days=5)).isoformat(), - 'likes_count': 5, - 'replies_count': 1, - 'reposts_count': 0, - 'replies': [{ - 'uuid': '26e12864-1c26-5c3a-9658-97a10f8fea67', - 'reply_to_activity_uuid': '68f126b0-1ceb-4a33-88be-d90fa7109eee', - 'handle': 'Worf', - 'message': 'This post has no honor!', - 'likes_count': 0, - 'replies_count': 0, - 'reposts_count': 0, - 'created_at': (now - timedelta(days=2)).isoformat() - }], - }, - { - 'uuid': '66e12864-8c26-4c3a-9658-95a10f8fea67', - 'handle': 'Worf', - 'message': 'I am out of prune juice', - 'created_at': (now - timedelta(days=7)).isoformat(), - 'expires_at': (now + timedelta(days=9)).isoformat(), - 'likes': 0, - 'replies': [] - }, - { - 'uuid': '248959df-3079-4947-b847-9e0892d1bab4', - 'handle': 'Garek', - 'message': 'My dear doctor, I am just simple tailor', - 'created_at': (now - timedelta(hours=1)).isoformat(), - 'expires_at': (now + timedelta(hours=12)).isoformat(), - 'likes': 0, - 'replies': [] - } - ] + #def run(logger): # CloudWatch + def run(cognito_user_id=None): + sql = db.template('activities','home') + + results = db.query_array_json(sql) + return results \ No newline at end of file diff --git a/backend-flask/services/message_groups.py b/backend-flask/services/message_groups.py index eb3cde21d..16be14136 100644 --- a/backend-flask/services/message_groups.py +++ b/backend-flask/services/message_groups.py @@ -1,24 +1,22 @@ -from datetime import datetime, timedelta, timezone +from lib.ddb import Ddb +from lib.db import db + class MessageGroups: - def run(user_handle): + def run(cognito_user_id): model = { 'errors': None, 'data': None } - now = datetime.now(timezone.utc).astimezone() - results = [ - { - 'uuid': '24b95582-9e7b-4e0a-9ad1-639773ab7552', - 'display_name': 'Andrew Brown', - 'handle': 'andrewbrown', - 'created_at': now.isoformat() - }, - { - 'uuid': '417c360e-c4e6-4fce-873b-d2d71469b4ac', - 'display_name': 'Worf', - 'handle': 'worf', - 'created_at': now.isoformat() - }] - model['data'] = results + sql = db.template('users','uuid_from_cognito_user_id') + my_user_uuid = db.query_value(sql,{'cognito_user_id': cognito_user_id}) + + print("UUID",my_user_uuid) + + + ddb = Ddb.client() + data = Ddb.list_message_groups(ddb, my_user_uuid) + print("list_message_groups:",data) + + model['data'] = data return model \ No newline at end of file diff --git a/backend-flask/services/messages.py b/backend-flask/services/messages.py index 61e01f556..ca106206e 100644 --- a/backend-flask/services/messages.py +++ b/backend-flask/services/messages.py @@ -1,27 +1,23 @@ from datetime import datetime, timedelta, timezone +from lib.ddb import Ddb +from lib.db import db + class Messages: - def run(user_sender_handle, user_receiver_handle): + def run(cognito_user_id, message_group_uuid): model = { 'errors': None, 'data': None } - now = datetime.now(timezone.utc).astimezone() + sql = db.template('users','uuid_from_cognito_user_id') + my_user_uuid = db.query_value(sql,{'cognito_user_id': cognito_user_id}) + + print("UUID",my_user_uuid) + + ddb = Ddb.client() + data = Ddb.list_messages(ddb, message_group_uuid) + print("list_messages") + print(data) - results = [ - { - 'uuid': '4e81c06a-db0f-4281-b4cc-98208537772a' , - 'display_name': 'Andrew Brown', - 'handle': 'andrewbrown', - 'message': 'Cloud is fun!', - 'created_at': now.isoformat() - }, - { - 'uuid': '66e12864-8c26-4c3a-9658-95a10f8fea67', - 'display_name': 'Andrew Brown', - 'handle': 'andrewbrown', - 'message': 'This platform is great!', - 'created_at': now.isoformat() - }] - model['data'] = results + model['data'] = data return model \ No newline at end of file diff --git a/backend-flask/services/notifications_activities.py b/backend-flask/services/notifications_activities.py new file mode 100644 index 000000000..588ed70da --- /dev/null +++ b/backend-flask/services/notifications_activities.py @@ -0,0 +1,37 @@ +from datetime import datetime, timedelta, timezone +from aws_xray_sdk.core import xray_recorder + +class NotificationsActivities: + def run(): + #subsegment = xray_recorder.begin_subsegment('notifications_activities') + now = datetime.now(timezone.utc).astimezone() + results = [{ + 'uuid': '68f126b0-1ceb-4a33-88be-d90fa7109eee', + 'handle': 'Papa Moussa FALL', + 'message': 'Cloud Devops Engineering is very passionate!', + 'created_at': (now - timedelta(days=2)).isoformat(), + 'expires_at': (now + timedelta(days=5)).isoformat(), + 'likes_count': 5, + 'replies_count': 1, + 'reposts_count': 0, + 'replies': [{ + 'uuid': '26e12864-1c26-5c3a-9658-97a10f8fea67', + 'reply_to_activity_uuid': '68f126b0-1ceb-4a33-88be-d90fa7109eee', + 'handle': 'Worf', + 'message': 'This post has no honor!', + 'likes_count': 0, + 'replies_count': 0, + 'reposts_count': 0, + 'created_at': (now - timedelta(days=2)).isoformat() + }], + } + ] + + #dict = { + # "now": now.isoformat(), + # "result_size": len(results) + #} + #subsegment.put_metadata('key', dict, 'namespace') + #xray_recorder.end_subsegment() + + return results \ No newline at end of file diff --git a/backend-flask/services/search_activities.py b/backend-flask/services/search_activities.py index a3e9b542e..90ea6fd93 100644 --- a/backend-flask/services/search_activities.py +++ b/backend-flask/services/search_activities.py @@ -13,7 +13,7 @@ def run(search_term): else: results = [{ 'uuid': '248959df-3079-4947-b847-9e0892d1bab4', - 'handle': 'Andrew Brown', + 'handle': 'Papa Moussa FALL', 'message': 'Cloud is fun!', 'created_at': now.isoformat() }] diff --git a/backend-flask/services/show_activity.py b/backend-flask/services/show_activity.py index 205e80e2e..dd17dcd82 100644 --- a/backend-flask/services/show_activity.py +++ b/backend-flask/services/show_activity.py @@ -4,7 +4,7 @@ def run(activity_uuid): now = datetime.now(timezone.utc).astimezone() results = [{ 'uuid': '68f126b0-1ceb-4a33-88be-d90fa7109eee', - 'handle': 'Andrew Brown', + 'handle': 'Papa Moussa FALL', 'message': 'Cloud is fun!', 'created_at': (now - timedelta(days=2)).isoformat(), 'expires_at': (now + timedelta(days=5)).isoformat(), diff --git a/backend-flask/services/update_profile.py b/backend-flask/services/update_profile.py new file mode 100644 index 000000000..e5d115fc8 --- /dev/null +++ b/backend-flask/services/update_profile.py @@ -0,0 +1,39 @@ +from lib.db import db + +class UpdateProfile: + def run(cognito_user_id,bio,display_name): + model = { + 'errors': None, + 'data': None + } + + if display_name == None or len(display_name) < 1: + model['errors'] = ['display_name_blank'] + + if model['errors']: + model['data'] = { + 'bio': bio, + 'display_name': display_name + } + else: + handle = UpdateProfile.update_profile(bio,display_name,cognito_user_id) + data = UpdateProfile.query_users_short(handle) + model['data'] = data + return model + + def update_profile(bio,display_name,cognito_user_id): + if bio == None: + bio = '' + + sql = db.template('users','update') + handle = db.query_commit(sql,{ + 'cognito_user_id': cognito_user_id, + 'bio': bio, + 'display_name': display_name + }) + def query_users_short(handle): + sql = db.template('users','short') + data = db.query_object_json(sql,{ + 'handle': handle + }) + return data \ No newline at end of file diff --git a/backend-flask/services/user_activities.py b/backend-flask/services/user_activities.py index c3c31fc91..59bd93e26 100644 --- a/backend-flask/services/user_activities.py +++ b/backend-flask/services/user_activities.py @@ -1,4 +1,6 @@ from datetime import datetime, timedelta, timezone + +from lib.db import db class UserActivities: def run(user_handle): model = { @@ -11,13 +13,8 @@ def run(user_handle): if user_handle == None or len(user_handle) < 1: model['errors'] = ['blank_user_handle'] else: - now = datetime.now() - results = [{ - 'uuid': '248959df-3079-4947-b847-9e0892d1bab4', - 'handle': 'Andrew Brown', - 'message': 'Cloud is fun!', - 'created_at': (now - timedelta(days=1)).isoformat(), - 'expires_at': (now + timedelta(days=31)).isoformat() - }] + sql = db.template('users','show') + results = db.query_object_json(sql,{'handle': user_handle}) + #results = db.query_array_json(sql) model['data'] = results return model \ No newline at end of file diff --git a/backend-flask/services/users_short.py b/backend-flask/services/users_short.py new file mode 100644 index 000000000..32e9d3665 --- /dev/null +++ b/backend-flask/services/users_short.py @@ -0,0 +1,9 @@ +from lib.db import db + +class UsersShort: + def run(handle): + sql = db.template('users','short') + results = db.query_object_json(sql,{ + 'handle': handle + }) + return results \ No newline at end of file diff --git a/backend-flask/session-manager-plugin.deb b/backend-flask/session-manager-plugin.deb new file mode 100644 index 000000000..e841652de Binary files /dev/null and b/backend-flask/session-manager-plugin.deb differ diff --git a/bin/avatar/build b/bin/avatar/build new file mode 100755 index 000000000..65593bb60 --- /dev/null +++ b/bin/avatar/build @@ -0,0 +1,14 @@ +#! /usr/bin/bash + + +ABS_PATH=$(readlink -f "$0") +SERVERLESS_PATH=$(dirname $ABS_PATH) +BIN_PATH=$(dirname $SERVERLESS_PATH) +PROJECT_PATH=$(dirname $BIN_PATH) +SERVERLESS_PROJECT_PATH="$PROJECT_PATH/thumbing-serverless-cdk" + +cd $SERVERLESS_PROJECT_PATH + +npm install +rm -rf node_modules/sharp +SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install --arch=x64 --platform=linux --libc=glibc sharp \ No newline at end of file diff --git a/bin/avatar/clear b/bin/avatar/clear new file mode 100755 index 000000000..190fd0d67 --- /dev/null +++ b/bin/avatar/clear @@ -0,0 +1,9 @@ +#! /usr/bin/bash + + +ABS_PATH=$(readlink -f "$0") +SERVERLESS_PATH=$(dirname $ABS_PATH) +DATA_FILE_PATH="$SERVERLESS_PATH/files/data.jpg" + +aws s3 rm "s3://cruddursn-upload-avatars/avatars/original/data.jpg" +aws s3 rm "s3://assets.$DOMAIN_NAME/avatars/processed/data.jpg" \ No newline at end of file diff --git a/bin/avatar/files/data.jpg b/bin/avatar/files/data.jpg new file mode 100644 index 000000000..c01ea56e5 Binary files /dev/null and b/bin/avatar/files/data.jpg differ diff --git a/bin/avatar/ls b/bin/avatar/ls new file mode 100644 index 000000000..6cd87a2c0 --- /dev/null +++ b/bin/avatar/ls @@ -0,0 +1,4 @@ +#! /usr/bin/bash + + +aws s3 ls \ No newline at end of file diff --git a/bin/avatar/upload b/bin/avatar/upload new file mode 100755 index 000000000..a25d24080 --- /dev/null +++ b/bin/avatar/upload @@ -0,0 +1,8 @@ +#! /usr/bin/bash + + +ABS_PATH=$(readlink -f "$0") +SERVERLESS_PATH=$(dirname $ABS_PATH) +DATA_FILE_PATH="$SERVERLESS_PATH/files/data.jpg" + +aws s3 cp "$DATA_FILE_PATH" "s3://cruddursn-upload-avatars/data.jpg" \ No newline at end of file diff --git a/bin/backend/build b/bin/backend/build new file mode 100755 index 000000000..d4df59d95 --- /dev/null +++ b/bin/backend/build @@ -0,0 +1,12 @@ +#! /usr/bin/bash + +ABS_PATH=$(readlink -f "$0") +BACKEND_PATH=$(dirname $ABS_PATH) +BIN_PATH=$(dirname $BACKEND_PATH) +PROJECT_PATH=$(dirname $BIN_PATH) +BACKEND_FLASK_PATH="$PROJECT_PATH/backend-flask" + +docker build \ +-f "$BACKEND_FLASK_PATH/Dockerfile.prod" \ +-t backend-flask-prod \ +"$BACKEND_FLASK_PATH/." \ No newline at end of file diff --git a/bin/backend/connect b/bin/backend/connect new file mode 100755 index 000000000..85f69f67f --- /dev/null +++ b/bin/backend/connect @@ -0,0 +1,19 @@ +#! /usr/bin/bash +if [ -z "$1" ]; then + echo "No TASK_ID argument supplied eg ./bin/ecs/connect-to-backend-flask 99b2f8953616495e99545e5a6066fbb5d" + exit 1 +fi +TASK_ID=$1 + +CONTAINER_NAME=backend-flask + +echo "TASK ID : $TASK_ID" +echo "Container Name: $CONTAINER_NAME" + +aws ecs execute-command \ +--region $AWS_DEFAULT_REGION \ +--cluster cruddur \ +--task $TASK_ID \ +--container $CONTAINER_NAME \ +--command "/bin/bash" \ +--interactive \ No newline at end of file diff --git a/bin/backend/deploy b/bin/backend/deploy new file mode 100755 index 000000000..a5622d673 --- /dev/null +++ b/bin/backend/deploy @@ -0,0 +1,26 @@ +#! /usr/bin/bash + +CLUSTER_NAME="cruddur" +SERVICE_NAME="backend-flask" +TASK_DEFINTION_FAMILY="backend-flask" + + +LATEST_TASK_DEFINITION_ARN=$(aws ecs describe-task-definition \ +--task-definition $TASK_DEFINTION_FAMILY \ +--query 'taskDefinition.taskDefinitionArn' \ +--output text) + +echo "TASK DEF ARN:" +echo $LATEST_TASK_DEFINITION_ARN + +aws ecs update-service \ +--cluster $CLUSTER_NAME \ +--service $SERVICE_NAME \ +--task-definition $LATEST_TASK_DEFINITION_ARN \ +--force-new-deployment + +#aws ecs describe-services \ +#--cluster $CLUSTER_NAME \ +#--service $SERVICE_NAME \ +#--query 'services[0].deployments' \ +#--output table \ No newline at end of file diff --git a/bin/backend/generate-env b/bin/backend/generate-env new file mode 100755 index 000000000..69a979b5e --- /dev/null +++ b/bin/backend/generate-env @@ -0,0 +1,9 @@ +#!/usr/bin/env ruby + +require 'erb' + +template = File.read 'erb/backend-flask.env.erb' +content = ERB.new(template).result(binding) +filename = "backend-flask.env" +File.write(filename, content) +puts "... env variables generated for backend-flask application" \ No newline at end of file diff --git a/bin/backend/push b/bin/backend/push new file mode 100755 index 000000000..a6359a5b5 --- /dev/null +++ b/bin/backend/push @@ -0,0 +1,7 @@ +#! /usr/bin/bash + +ECR_BACKEND_FLASK_URL="$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/backend-flask" +echo $ECR_BACKEND_FLASK_URL + +docker tag backend-flask-prod:latest $ECR_BACKEND_FLASK_URL:latest +docker push $ECR_BACKEND_FLASK_URL:latest \ No newline at end of file diff --git a/bin/backend/register b/bin/backend/register new file mode 100755 index 000000000..d9e12d585 --- /dev/null +++ b/bin/backend/register @@ -0,0 +1,12 @@ +#! /usr/bin/bash + +ABS_PATH=$(readlink -f "$0") +FRONTEND_PATH=$(dirname $ABS_PATH) +BIN_PATH=$(dirname $FRONTEND_PATH) +PROJECT_PATH=$(dirname $BIN_PATH) +TASK_DEF_PATH="$PROJECT_PATH/aws/task-definitions/backend-flask.json" + +echo $TASK_DEF_PATH + +aws ecs register-task-definition \ +--cli-input-json "file://$TASK_DEF_PATH" \ No newline at end of file diff --git a/bin/backend/run b/bin/backend/run new file mode 100755 index 000000000..689ceca5e --- /dev/null +++ b/bin/backend/run @@ -0,0 +1,29 @@ +#! /usr/bin/bash + +ABS_PATH=$(readlink -f "$0") +BACKEND_PATH=$(dirname $ABS_PATH) +BIN_PATH=$(dirname $BACKEND_PATH) +PROJECT_PATH=$(dirname $BIN_PATH) +ENVFILE_PATH="$PROJECT_PATH/backend-flask.env" + +docker run --rm \ + --env-file $ENVFILE_PATH \ + --network cruddur-net \ + --publish 4567:4567 \ + -it backend-flask-prod + +# --env AWS_ENDPOINT_URL="http://dynamodb-local:8000" \ +# --env CONNECTION_URL="postgresql://postgres:password@db:5432/cruddur" \ +# --env FRONTEND_URL="https://3000-omenking-awsbootcampcru-fz5zc9avjbe.ws-us93.gitpod.io" \ +# --env BACKEND_URL="https://4567-omenking-awsbootcampcru-fz5zc9avjbe.ws-us93.gitpod.io" \ +# --env OTEL_SERVICE_NAME='backend-flask' \ +# --env OTEL_EXPORTER_OTLP_ENDPOINT="https://api.honeycomb.io" \ +# --env OTEL_EXPORTER_OTLP_HEADERS="x-honeycomb-team=SqeqeKIB5xVY7SO9ZnG7EO" \ +# --env AWS_XRAY_URL="*4567-omenking-awsbootcampcru-fz5zc9avjbe.ws-us93.gitpod.io*" \ +# --env AWS_XRAY_DAEMON_ADDRESS="xray-daemon:2000" \ +# --env AWS_DEFAULT_REGION="ca-central-1" \ +# --env AWS_ACCESS_KEY_ID="AKIAVUO25ZPVJYSOSBY2" \ +# --env AWS_SECRET_ACCESS_KEY="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" \ +# --env ROLLBAR_ACCESS_TOKEN="ca8cde9275fe48c69c6eaa7cf504cdf0" \ +# --env AWS_COGNITO_USER_POOL_ID="ca-central-1_CQ4wDfnwc" \ +# --env AWS_COGNITO_USER_POOL_CLIENT_ID="5b6ro31g97urk767adrbrdj1g5" \ \ No newline at end of file diff --git a/bin/bootstrap b/bin/bootstrap new file mode 100755 index 000000000..d518e54f7 --- /dev/null +++ b/bin/bootstrap @@ -0,0 +1,14 @@ +#! /usr/bin/bash +set -e # stop if it fails at any point + +CYAN='\033[1;36m' +NO_COLOR='\033[0m' +LABEL="bootstrap" +printf "${CYAN}====== ${LABEL}${NO_COLOR}\n" + +ABS_PATH=$(readlink -f "$0") +BIN_DIR=$(dirname $ABS_PATH) + +source "$BIN_DIR/ecr/login" +source "$BIN_DIR/backend/generate-env" +source "$BIN_DIR/frontend/generate-env" \ No newline at end of file diff --git a/bin/busybox/busybox b/bin/busybox/busybox new file mode 100755 index 000000000..aa5f75159 --- /dev/null +++ b/bin/busybox/busybox @@ -0,0 +1,6 @@ +#! /usr/bin/bash + +docker run --rm \ + --network cruddur-net \ + --publish 4567:4567 \ + -it busybox \ No newline at end of file diff --git a/bin/cognito/list-users b/bin/cognito/list-users new file mode 100755 index 000000000..7c7015851 --- /dev/null +++ b/bin/cognito/list-users @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +import boto3 +import os +import json + +userpool_id = os.getenv("AWS_COGNITO_USER_POOL_ID") +client = boto3.client('cognito-idp') +params = { + 'UserPoolId': userpool_id, + 'AttributesToGet': [ + 'preferred_username', + 'sub' + ] +} +response = client.list_users(**params) +users = response['Users'] + +print(json.dumps(users, sort_keys=True, indent=2, default=str)) + +dict_users = {} +for user in users: + attrs = user['Attributes'] + sub = next((a for a in attrs if a["Name"] == 'sub'), None) + handle = next((a for a in attrs if a["Name"] == 'preferred_username'), None) + dict_users[handle['Value']] = sub['Value'] + +#print(dict_users) +print(json.dumps(dict_users, sort_keys=True, indent=2, default=str)) diff --git a/bin/db/connect b/bin/db/connect new file mode 100755 index 000000000..86fe8afb0 --- /dev/null +++ b/bin/db/connect @@ -0,0 +1,9 @@ +#! /usr/bin/bash +if [ "$1" = "prod" ]; then + echo "Running in production mode" + URL=$PROD_CONNECTION_URL +else + URL=$CONNECTION_URL +fi + +psql $URL \ No newline at end of file diff --git a/bin/db/create b/bin/db/create new file mode 100755 index 000000000..1aeb56ce6 --- /dev/null +++ b/bin/db/create @@ -0,0 +1,16 @@ +#! /usr/bin/bash + +CYAN='\033[1;36m' +NO_COLOR='\033[0m' +LABEL="db-create" +printf "${CYAN}== ${LABEL}${NO_COLOR}\n" + +if [ "$1" = "prod" ]; then + echo "Running in production mode" + NO_DB_CONNECTION_URL=$(sed 's/\/cruddur//g' <<<"$PROD_CONNECTION_URL") + psql $NO_DB_CONNECTION_URL -c "create database cruddur;" +else + echo "Running in development mode" + NO_DB_CONNECTION_URL=$(sed 's/\/cruddur//g' <<<"$CONNECTION_URL") + psql $NO_DB_CONNECTION_URL -c "create database cruddur;" +fi \ No newline at end of file diff --git a/bin/db/drop b/bin/db/drop new file mode 100755 index 000000000..b74e2753b --- /dev/null +++ b/bin/db/drop @@ -0,0 +1,15 @@ +#! /usr/bin/bash + +CYAN='\033[1;36m' +NO_COLOR='\033[0m' +LABEL="db-drop" +printf "${CYAN}== ${LABEL}${NO_COLOR}\n" + +if [ "$1" = "prod" ]; then + echo "Running in production mode" + NO_DB_CONNECTION_URL=$(sed 's/\/cruddur//g' <<<"$PROD_CONNECTION_URL") + psql $NO_DB_CONNECTION_URL -c "drop database cruddur;" +else + NO_DB_CONNECTION_URL=$(sed 's/\/cruddur//g' <<<"$CONNECTION_URL") + psql $NO_DB_CONNECTION_URL -c "drop database IF EXISTS cruddur;" +fi diff --git a/bin/db/kill-all b/bin/db/kill-all new file mode 100755 index 000000000..7ea8f6049 --- /dev/null +++ b/bin/db/kill-all @@ -0,0 +1,17 @@ +#! /usr/bin/bash + +CYAN='\033[1;36m' +NO_COLOR='\033[0m' +LABEL="db-kill-all" +printf "${CYAN}== ${LABEL}${NO_COLOR}\n" + +ABS_PATH=$(readlink -f "$0") +DB_PATH=$(dirname $ABS_PATH) +BIN_PATH=$(dirname $DB_PATH) +PROJECT_PATH=$(dirname $BIN_PATH) +BACKEND_FLASK_PATH="$PROJECT_PATH/backend-flask" +kill_path="$BACKEND_FLASK_PATH/db/kill-all-connections.sql" +echo $kill_path + +psql $CONNECTION_URL cruddur < $kill_path + diff --git a/bin/db/migrate b/bin/db/migrate new file mode 100755 index 000000000..9e896eef2 --- /dev/null +++ b/bin/db/migrate @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 + +import os +import sys +import glob +import re +import time +import importlib + +current_path = os.path.dirname(os.path.abspath(__file__)) +parent_path = os.path.abspath(os.path.join(current_path, '..', '..','backend-flask')) +sys.path.append(parent_path) +from lib.db import db + +def get_last_successful_run(): + sql = """ + SELECT last_successful_run + FROM public.schema_information + LIMIT 1 + """ + return int(db.query_value(sql,{},verbose=False)) + +def set_last_successful_run(value): + sql = """ + UPDATE schema_information + SET last_successful_run = %(last_successful_run)s + WHERE id = 1 + """ + db.query_commit(sql,{'last_successful_run': value}) + return value + +last_successful_run = get_last_successful_run() + +migrations_path = os.path.abspath(os.path.join(current_path, '..', '..','backend-flask','db','migrations')) +sys.path.append(migrations_path) +migration_files = glob.glob(f"{migrations_path}/*") + + +for migration_file in migration_files: + filename = os.path.basename(migration_file) + module_name = os.path.splitext(filename)[0] + match = re.match(r'^\d+', filename) + if match: + file_time = int(match.group()) + if last_successful_run <= file_time: + mod = importlib.import_module(module_name) + print('==== running migration: ',module_name) + mod.migration.migrate() + timestamp = str(time.time()).replace(".","") + last_successful_run = set_last_successful_run(timestamp) \ No newline at end of file diff --git a/bin/db/rollback b/bin/db/rollback new file mode 100755 index 000000000..39e3c9dba --- /dev/null +++ b/bin/db/rollback @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 + +import os +import sys +import glob +import re +import time +import importlib + +current_path = os.path.dirname(os.path.abspath(__file__)) +parent_path = os.path.abspath(os.path.join(current_path, '..', '..','backend-flask')) +sys.path.append(parent_path) +from lib.db import db + +def get_last_successful_run(): + sql = """ + SELECT last_successful_run + FROM public.schema_information + LIMIT 1 + """ + return int(db.query_value(sql,{},verbose=False)) + +def set_last_successful_run(value): + sql = """ + UPDATE schema_information + SET last_successful_run = %(last_successful_run)s + WHERE id = 1 + """ + db.query_commit(sql,{'last_successful_run': value}) + return value + +last_successful_run = get_last_successful_run() + +migrations_path = os.path.abspath(os.path.join(current_path, '..', '..','backend-flask','db','migrations')) +sys.path.append(migrations_path) +migration_files = glob.glob(f"{migrations_path}/*") + + +last_migration_file = None +for migration_file in migration_files: + if last_migration_file == None: + filename = os.path.basename(migration_file) + module_name = os.path.splitext(filename)[0] + match = re.match(r'^\d+', filename) + if match: + file_time = int(match.group()) + print("==<><>") + print(last_successful_run, file_time) + print(last_successful_run > file_time) + if last_successful_run > file_time: + last_migration_file = module_name + mod = importlib.import_module(module_name) + print('=== rolling back: ',module_name) + mod.migration.rollback() + set_last_successful_run(file_time) \ No newline at end of file diff --git a/bin/db/schema-load b/bin/db/schema-load new file mode 100755 index 000000000..878bb3000 --- /dev/null +++ b/bin/db/schema-load @@ -0,0 +1,23 @@ +#! /usr/bin/bash + +CYAN='\033[1;36m' +NO_COLOR='\033[0m' +LABEL="db-schema-load" +printf "${CYAN}== ${LABEL}${NO_COLOR}\n" + +ABS_PATH=$(readlink -f "$0") +DB_PATH=$(dirname $ABS_PATH) +BIN_PATH=$(dirname $DB_PATH) +PROJECT_PATH=$(dirname $BIN_PATH) +BACKEND_FLASK_PATH="$PROJECT_PATH/aws-bootcamp-cruddur-2023/backend-flask" +schema_path="$BACKEND_FLASK_PATH/db/schema.sql" +echo $schema_path + +if [ "$1" = "prod" ]; then + echo "Running in production mode" + URL=$PROD_CONNECTION_URL +else + URL=$CONNECTION_URL +fi + +psql $URL cruddur < $schema_path \ No newline at end of file diff --git a/bin/db/seed b/bin/db/seed new file mode 100755 index 000000000..4270c5520 --- /dev/null +++ b/bin/db/seed @@ -0,0 +1,23 @@ +#! /usr/bin/bash + +CYAN='\033[1;36m' +NO_COLOR='\033[0m' +LABEL="db-seed" +printf "${CYAN}== ${LABEL}${NO_COLOR}\n" + +ABS_PATH=$(readlink -f "$0") +DB_PATH=$(dirname $ABS_PATH) +BIN_PATH=$(dirname $DB_PATH) +PROJECT_PATH=$(dirname $BIN_PATH) +BACKEND_FLASK_PATH="$PROJECT_PATH/aws-bootcamp-cruddur-2023/backend-flask" +seed_path="$BACKEND_FLASK_PATH/db/seed.sql" +echo $seed_path + +if [ "$1" = "prod" ]; then + echo "Running in production mode" + URL=$PROD_CONNECTION_URL +else + URL=$CONNECTION_URL +fi + +psql $URL cruddur < $seed_path \ No newline at end of file diff --git a/bin/db/setup b/bin/db/setup new file mode 100755 index 000000000..91891c20d --- /dev/null +++ b/bin/db/setup @@ -0,0 +1,16 @@ +#! /usr/bin/bash +set -e # stop if it fails at any point + +CYAN='\033[1;36m' +NO_COLOR='\033[0m' +LABEL="db-setup" +printf "${CYAN}==== ${LABEL}${NO_COLOR}\n" + +ABS_PATH=$(readlink -f "$0") +DB_PATH=$(dirname $ABS_PATH) + +source "$DB_PATH/db/drop" +source "$DB_PATH/db/create" +source "$DB_PATH/db/schema-load" +source "$DB_PATH/db/seed" +python "$DB_PATH/db/update_cognito_user_ids" \ No newline at end of file diff --git a/bin/db/test b/bin/db/test new file mode 100755 index 000000000..559e00d64 --- /dev/null +++ b/bin/db/test @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 + +import psycopg +import os +import sys + +connection_url = os.getenv("PROD_CONNECTION_URL") + +conn = None +try: + print('attempting connection') + conn = psycopg.connect(connection_url) + print("Connection successful!") +except psycopg.Error as e: + print("Unable to connect to the database:", e) +finally: + conn.close() \ No newline at end of file diff --git a/bin/db/update_cognito_user_ids b/bin/db/update_cognito_user_ids new file mode 100755 index 000000000..1aa433cf1 --- /dev/null +++ b/bin/db/update_cognito_user_ids @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 + +import boto3 +import os +import sys + +print("== db-update-cognito-user-ids") + +current_path = os.path.dirname(os.path.abspath(__file__)) +parent_path = os.path.abspath(os.path.join(current_path, '..', '..','backend-flask')) +sys.path.append(parent_path) +from lib.db import db + +def update_users_with_cognito_user_id(handle,sub): + sql = """ + UPDATE public.users + SET cognito_user_id = %(sub)s + WHERE + users.handle = %(handle)s; + """ + db.query_commit(sql,{ + 'handle' : handle, + 'sub' : sub + }) + +def get_cognito_user_ids(): + userpool_id = os.getenv("AWS_COGNITO_USER_POOL_ID") + client = boto3.client('cognito-idp') + params = { + 'UserPoolId': userpool_id, + 'AttributesToGet': [ + 'preferred_username', + 'sub' + ] + } + response = client.list_users(**params) + users = response['Users'] + dict_users = {} + for user in users: + attrs = user['Attributes'] + sub = next((a for a in attrs if a["Name"] == 'sub'), None) + handle = next((a for a in attrs if a["Name"] == 'preferred_username'), None) + dict_users[handle['Value']] = sub['Value'] + return dict_users + + +users = get_cognito_user_ids() + +for handle, sub in users.items(): + print('----',handle,sub) + update_users_with_cognito_user_id( + handle=handle, + sub=sub + ) \ No newline at end of file diff --git a/bin/db_bootstrap b/bin/db_bootstrap new file mode 100755 index 000000000..41dc963e7 --- /dev/null +++ b/bin/db_bootstrap @@ -0,0 +1,18 @@ +#! /usr/bin/bash +set -e # stop if it fails at any point + +CYAN='\033[1;36m' +NO_COLOR='\033[0m' +LABEL="bootstrap" +printf "${CYAN}====== ${LABEL}${NO_COLOR}\n" + +ABS_PATH=$(readlink -f "$0") +BIN_DIR=$(dirname $ABS_PATH) + +source "$BIN_DIR/db/setup" +source "$BIN_DIR/ddb/schema-load" +source "$BIN_DIR/ddb/seed" + + +#source "$BIN_DIR/backend/generate-env" +#source "$BIN_DIR/frontend/generate-env" \ No newline at end of file diff --git a/bin/ddb/delete-table b/bin/ddb/delete-table new file mode 100755 index 000000000..5924b442d --- /dev/null +++ b/bin/ddb/delete-table @@ -0,0 +1,10 @@ +#! /usr/bin/bash +set -e # stop if it fails at any point + +if [ "$1" = "prod" ]; then + ENDPOINT_URL="" +else + ENDPOINT_URL="--endpoint-url=http://localhost:8000" +fi + +aws dynamodb delete-table $ENDPOINT_URL --table-name 'cruddur-messages' \ No newline at end of file diff --git a/bin/ddb/drop b/bin/ddb/drop new file mode 100755 index 000000000..1fa61a8fc --- /dev/null +++ b/bin/ddb/drop @@ -0,0 +1,23 @@ +#! /usr/bin/bash +set -e # stop if it fails at any point + +# ./bin/ddb/drop cruddur-messages prod + +if [ -z "$1" ]; then + echo "No TABLE_NAME supplied eg ./bin/ddb/drop cruddur-messages prod" + exit 1 +fi + +TABLE_NAME=$1 + +if [ "$1" = "prod" ]; then + ENDPOINT_URL="" +else + ENDPOINT_URL="--endpoint-url=http://localhost:8000" +fi + + +echo "deleting table: $TABLE_NAME ..." + +aws dynamodb delete-table $ENDPOINT_URL \ +--table-name $TABLE_NAME \ No newline at end of file diff --git a/bin/ddb/list-tables b/bin/ddb/list-tables new file mode 100755 index 000000000..733692950 --- /dev/null +++ b/bin/ddb/list-tables @@ -0,0 +1,12 @@ +#! /usr/bin/bash +set -e # stop if it fails at any point + +if [ "$1" = "prod" ]; then + ENDPOINT_URL="" +else + ENDPOINT_URL="--endpoint-url=http://localhost:8000" +fi + +aws dynamodb list-tables $ENDPOINT_URL \ +--query TableNames \ +--output table \ No newline at end of file diff --git a/bin/ddb/patterns/get-conversation b/bin/ddb/patterns/get-conversation new file mode 100755 index 000000000..6d1d214ce --- /dev/null +++ b/bin/ddb/patterns/get-conversation @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 + +import boto3 +import sys +import json +import datetime + +attrs = { + 'endpoint_url': 'http://localhost:8000' +} + +if len(sys.argv) == 2: + if "prod" in sys.argv[1]: + attrs = {} + +dynamodb = boto3.client('dynamodb',**attrs) +table_name = 'cruddur-messages' + +message_group_uuid = "5ae290ed-55d1-47a0-bc6d-fe2bc2700399" + +# define the query parameters +current_year = datetime.datetime.now().year +query_params = { + 'TableName': table_name, + 'ScanIndexForward': False, + 'Limit': 20, + 'ReturnConsumedCapacity': 'TOTAL', + 'KeyConditionExpression': 'pk = :pk AND begins_with(sk,:year)', + #'KeyConditionExpression': 'pk = :pk AND sk BETWEEN :start_date AND :end_date', + 'ExpressionAttributeValues': { + ':year': {'S': str(current_year) }, + #":start_date": { "S": "2023-03-01T00:00:00.000000+00:00" }, + #":end_date": { "S": "2023-03-19T23:59:59.999999+00:00" }, + ':pk': {'S': f"MSG#{message_group_uuid}"} + } +} + + +# query the table +response = dynamodb.query(**query_params) + +# print the items returned by the query +print(json.dumps(response, sort_keys=True, indent=2)) + +# print the consumed capacity +print(json.dumps(response['ConsumedCapacity'], sort_keys=True, indent=2)) + +items = response['Items'] +items.reverse() + +for item in items: + sender_handle = item['user_handle']['S'] + message = item['message']['S'] + timestamp = item['sk']['S'] + dt_object = datetime.datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S.%f%z') + formatted_datetime = dt_object.strftime('%Y-%m-%d %I:%M %p') + print(f'{sender_handle: <12}{formatted_datetime: <22}{message[:40]}...') \ No newline at end of file diff --git a/bin/ddb/patterns/list-conversations b/bin/ddb/patterns/list-conversations new file mode 100755 index 000000000..6737f26b8 --- /dev/null +++ b/bin/ddb/patterns/list-conversations @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 + +import boto3 +import sys +import json +import os +import datetime + +current_path = os.path.dirname(os.path.abspath(__file__)) +parent_path = os.path.abspath(os.path.join(current_path, '..', '..', '..')) +sys.path.append(parent_path) +from lib.db import db + +attrs = { + 'endpoint_url': 'http://localhost:8000' +} + +if len(sys.argv) == 2: + if "prod" in sys.argv[1]: + attrs = {} + +dynamodb = boto3.client('dynamodb',**attrs) +table_name = 'cruddur-messages' + +def get_my_user_uuid(): + sql = """ + SELECT + users.uuid + FROM users + WHERE + users.handle =%(handle)s + """ + uuid = db.query_value(sql,{ + 'handle': 'papamfall' + }) + return uuid + +my_user_uuid = get_my_user_uuid() +print(f"my-uuid: {my_user_uuid}") + +current_year = datetime.datetime.now().year +# define the query parameters +query_params = { + 'TableName': table_name, + 'KeyConditionExpression': 'pk = :pk AND begins_with(sk,:year)', + 'ScanIndexForward': False, + 'ExpressionAttributeValues': { + ':year': {'S': str(current_year) }, + ':pk': {'S': f"GRP#{my_user_uuid}"} + }, + 'ReturnConsumedCapacity': 'TOTAL' +} + +# query the table +response = dynamodb.query(**query_params) + +# print the items returned by the query +print(json.dumps(response, sort_keys=True, indent=2)) \ No newline at end of file diff --git a/bin/ddb/scan b/bin/ddb/scan new file mode 100755 index 000000000..59385f4c7 --- /dev/null +++ b/bin/ddb/scan @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 + +import boto3 +import sys +attrs = { + 'endpoint_url': 'http://localhost:8000' +} + +if len(sys.argv) == 2: + if "prod" in sys.argv[1]: + attrs = {} + +ddb = boto3.resource('dynamodb', **attrs) + +table_name = 'cruddur-messages' + + +table = ddb.Table(table_name) +response = table.scan() + +items = response['Items'] +for item in items: + print(item) \ No newline at end of file diff --git a/bin/ddb/schema-load b/bin/ddb/schema-load new file mode 100755 index 000000000..bb9668232 --- /dev/null +++ b/bin/ddb/schema-load @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 + +import boto3 +import sys + +attrs = { + 'endpoint_url': 'http://localhost:8000' +} + +if len(sys.argv) == 2: + if "prod" in sys.argv[1]: + attrs = {} + +dynamodb = boto3.client('dynamodb', **attrs) +table_name = 'cruddur-messages' + +# define the schema for the table +table_schema = { + 'AttributeDefinitions': [ + { + 'AttributeName': 'message_group_uuid', + 'AttributeType': 'S' + },{ + 'AttributeName': 'pk', + 'AttributeType': 'S' + },{ + 'AttributeName': 'sk', + 'AttributeType': 'S' + } + ], + 'KeySchema': [{ + 'AttributeName': 'pk', + 'KeyType': 'HASH' + },{ + 'AttributeName': 'sk', + 'KeyType': 'RANGE' + } + ], + 'BillingMode': 'PROVISIONED', + 'ProvisionedThroughput': { + 'ReadCapacityUnits': 5, + 'WriteCapacityUnits': 5 + }, + 'GlobalSecondaryIndexes':[{ + 'IndexName':'message-group-sk-index', + 'KeySchema':[{ + 'AttributeName': 'message_group_uuid', + 'KeyType': 'HASH' + },{ + 'AttributeName': 'sk', + 'KeyType': 'RANGE' + }], + 'Projection': { + 'ProjectionType': 'ALL' + }, + 'ProvisionedThroughput': { + 'ReadCapacityUnits': 5, + 'WriteCapacityUnits': 5 + }, + }] +} + +# create the table +response = dynamodb.create_table( + TableName=table_name, + **table_schema +) + +# print the response +print(response) \ No newline at end of file diff --git a/bin/ddb/seed b/bin/ddb/seed new file mode 100755 index 000000000..3464f422f --- /dev/null +++ b/bin/ddb/seed @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 + +import boto3 +import os +import sys +from datetime import datetime, timedelta, timezone +import uuid + +current_path = os.path.dirname(os.path.abspath(__file__)) +parent_path = os.path.abspath(os.path.join(current_path, '..', '..')) +sys.path.append(parent_path) +from lib.db import db + +attrs = { + 'endpoint_url': 'http://localhost:8000' +} +# unset endpoint url for use with production database +if len(sys.argv) == 2: + if "prod" in sys.argv[1]: + attrs = {} +ddb = boto3.client('dynamodb',**attrs) + +def get_user_uuids(): + sql = """ + SELECT + users.uuid, + users.display_name, + users.handle + FROM users + WHERE + users.handle IN( + %(my_handle)s, + %(other_handle)s + ) + """ + users = db.query_array_json(sql,{ + 'my_handle': 'papamfall', + 'other_handle': 'sokhnangom' + }) + my_user = next((item for item in users if item["handle"] == 'papamfall'), None) + other_user = next((item for item in users if item["handle"] == 'sokhnangom'), None) + results = { + 'my_user': my_user, + 'other_user': other_user + } + print('get_user_uuids') + print(results) + return results + +def create_message_group(client,message_group_uuid, my_user_uuid, last_message_at=None, message=None, other_user_uuid=None, other_user_display_name=None, other_user_handle=None): + table_name = 'cruddur-messages' + record = { + 'pk': {'S': f"GRP#{my_user_uuid}"}, + 'sk': {'S': last_message_at}, + 'message_group_uuid': {'S': message_group_uuid}, + 'message': {'S': message}, + 'user_uuid': {'S': other_user_uuid}, + 'user_display_name': {'S': other_user_display_name}, + 'user_handle': {'S': other_user_handle} + } + + response = client.put_item( + TableName=table_name, + Item=record + ) + print(response) + +def create_message(client,message_group_uuid, created_at, message, my_user_uuid, my_user_display_name, my_user_handle): + table_name = 'cruddur-messages' + record = { + 'pk': {'S': f"MSG#{message_group_uuid}"}, + 'sk': {'S': created_at }, + 'message_uuid': { 'S': str(uuid.uuid4()) }, + 'message': {'S': message}, + 'user_uuid': {'S': my_user_uuid}, + 'user_display_name': {'S': my_user_display_name}, + 'user_handle': {'S': my_user_handle} + } + # insert the record into the table + response = client.put_item( + TableName=table_name, + Item=record + ) + # print the response + print(response) + +message_group_uuid = "5ae290ed-55d1-47a0-bc6d-fe2bc2700399" +now = datetime.now(timezone.utc).astimezone() +users = get_user_uuids() + +create_message_group( + client=ddb, + message_group_uuid=message_group_uuid, + my_user_uuid=users['my_user']['uuid'], + other_user_uuid=users['other_user']['uuid'], + other_user_handle=users['other_user']['handle'], + other_user_display_name=users['other_user']['display_name'], + last_message_at=now.isoformat(), + message="this is a filler message" +) + +create_message_group( + client=ddb, + message_group_uuid=message_group_uuid, + my_user_uuid=users['other_user']['uuid'], + other_user_uuid=users['my_user']['uuid'], + other_user_handle=users['my_user']['handle'], + other_user_display_name=users['my_user']['display_name'], + last_message_at=now.isoformat(), + message="this is a filler message" +) + +conversation = """ +Person 1: Have you ever watched Babylon 5? It's one of my favorite TV shows! +Person 2: Yes, I have! I love it too. What's your favorite season? +Person 1: I think my favorite season has to be season 3. So many great episodes, like "Severed Dreams" and "War Without End." +Person 2: Yeah, season 3 was amazing! I also loved season 4, especially with the Shadow War heating up and the introduction of the White Star. +Person 1: Agreed, season 4 was really great as well. I was so glad they got to wrap up the storylines with the Shadows and the Vorlons in that season. +Person 2: Definitely. What about your favorite character? Mine is probably Londo Mollari. +Person 1: Londo is great! My favorite character is probably G'Kar. I loved his character development throughout the series. +Person 2: G'Kar was definitely a standout character. I also really liked Delenn's character arc and how she grew throughout the series. +Person 1: Delenn was amazing too, especially with her role in the Minbari Civil War and her relationship with Sheridan. Speaking of which, what did you think of the Sheridan character? +Person 2: I thought Sheridan was a great protagonist. He was a strong leader and had a lot of integrity. And his relationship with Delenn was so well-done. +Person 1: I totally agree! I also really liked the dynamic between Garibaldi and Bester. Those two had some great scenes together. +Person 2: Yes! Their interactions were always so intense and intriguing. And speaking of intense scenes, what did you think of the episode "Intersections in Real Time"? +Person 1: Oh man, that episode was intense. It was so well-done, but I could barely watch it. It was just too much. +Person 2: Yeah, it was definitely hard to watch. But it was also one of the best episodes of the series in my opinion. +Person 1: Absolutely. Babylon 5 had so many great episodes like that. Do you have a favorite standalone episode? +Person 2: Hmm, that's a tough one. I really loved "The Coming of Shadows" in season 2, but "A Voice in the Wilderness" in season 1 was also great. What about you? +Person 1: I think my favorite standalone episode might be "The Long Twilight Struggle" in season 2. It had some great moments with G'Kar and Londo. +Person 2: Yes, "The Long Twilight Struggle" was definitely a standout episode. Babylon 5 really had so many great episodes and moments throughout its run. +Person 1: Definitely. It's a shame it ended after only five seasons, but I'm glad we got the closure we did with the series finale. +Person 2: Yeah, the series finale was really well-done. It tied up a lot of loose ends and left us with a great sense of closure. +Person 1: It really did. Overall, Babylon 5 is just such a great show with fantastic characters, writing, and world-building. +Person 2: Agreed. It's one of my favorite sci-fi shows of all time and I'm always happy to revisit it. +Person 1: Same here. I think one of the things that makes Babylon 5 so special is its emphasis on politics and diplomacy. It's not just a show about space battles and aliens, but about the complex relationships between different species and their political maneuvering. +Person 2: Yes, that's definitely one of the show's strengths. And it's not just about big-picture politics, but also about personal relationships and the choices characters make. +Person 1: Exactly. I love how Babylon 5 explores themes of redemption, forgiveness, and sacrifice. Characters like G'Kar and Londo have such compelling arcs that are driven by their choices and actions. +Person 2: Yes, the character development in Babylon 5 is really top-notch. Even minor characters like Vir and Franklin get their moments to shine and grow over the course of the series. +Person 1: I couldn't agree more. And the way the show handles its themes is so nuanced and thought-provoking. For example, the idea of "the one" and how it's used by different characters in different ways. +Person 2: Yes, that's a really interesting theme to explore. And it's not just a one-dimensional concept, but something that's explored in different contexts and with different characters. +Person 1: And the show also does a great job of balancing humor and drama. There are so many funny moments in the show, but it never detracts from the serious themes and the high stakes. +Person 2: Absolutely. The humor is always organic and never feels forced. And the show isn't afraid to go dark when it needs to, like in "Intersections in Real Time" or the episode "Sleeping in Light." +Person 1: Yeah, those episodes are definitely tough to watch, but they're also some of the most powerful and memorable episodes of the series. And it's not just the writing that's great, but also the acting and the production values. +Person 2: Yes, the acting is fantastic across the board. From Bruce Boxleitner's performance as Sheridan to Peter Jurasik's portrayal of Londo, every actor brings their A-game. And the production design and special effects are really impressive for a TV show from the 90s. +Person 1: Definitely. Babylon 5 was really ahead of its time in terms of its visuals and special effects. And the fact that it was all done on a TV budget makes it even more impressive. +Person 2: Yeah, it's amazing what they were able to accomplish with the limited resources they had. It just goes to show how talented the people behind the show were. +Person 1: Agreed. It's no wonder that Babylon 5 has such a devoted fanbase, even all these years later. It's just such a well-crafted and timeless show. +Person 2: Absolutely. I'm glad we can still appreciate it and talk about it all these years later. It really is a show that stands the test of time. +Person 1: One thing I really appreciate about Babylon 5 is how it handles diversity and representation. It has a really diverse cast of characters from different species and backgrounds, and it doesn't shy away from exploring issues of prejudice and discrimination. +Person 2: Yes, that's a great point. The show was really ahead of its time in terms of its diverse cast and the way it tackled issues of race, gender, and sexuality. And it did so in a way that felt natural and integrated into the story. +Person 1: Definitely. It's great to see a show that's not afraid to tackle these issues head-on and address them in a thoughtful and nuanced way. And it's not just about representation, but also about exploring different cultures and ways of life. +Person 2: Yes, the show does a great job of world-building and creating distinct cultures for each of the species. And it's not just about their physical appearance, but also about their customs, beliefs, and values. +Person 1: Absolutely. It's one of the things that sets Babylon 5 apart from other sci-fi shows. The attention to detail and the thought that went into creating this universe is really impressive. +Person 2: And it's not just the aliens that are well-developed, but also the human characters. The show explores the different factions and political ideologies within EarthGov, as well as the different cultures and traditions on Earth. +Person 1: Yes, that's another great aspect of the show. It's not just about the conflicts between different species, but also about the internal struggles within humanity. And it's all tied together by the overarching plot of the Shadow War and the fate of the galaxy. +Person 2: Definitely. The show does a great job of balancing the episodic stories with the larger arc, so that every episode feels important and contributes to the overall narrative. +Person 1: And the show is also great at building up tension and suspense. The slow burn of the Shadow War and the mystery of the Vorlons and the Shadows kept me on the edge of my seat throughout the series. +Person 2: Yes, the show is really good at building up anticipation and delivering satisfying payoffs. Whether it's the resolution of a character arc or the climax of a season-long plotline, Babylon 5 always delivers. +Person 1: Agreed. It's just such a well-crafted and satisfying show, with so many memorable moments and characters. I'm really glad we got to talk about it today. +Person 2: Me too. It's always great to geek out about Babylon 5 with someone who appreciates it as much as I do! +Person 1: Yeah, it's always fun to discuss our favorite moments and characters from the show. And there are so many great moments to choose from! +Person 2: Definitely. I think one of the most memorable moments for me was the "goodbye" scene between G'Kar and Londo in the episode "Objects at Rest." It was such a poignant and emotional moment, and it really showed how far their characters had come. +Person 1: Yes, that was a really powerful scene. It was great to see these two former enemies come together and find common ground. And it was a great way to wrap up their character arcs. +Person 2: Another memorable moment for me was the speech that Sheridan gives in "Severed Dreams." It's such an iconic moment in the show, and it really encapsulates the themes of the series. +Person 1: Yes, that speech is definitely one of the highlights of the series. It's so well-written and well-delivered, and it really captures the sense of hope and defiance that the show is all about. +Person 2: And speaking of great speeches, what did you think of the "Ivanova is always right" speech from "Moments of Transition"? +Person 1: Oh man, that speech gives me chills every time I watch it. It's such a powerful moment for Ivanova, and it really shows her strength and determination as a leader. +Person 2: Yes, that speech is definitely a standout moment for Ivanova's character. And it's just one example of the great writing and character development in the show. +Person 1: Absolutely. It's a testament to the talent of the writers and actors that they were able to create such rich and complex characters with so much depth and nuance. +Person 2: And it's not just the main characters that are well-developed, but also the supporting characters like Marcus, Zack, and Lyta. They all have their own stories and struggles, and they all contribute to the larger narrative in meaningful ways. +Person 1: Definitely. Babylon 5 is just such a well-rounded and satisfying show in every way. It's no wonder that it's still beloved by fans all these years later. +Person 2: Agreed. It's a show that has stood the test of time, and it will always hold a special place in my heart as one of my favorite TV shows of all time. +Person 1: One of the most interesting ethical dilemmas presented in Babylon 5 is the treatment of the Narn by the Centauri. What do you think about that storyline? +Person 2: Yeah, it's definitely a difficult issue to grapple with. On the one hand, the Centauri were portrayed as the aggressors, and their treatment of the Narn was brutal and unjust. But on the other hand, the show also presented some nuance to the situation, with characters like Londo and Vir struggling with their own complicity in the conflict. +Person 1: Exactly. I think one of the strengths of the show is its willingness to explore complex ethical issues like this. It's not just about good guys versus bad guys, but about the shades of grey in between. +Person 2: Yeah, and it raises interesting questions about power and oppression. The Centauri had more advanced technology and military might than the Narn, which allowed them to dominate and subjugate the Narn people. But at the same time, there were also political and economic factors at play that contributed to the conflict. +Person 1: And it's not just about the actions of the Centauri government, but also about the actions of individual characters. Londo, for example, was initially portrayed as a somewhat sympathetic character, but as the series progressed, we saw how his choices and actions contributed to the suffering of the Narn people. +Person 2: Yes, and that raises interesting questions about personal responsibility and accountability. Can an individual be held responsible for the actions of their government or their society? And if so, to what extent? +Person 1: That's a really good point. And it's also interesting to consider the role of empathy and compassion in situations like this. Characters like G'Kar and Delenn showed compassion towards the Narn people and fought against their oppression, while others like Londo and Cartagia were more indifferent or even sadistic in their treatment of the Narn. +Person 2: Yeah, and that raises the question of whether empathy and compassion are innate traits, or whether they can be cultivated through education and exposure to different cultures and perspectives. +Person 1: Definitely. And it's also worth considering the role of forgiveness and reconciliation. The Narn and Centauri eventually came to a sort of reconciliation in the aftermath of the Shadow War, but it was a difficult and painful process that required a lot of sacrifice and forgiveness on both sides. +Person 2: Yes, and that raises the question of whether forgiveness is always possible or appropriate in situations of oppression and injustice. Can the victims of such oppression ever truly forgive their oppressors, or is that too much to ask? +Person 1: It's a tough question to answer. I think the show presents a hopeful message in the end, with characters like G'Kar and Londo finding a measure of redemption and reconciliation. But it's also clear that the scars of the conflict run deep and that healing takes time and effort. +Person 2: Yeah, that's a good point. Ultimately, I think the show's treatment of the Narn-Centauri conflict raises more questions than it answers, which is a testament to its complexity and nuance. It's a difficult issue to grapple with, but one that's worth exploring and discussing. +Person 1: Let's switch gears a bit and talk about the character of Natasha Alexander. What did you think about her role in the series? +Person 2: I thought Natasha Alexander was a really interesting character. She was a tough and competent security officer, but she also had a vulnerable side and a complicated past. +Person 1: Yeah, I agree. I think she added a lot of depth to the show and was a great foil to characters like Garibaldi and Zack. +Person 2: And I also appreciated the way the show handled her relationship with Garibaldi. It was clear that they had a history and a lot of unresolved tension, but the show never made it too melodramatic or over-the-top. +Person 1: That's a good point. I think the show did a good job of balancing the personal drama with the larger political and sci-fi elements. And it was refreshing to see a female character who was just as tough and competent as the male characters. +Person 2: Definitely. I think Natasha Alexander was a great example of a well-written and well-rounded female character. She wasn't just there to be eye candy or a love interest, but had her own story and agency. +Person 1: However, I did feel like the show could have done more with her character. She was introduced fairly late in the series, and didn't have as much screen time as some of the other characters. +Person 2: That's true. I think the show had a lot of characters to juggle, and sometimes that meant some characters got sidelined or didn't get as much development as they deserved. +Person 1: And I also thought that her storyline with Garibaldi could have been developed a bit more. They had a lot of history and tension between them, but it felt like it was resolved too quickly and neatly. +Person 2: I can see where you're coming from, but I also appreciated the way the show didn't drag out the drama unnecessarily. It was clear that they both had feelings for each other, but they also had to focus on their jobs and the larger conflicts at play. +Person 1: I can see that perspective as well. Overall, I think Natasha Alexander was a great addition to the show and added a lot of value to the series. It's a shame we didn't get to see more of her. +Person 2: Agreed. But at least the show was able to give her a satisfying arc and resolution in the end. And that's a testament to the show's strength as a whole. +Person 1: One thing that really stands out about Babylon 5 is the quality of the special effects. What did you think about the show's use of CGI and other visual effects? +Person 2: I thought the special effects in Babylon 5 were really impressive, especially for a show that aired in the 90s. The use of CGI to create the spaceships and other sci-fi elements was really innovative for its time. +Person 1: Yes, I was really blown away by the level of detail and realism in the effects. The ships looked so sleek and futuristic, and the space battles were really intense and exciting. +Person 2: And I also appreciated the way the show integrated the visual effects with the live-action footage. It never felt like the effects were taking over or overshadowing the characters or the story. +Person 1: Absolutely. The show had a great balance of practical effects and CGI, which helped to ground the sci-fi elements in a more tangible and realistic world. +Person 2: And it's also worth noting the way the show's use of visual effects evolved over the course of the series. The effects in the first season were a bit rough around the edges, but by the end of the series, they had really refined and perfected the look and feel of the show. +Person 1: Yes, I agree. And it's impressive how they were able to accomplish all of this on a TV budget. The fact that the show was able to create such a rich and immersive sci-fi universe with limited resources is a testament to the talent and creativity of the production team. +Person 2: Definitely. And it's one of the reasons why the show has aged so well. Even today, the visual effects still hold up and look impressive, which is a rarity for a show that's almost 30 years old. +Person 1: Agreed. And it's also worth noting the way the show's use of visual effects influenced other sci-fi shows that came after it. Babylon 5 really set the bar for what was possible in terms of sci-fi visuals on TV. +Person 2: Yes, it definitely had a big impact on the genre as a whole. And it's a great example of how innovative and groundbreaking sci-fi can be when it's done right. +Person 1: Another character I wanted to discuss is Zathras. What did you think of his character? +Person 2: Zathras was a really unique and memorable character. He was quirky and eccentric, but also had a lot of heart and sincerity. +Person 1: Yes, I thought he was a great addition to the show. He added some much-needed comic relief, but also had some important moments of character development. +Person 2: And I appreciated the way the show used him as a sort of plot device, with his knowledge of time and space being instrumental in the resolution of some of the show's major storylines. +Person 1: Definitely. It was a great way to integrate a seemingly minor character into the larger narrative. And it was also interesting to see the different versions of Zathras from different points in time. +Person 2: Yeah, that was a clever storytelling device that really added to the sci-fi elements of the show. And it was also a great showcase for actor Tim Choate, who played the character with so much charm and energy. +Person 1: I also thought that Zathras was a great example of the show's commitment to creating memorable and unique characters. Even characters that only appeared in a few episodes, like Zathras or Bester, were given distinct personalities and backstories. +Person 2: Yes, that's a good point. Babylon 5 was really great at creating a diverse and interesting cast of characters, with each one feeling like a fully-realized and distinct individual. +Person 1: And Zathras was just one example of that. He was a small but important part of the show's legacy, and he's still remembered fondly by fans today. +Person 2: Definitely. I think his character is a great example of the show's ability to balance humor and heart, and to create memorable and beloved characters that fans will cherish for years to come. +""" + + +lines = conversation.lstrip('\n').rstrip('\n').split('\n') +for i in range(len(lines)): + if lines[i].startswith('Person 1: '): + key = 'my_user' + message = lines[i].replace('Person 1: ', '') + elif lines[i].startswith('Person 2: '): + key = 'other_user' + message = lines[i].replace('Person 2: ', '') + else: + print(lines[i]) + raise 'invalid line' + + created_at = (now + timedelta(minutes=i)).isoformat() + create_message( + client=ddb, + message_group_uuid=message_group_uuid, + created_at=created_at, + message=message, + my_user_uuid=users[key]['uuid'], + my_user_display_name=users[key]['display_name'], + my_user_handle=users[key]['handle'] + ) \ No newline at end of file diff --git a/bin/ecr/login b/bin/ecr/login new file mode 100755 index 000000000..2cd176b31 --- /dev/null +++ b/bin/ecr/login @@ -0,0 +1,3 @@ +#! /usr/bin/bash + +aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin "$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com" \ No newline at end of file diff --git a/bin/ecs/list-tasks b/bin/ecs/list-tasks new file mode 100755 index 000000000..1fb8fa544 --- /dev/null +++ b/bin/ecs/list-tasks @@ -0,0 +1,4 @@ + +#! /usr/bin/bash + +aws ecs list-tasks --cluster cruddur --desired-status STOPPED \ No newline at end of file diff --git a/bin/flask/health-check b/bin/flask/health-check new file mode 100755 index 000000000..b644245b8 --- /dev/null +++ b/bin/flask/health-check @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 + +import urllib.request + +try: + response = urllib.request.urlopen('http://localhost:4567/api/health-check') + if response.getcode() == 200: + print("[OK] Flask server is running") + exit(0) + else: + print("[BAD] Flask server is not running") + exit(1) +except Exception as e: + print(e) + exit(1) \ No newline at end of file diff --git a/bin/frontend/build b/bin/frontend/build new file mode 100755 index 000000000..4be9935a9 --- /dev/null +++ b/bin/frontend/build @@ -0,0 +1,17 @@ +#! /usr/bin/bash + +ABS_PATH=$(readlink -f "$0") +FRONTEND_PATH=$(dirname $ABS_PATH) +BIN_PATH=$(dirname $FRONTEND_PATH) +PROJECT_PATH=$(dirname $BIN_PATH) +FRONTEND_REACT_JS_PATH="$PROJECT_PATH/frontend-react-js" + +docker build \ +--build-arg REACT_APP_BACKEND_URL="https://api.cruddursn.net" \ +--build-arg REACT_APP_AWS_PROJECT_REGION="$AWS_DEFAULT_REGION" \ +--build-arg REACT_APP_AWS_COGNITO_REGION="$AWS_DEFAULT_REGION" \ +--build-arg REACT_APP_AWS_USER_POOLS_ID="us-east-1_fjE3ZCqqX" \ +--build-arg REACT_APP_CLIENT_ID="6ff7h7vbdua1rvjctbi7lode3l" \ +-t frontend-react-js \ +-f "$FRONTEND_REACT_JS_PATH/Dockerfile.prod" \ +"$FRONTEND_REACT_JS_PATH/." \ No newline at end of file diff --git a/bin/frontend/connect b/bin/frontend/connect new file mode 100755 index 000000000..9f4f2de80 --- /dev/null +++ b/bin/frontend/connect @@ -0,0 +1,19 @@ +#! /usr/bin/bash +if [ -z "$1" ]; then + echo "No TASK_ID argument supplied eg ./bin/ecs/connect-to-frontend-react-js 99b2f8953616495e99545e5a6066fbb5d" + exit 1 +fi +TASK_ID=$1 + +CONTAINER_NAME=frontend-react-js + +echo "TASK ID : $TASK_ID" +echo "Container Name: $CONTAINER_NAME" + +aws ecs execute-command \ +--region $AWS_DEFAULT_REGION \ +--cluster cruddur \ +--task $TASK_ID \ +--container $CONTAINER_NAME \ +--command "/bin/sh" \ +--interactive \ No newline at end of file diff --git a/bin/frontend/deploy b/bin/frontend/deploy new file mode 100755 index 000000000..9c17b50a9 --- /dev/null +++ b/bin/frontend/deploy @@ -0,0 +1,19 @@ +#! /usr/bin/bash + +CLUSTER_NAME="cruddur" +SERVICE_NAME="frontend-react-js" +TASK_DEFINTION_FAMILY="frontend-react-js" + +LATEST_TASK_DEFINITION_ARN=$(aws ecs describe-task-definition \ +--task-definition $TASK_DEFINTION_FAMILY \ +--query 'taskDefinition.taskDefinitionArn' \ +--output text) + +echo "TASK DEF ARN:" +echo $LATEST_TASK_DEFINITION_ARN + +aws ecs update-service \ +--cluster $CLUSTER_NAME \ +--service $SERVICE_NAME \ +--task-definition $LATEST_TASK_DEFINITION_ARN \ +--force-new-deployment \ No newline at end of file diff --git a/bin/frontend/generate-env b/bin/frontend/generate-env new file mode 100755 index 000000000..87ec9bccb --- /dev/null +++ b/bin/frontend/generate-env @@ -0,0 +1,9 @@ +#!/usr/bin/env ruby + +require 'erb' + +template = File.read 'erb/frontend-react-js.env.erb' +content = ERB.new(template).result(binding) +filename = "frontend-react-js.env" +File.write(filename, content) +puts "... env variables generated for frontend-react-js application" \ No newline at end of file diff --git a/bin/frontend/push b/bin/frontend/push new file mode 100755 index 000000000..68156f04a --- /dev/null +++ b/bin/frontend/push @@ -0,0 +1,5 @@ +ECR_FRONTEND_REACT_URL="$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/frontend-react-js" +echo $ECR_FRONTEND_REACT_URL + +docker tag frontend-react-js:latest $ECR_FRONTEND_REACT_URL:latest +docker push $ECR_FRONTEND_REACT_URL:latest \ No newline at end of file diff --git a/bin/frontend/register b/bin/frontend/register new file mode 100755 index 000000000..07e1106c5 --- /dev/null +++ b/bin/frontend/register @@ -0,0 +1,12 @@ +#! /usr/bin/bash + +ABS_PATH=$(readlink -f "$0") +BACKEND_PATH=$(dirname $ABS_PATH) +BIN_PATH=$(dirname $BACKEND_PATH) +PROJECT_PATH=$(dirname $BIN_PATH) +TASK_DEF_PATH="$PROJECT_PATH/aws/task-definitions/frontend-react-js.json" + +echo $TASK_DEF_PATH + +aws ecs register-task-definition \ +--cli-input-json "file://$TASK_DEF_PATH" \ No newline at end of file diff --git a/bin/frontend/run b/bin/frontend/run new file mode 100755 index 000000000..ca9d9d28d --- /dev/null +++ b/bin/frontend/run @@ -0,0 +1,13 @@ +#! /usr/bin/bash + +ABS_PATH=$(readlink -f "$0") +BACKEND_PATH=$(dirname $ABS_PATH) +BIN_PATH=$(dirname $BACKEND_PATH) +PROJECT_PATH=$(dirname $BIN_PATH) +ENVFILE_PATH="$PROJECT_PATH/frontend-react-js.env" + +docker run --rm \ + --env-file $ENVFILE_PATH \ + --network cruddur-net \ + --publish 4567:4567 \ + -it frontend-react-js-prod \ No newline at end of file diff --git a/bin/generate/migration b/bin/generate/migration new file mode 100755 index 000000000..57b8983c0 --- /dev/null +++ b/bin/generate/migration @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +import time +import os +import sys + +if len(sys.argv) == 2: + name = sys.argv[1] +else: + print("pass a filename: eg. ./bin/generate/migration hello") + exit(0) + +timestamp = str(time.time()).replace(".","") + +filename = f"{timestamp}_{name}.py" + +klass = name.replace('_', ' ').title().replace(' ','') + +file_content = f""" +class {klass}Migration(Migration): + def migrate_sql(): + data = \"\"\" + \"\"\" + return data + def rollback_sql(): + data = \"\"\" + \"\"\" + return data + def migrate(): + this.query_commit(this.migrate_sql(),{{ + }}) + def rollback(): + this.query_commit(this.rollback_sql(),{{ + }}) +""" +file_content = file_content.lstrip('\n').rstrip('\n') + +current_path = os.path.dirname(os.path.abspath(__file__)) +file_path = os.path.abspath(os.path.join(current_path, '..', '..','backend-flask','db','migrations',filename)) +print(file_path) + +with open(file_path, 'w') as f: + f.write(file_content) \ No newline at end of file diff --git a/bin/lambda-layers/ruby-jwt b/bin/lambda-layers/ruby-jwt new file mode 100755 index 000000000..256c441aa --- /dev/null +++ b/bin/lambda-layers/ruby-jwt @@ -0,0 +1,14 @@ +#! /usr/bin/bash + +gem i jwt -Ni /tmp/lambda-layers/ruby-jwt/ruby/gems/2.7.0 +cd /tmp/lambda-layers/ruby-jwt + +zip -r lambda-layers . -x ".*" -x "*/.*" +zipinfo -t lambda-layers + +aws lambda publish-layer-version \ + --layer-name jwt \ + --description "Lambda Layer for JWT" \ + --license-info "MIT" \ + --zip-file fileb://lambda-layers.zip \ + --compatible-runtimes ruby2.7 \ No newline at end of file diff --git a/bin/prepare b/bin/prepare new file mode 100755 index 000000000..afd3c5cae --- /dev/null +++ b/bin/prepare @@ -0,0 +1,19 @@ +#! /usr/bin/bash +set -e # stop if it fails at any point + +CYAN='\033[1;36m' +NO_COLOR='\033[0m' +LABEL="bootstrap" +printf "${CYAN}====== ${LABEL}${NO_COLOR}\n" + +ABS_PATH=$(readlink -f "$0") +BIN_DIR=$(dirname $ABS_PATH) +DB_PATH="$BIN_DIR/db" +DDB_PATH="$BIN_DIR/ddb" + +source "$DB_PATH/create" +source "$DB_PATH/schema-load" +source "$DB_PATH/seed" +python "$DB_PATH/update_cognito_user_ids" +python "$DDB_PATH/schema-load" +python "$DDB_PATH/seed" diff --git a/bin/rds/update-sg-rule b/bin/rds/update-sg-rule new file mode 100755 index 000000000..1e6e45294 --- /dev/null +++ b/bin/rds/update-sg-rule @@ -0,0 +1,10 @@ +#! /usr/bin/bash + +CYAN='\033[1;36m' +NO_COLOR='\033[0m' +LABEL="rds-update-sg-rule" +printf "${CYAN}==== ${LABEL}${NO_COLOR}\n" + +aws ec2 modify-security-group-rules \ + --group-id $DB_SG_ID \ + --security-group-rules "SecurityGroupRuleId=$DB_SG_RULE_ID,SecurityGroupRule={Description=GITPOD,IpProtocol=tcp,FromPort=5432,ToPort=5432,CidrIpv4=$GITPOD_IP/32}" \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..0bb492702 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,71 @@ +version: "3.8" +services: + backend-flask: + env_file: + - backend-flask.env + build: ./backend-flask + ports: + - "4567:4567" + volumes: + - ./backend-flask:/backend-flask + networks: + - cruddur-net + frontend-react-js: + env_file: + - frontend-react-js.env + build: ./frontend-react-js + ports: + - "3000:3000" + networks: + - cruddur-net + volumes: + - ./frontend-react-js:/frontend-react-js + dynamodb-local: + # https://stackoverflow.com/questions/67533058/persist-local-dynamodb-data-in-volumes-lack-permission-unable-to-open-databa + # We needed to add user:root to get this working. + user: root + command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data" + image: "amazon/dynamodb-local:latest" + container_name: dynamodb-local + ports: + - "8000:8000" + networks: + - cruddur-net + volumes: + - "./docker/dynamodb:/home/dynamodblocal/data" + working_dir: /home/dynamodblocal + db: + image: postgres:13-alpine + restart: always + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=password + ports: + - '5432:5432' + networks: + - cruddur-net + volumes: + - db:/var/lib/postgresql/data + xray-daemon: + hostname: xray-daemon + image: "amazon/aws-xray-daemon" + environment: + AWS_ACCESS_KEY_ID: "${AWS_ACCESS_KEY_ID}" + AWS_SECRET_ACCESS_KEY: "${AWS_SECRET_ACCESS_KEY}" + AWS_REGION: "us-east-1" + command: + - "xray -o -b xray-daemon:2000" + ports: + - 2000:2000/udp + networks: + - cruddur-net +# the name flag is a hack to change the default prepend folder +# name when outputting the image names +networks: + cruddur-net: + driver: bridge + name: cruddur-net + +volumes: + db: + driver: local \ No newline at end of file diff --git a/docker/dynamodb/shared-local-instance.db b/docker/dynamodb/shared-local-instance.db new file mode 100644 index 000000000..27509b833 Binary files /dev/null and b/docker/dynamodb/shared-local-instance.db differ diff --git a/erb/backend-flask.env.erb b/erb/backend-flask.env.erb new file mode 100644 index 000000000..20da4ff75 --- /dev/null +++ b/erb/backend-flask.env.erb @@ -0,0 +1,15 @@ +AWS_ENDPOINT_URL=http://dynamodb-local:8000 +CONNECTION_URL=postgresql://postgres:password@db:5432/cruddur +FRONTEND_URL=https://3000-<%= ENV['GITPOD_WORKSPACE_ID'] %>.<%= ENV['GITPOD_WORKSPACE_CLUSTER_HOST'] %> +BACKEND_URL=https://4567-<%= ENV['GITPOD_WORKSPACE_ID'] %>.<%= ENV['GITPOD_WORKSPACE_CLUSTER_HOST'] %> +OTEL_SERVICE_NAME=backend-flask +OTEL_EXPORTER_OTLP_ENDPOINT=https://api.honeycomb.io +OTEL_EXPORTER_OTLP_HEADERS=x-honeycomb-team=<%= ENV['HONEYCOMB_API_KEY'] %> +AWS_XRAY_URL=*4567-<%= ENV['GITPOD_WORKSPACE_ID'] %>.<%= ENV['GITPOD_WORKSPACE_CLUSTER_HOST'] %>* +AWS_XRAY_DAEMON_ADDRESS=xray-daemon:2000 +AWS_DEFAULT_REGION=<%= ENV['AWS_DEFAULT_REGION'] %> +AWS_ACCESS_KEY_ID=<%= ENV['AWS_ACCESS_KEY_ID'] %> +AWS_SECRET_ACCESS_KEY=<%= ENV['AWS_SECRET_ACCESS_KEY'] %> +ROLLBAR_ACCESS_TOKEN=<%= ENV['ROLLBAR_ACCESS_TOKEN'] %> +AWS_COGNITO_USER_POOL_ID=<%= ENV['AWS_COGNITO_USER_POOL_ID'] %> +AWS_COGNITO_USER_POOL_CLIENT_ID=6ff7h7vbdua1rvjctbi7lode3l \ No newline at end of file diff --git a/erb/frontend-react-js.env.erb b/erb/frontend-react-js.env.erb new file mode 100644 index 000000000..77523940c --- /dev/null +++ b/erb/frontend-react-js.env.erb @@ -0,0 +1,7 @@ +REACT_APP_BACKEND_URL=https://4567-<%= ENV['GITPOD_WORKSPACE_ID'] %>.<%= ENV['GITPOD_WORKSPACE_CLUSTER_HOST'] %> +REACT_APP_AWS_PROJECT_REGION=<%= ENV['AWS_DEFAULT_REGION'] %> +REACT_APP_AWS_COGNITO_REGION=<%= ENV['AWS_DEFAULT_REGION'] %> +REACT_APP_AWS_USER_POOLS_ID=us-east-1_fjE3ZCqqX +REACT_APP_CLIENT_ID=6ff7h7vbdua1rvjctbi7lode3l +REACT_APP_API_GATEWAY_ENDPOINT_URL=https://jd9qaqq53e.execute-api.us-east-1.amazonaws.com +REACT_APP_FRONTEND_URL=https://3000-<%= ENV['GITPOD_WORKSPACE_ID'] %>.<%= ENV['GITPOD_WORKSPACE_CLUSTER_HOST'] %> \ No newline at end of file diff --git a/frontend-react-js/Dockerfile b/frontend-react-js/Dockerfile new file mode 100644 index 000000000..c98a624e5 --- /dev/null +++ b/frontend-react-js/Dockerfile @@ -0,0 +1,9 @@ +FROM node:16.18 + +ENV PORT=3000 + +COPY . /frontend-react-js +WORKDIR /frontend-react-js +RUN npm install +EXPOSE ${PORT} +CMD ["npm", "start"]ß \ No newline at end of file diff --git a/frontend-react-js/Dockerfile.prod b/frontend-react-js/Dockerfile.prod new file mode 100644 index 000000000..1cf0832ae --- /dev/null +++ b/frontend-react-js/Dockerfile.prod @@ -0,0 +1,28 @@ +# Base Image ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +FROM node:16.18 AS build + +ARG REACT_APP_BACKEND_URL +ARG REACT_APP_AWS_PROJECT_REGION +ARG REACT_APP_AWS_COGNITO_REGION +ARG REACT_APP_AWS_USER_POOLS_ID +ARG REACT_APP_CLIENT_ID + +ENV REACT_APP_BACKEND_URL=$REACT_APP_BACKEND_URL +ENV REACT_APP_AWS_PROJECT_REGION=$REACT_APP_AWS_PROJECT_REGION +ENV REACT_APP_AWS_COGNITO_REGION=$REACT_APP_AWS_COGNITO_REGION +ENV REACT_APP_AWS_USER_POOLS_ID=$REACT_APP_AWS_USER_POOLS_ID +ENV REACT_APP_CLIENT_ID=$REACT_APP_CLIENT_ID + +COPY . ./frontend-react-js +WORKDIR /frontend-react-js +RUN npm install +RUN npm run build + +# New Base Image ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +FROM nginx:1.23.3-alpine + +# --from build is coming from the Base Image +COPY --from=build /frontend-react-js/build /usr/share/nginx/html +COPY --from=build /frontend-react-js/nginx.conf /etc/nginx/nginx.conf + +EXPOSE 3000 \ No newline at end of file diff --git a/frontend-react-js/build/android-chrome-192x192.png b/frontend-react-js/build/android-chrome-192x192.png new file mode 100644 index 000000000..1a2ba3d1c Binary files /dev/null and b/frontend-react-js/build/android-chrome-192x192.png differ diff --git a/frontend-react-js/build/android-chrome-512x512.png b/frontend-react-js/build/android-chrome-512x512.png new file mode 100644 index 000000000..9f78ce97c Binary files /dev/null and b/frontend-react-js/build/android-chrome-512x512.png differ diff --git a/frontend-react-js/build/apple-touch-icon.png b/frontend-react-js/build/apple-touch-icon.png new file mode 100644 index 000000000..e6057d76b Binary files /dev/null and b/frontend-react-js/build/apple-touch-icon.png differ diff --git a/frontend-react-js/build/asset-manifest.json b/frontend-react-js/build/asset-manifest.json new file mode 100644 index 000000000..2564e0abf --- /dev/null +++ b/frontend-react-js/build/asset-manifest.json @@ -0,0 +1,27 @@ +{ + "files": { + "main.css": "/static/css/main.553110f2.css", + "main.js": "/static/js/main.09063207.js", + "static/js/787.cda612ba.chunk.js": "/static/js/787.cda612ba.chunk.js", + "static/media/logo.svg": "/static/media/logo.5753985f6358b14bd71dac0a2953098c.svg", + "static/media/heart.svg": "/static/media/heart.c31d61db7c2d096bd3219a00053eca01.svg", + "static/media/home.svg": "/static/media/home.4e94b7659059a75c271100122f0b2f23.svg", + "static/media/messages.svg": "/static/media/messages.9c2a92621ab30f0780dfbfcc7af26768.svg", + "static/media/profile.svg": "/static/media/profile.a195fff06191f3e91d26f8f44f495b0a.svg", + "static/media/more.svg": "/static/media/more.e13deeada4344cadc8f349f1575607fa.svg", + "index.html": "/index.html", + "static/media/bomb.svg": "/static/media/bomb.585e455838778938e175c4311b6a9284.svg", + "static/media/elipses.svg": "/static/media/elipses.e1e2c11435294de336f167d28e3387f2.svg", + "static/media/reply.svg": "/static/media/reply.e855b43c7d756ec100f0520b73df4964.svg", + "static/media/repost.svg": "/static/media/repost.3d8455b75243ae9c8440ddecb016ce72.svg", + "static/media/notifications.svg": "/static/media/notifications.30f93bb0a78cc8bb07e1a283bb8a3495.svg", + "static/media/share.svg": "/static/media/share.858c53d43c95f51422eb938be3942086.svg", + "main.553110f2.css.map": "/static/css/main.553110f2.css.map", + "main.09063207.js.map": "/static/js/main.09063207.js.map", + "787.cda612ba.chunk.js.map": "/static/js/787.cda612ba.chunk.js.map" + }, + "entrypoints": [ + "static/css/main.553110f2.css", + "static/js/main.09063207.js" + ] +} \ No newline at end of file diff --git a/frontend-react-js/build/browserconfig.xml b/frontend-react-js/build/browserconfig.xml new file mode 100644 index 000000000..b3930d0f0 --- /dev/null +++ b/frontend-react-js/build/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #da532c + + + diff --git a/frontend-react-js/build/favicon-16x16.png b/frontend-react-js/build/favicon-16x16.png new file mode 100644 index 000000000..29f87bf71 Binary files /dev/null and b/frontend-react-js/build/favicon-16x16.png differ diff --git a/frontend-react-js/build/favicon-32x32.png b/frontend-react-js/build/favicon-32x32.png new file mode 100644 index 000000000..c2276b3d7 Binary files /dev/null and b/frontend-react-js/build/favicon-32x32.png differ diff --git a/frontend-react-js/build/favicon.ico b/frontend-react-js/build/favicon.ico new file mode 100644 index 000000000..741879294 Binary files /dev/null and b/frontend-react-js/build/favicon.ico differ diff --git a/frontend-react-js/build/index.html b/frontend-react-js/build/index.html new file mode 100644 index 000000000..d37082139 --- /dev/null +++ b/frontend-react-js/build/index.html @@ -0,0 +1 @@ +Cruddur
\ No newline at end of file diff --git a/frontend-react-js/build/mstile-144x144.png b/frontend-react-js/build/mstile-144x144.png new file mode 100644 index 000000000..6d19dfdd5 Binary files /dev/null and b/frontend-react-js/build/mstile-144x144.png differ diff --git a/frontend-react-js/build/mstile-150x150.png b/frontend-react-js/build/mstile-150x150.png new file mode 100644 index 000000000..53d6b5c27 Binary files /dev/null and b/frontend-react-js/build/mstile-150x150.png differ diff --git a/frontend-react-js/build/mstile-310x150.png b/frontend-react-js/build/mstile-310x150.png new file mode 100644 index 000000000..6918c61cb Binary files /dev/null and b/frontend-react-js/build/mstile-310x150.png differ diff --git a/frontend-react-js/build/mstile-310x310.png b/frontend-react-js/build/mstile-310x310.png new file mode 100644 index 000000000..396fbf095 Binary files /dev/null and b/frontend-react-js/build/mstile-310x310.png differ diff --git a/frontend-react-js/build/mstile-70x70.png b/frontend-react-js/build/mstile-70x70.png new file mode 100644 index 000000000..746cc5e21 Binary files /dev/null and b/frontend-react-js/build/mstile-70x70.png differ diff --git a/frontend-react-js/build/robots.txt b/frontend-react-js/build/robots.txt new file mode 100644 index 000000000..e9e57dc4d --- /dev/null +++ b/frontend-react-js/build/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/frontend-react-js/build/safari-pinned-tab.svg b/frontend-react-js/build/safari-pinned-tab.svg new file mode 100644 index 000000000..df3252572 --- /dev/null +++ b/frontend-react-js/build/safari-pinned-tab.svg @@ -0,0 +1,82 @@ + + + + +Created by potrace 1.14, written by Peter Selinger 2001-2017 + + + + + diff --git a/frontend-react-js/build/site.webmanifest b/frontend-react-js/build/site.webmanifest new file mode 100644 index 000000000..b20abb7cb --- /dev/null +++ b/frontend-react-js/build/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/frontend-react-js/build/static/css/main.553110f2.css b/frontend-react-js/build/static/css/main.553110f2.css new file mode 100644 index 000000000..b2a794f40 --- /dev/null +++ b/frontend-react-js/build/static/css/main.553110f2.css @@ -0,0 +1,2 @@ +*{box-sizing:border-box}body,html{background:#0e031c;height:100%;width:100%}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;margin:0}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}article,main{height:100%;width:100%}.content{background:#1e25e753;height:100%;width:600px}nav{flex-shrink:1;padding-right:24px;padding-top:16px;width:280px}nav .logo{fill:#9500ff;height:48px;width:48px}nav a.primary{align-items:center;color:hsla(0,0%,100%,.5);display:flex;font-size:20px;gap:24px;margin:8px 0;padding:12px 0;text-decoration:none}nav a.primary:hover{color:#fff}nav a.primary .icon{fill:hsla(0,0%,100%,.5);height:24px;width:24px}nav a.primary:hover .icon{fill:#fff}nav a.primary.active{color:#9500ff!important;font-weight:600}nav a.primary.active svg{fill:#9500ff!important}nav button.post{background:#9500ff;border:none;border-radius:999px;color:#fff;display:block;font-size:20px;font-weight:600;margin-top:24px;padding:16px 0;text-align:center;text-decoration:none;width:100%}.profile-info-wrapper.popped .profile-dialog{display:block}.profile-info,.profile-info-wrapper{position:relative}.profile-info{align-items:center;border-radius:999px;display:flex;gap:12px;margin-top:12px;padding:12px;z-index:2}.profile-info:hover{background:hsla(0,0%,100%,.08)}.profile-info .profile-avatar{background:#9500ff;border-radius:999px;flex-shrink:0;height:40px;width:40px}.profile-desc{display:flex;flex-direction:column;flex-grow:1;gap:2px}.profile-info .profile-display-name{color:#fff;font-size:14px;font-weight:800}.profile-info .profile-username{color:hsla(0,0%,100%,.5);font-size:14px}.profile-info .icon{fill:hsla(0,0%,100%,.5);height:18px;margin-right:8px;width:18px}.profile-dialog{background:#000;border-radius:8px;bottom:70px;box-shadow:0 0 4px hsla(0,0%,100%,.6);display:none;min-height:60px;min-width:240px;overflow:hidden;position:absolute;z-index:3}.profile-dialog button{background:none;border:none;color:#fff;cursor:pointer;font-size:16px;font-weight:600;outline:none;padding:24px;text-align:left;width:100%}.profile-dialog button:hover{background:hsla(0,0%,100%,.1)}section{flex-shrink:1;padding:16px;width:380px}section footer{display:flex;gap:16px;padding:12px}section footer a{color:hsla(0,0%,100%,.2);font-size:14px;text-decoration:none}section footer a:hover{color:#fff}.search_field input[type=text]{background:rgba(149,0,255,.1);border:1px solid rgba(149,0,255,.1);border-radius:8px;color:#fff;font-size:16px;outline:none;padding:16px;width:100%}.search_field input[type=text]:focus{border:1px solid #9500ff}.trendings{background:#000;border-radius:8px;display:flex;flex-direction:column;margin-top:24px}.trendings .trendings-title{border-bottom:1px solid #1f2431;color:#fff;font-size:16px;padding:16px}.trendings a{color:#fff;font-size:16px;padding:16px;text-decoration:none}.trendings a span{display:block}.trendings a span.count{color:hsla(0,0%,100%,.5);font-size:13px}.suggested_users{background:#000;border-radius:8px;display:flex;flex-direction:column;margin-top:24px}.suggested_users_title{border-bottom:1px solid #1f2431;color:#fff;font-size:16px;padding:16px}.suggested_users .user{color:#fff;display:flex;font-size:16px;padding:16px;text-decoration:none}.suggested_users .user span{display:block}.suggested_users .user span.handle{color:hsla(0,0%,100%,.5);font-size:13px}.suggested_users .avatar{background:#9500ff;border-radius:999px;flex-shrink:0;height:36px;margin-right:12px;width:36px}.join{background:#000;border-radius:8px;display:flex;flex-direction:column;margin-top:24px}.join .join-title{border-bottom:1px solid #1f2431;color:#fff;font-size:16px;padding:16px}.join p{color:#fff;font-size:16px;text-align:center}.join-content{padding:12px 24px 24px}.join-content a.action{background:#9500ff;border:none;border-radius:999px;color:#fff;font-size:20px;font-weight:600;margin-top:24px;padding:16px 0;width:100%}.join-content a.action,.join-content a.subaction{display:block;text-align:center;text-decoration:none}.join-content a.subaction{color:#9500ff;font-size:16px;margin-top:12px;padding:12px 0}.activity_feed{flex-shrink:0;overflow:hidden}.activity_feed,.activity_feed_collection{display:flex;flex-direction:column}.activity_feed_heading{border-bottom:1px solid #3a3f4e;padding:16px 16px 48px}.activity_feed_heading .title{color:#fff;font-size:20px;font-weight:800}.activity_item{border-bottom:1px solid #1f2431;display:flex;flex-direction:column;overflow:hidden;padding:16px}.activity_item:last-child{border-bottom:none}.activity_item .activity_actions{align-items:flex-end;display:flex;padding-left:60px;padding-top:16px}.activity_item .activity_actions .action{border-radius:999px;cursor:pointer;display:flex;flex-direction:row;flex-grow:1;gap:12px;padding:8px}.activity_item .activity_actions .action .counter{color:hsla(0,0%,100%,.8);font-weight:800}.activity_item .activity_actions .icon{fill:#fff;height:18px;width:18px}.activity_item .activity_actions .action:hover{background:rgba(149,0,255,.2)}.activity_item .activity_actions .action:hover .icon{fill:#9500ff}.activity_content_wrap{align-items:start;display:flex;flex-direction:row}.activity_content{flex-grow:1;padding-left:12px}.activity_content_wrap .activity_avatar{background:#9500ff;border-radius:999px;flex-shrink:0;height:60px;width:60px}.activity_content .activity_meta{display:flex;justify-content:space-between}.activity_content a.activity_identity{flex-grow:1;font-size:16px;text-decoration:none}.activity_content a.activity_identity .display_name{color:#fff;font-weight:800}.activity_content a.activity_identity .handle{color:hsla(0,0%,100%,.5)}.activity_content .activity_times{align-items:flex-end;display:flex;flex-direction:column}.activity_content .activity_times .created_at{color:#484d5f;font-size:14px}.activity_content .activity_times .expires_at{color:#9500ff;display:flex;font-size:14px;gap:2px}.activity_content .activity_times .expires_at svg.icon{fill:#9500ff;height:14px;width:14px}.activity_content .message{color:#fff;font-size:16px;margin-top:8px}form.activity_form{display:flex;flex-direction:column;padding:16px}form.activity_form textarea{background:rgba(149,0,255,.1);border:none;border:1px solid rgba(149,0,255,.1);border-radius:4px;color:#fff;display:block;font-family:Arial,Helvetica,sans-serif;font-size:16px;height:140px;outline:none;padding:16px;resize:none;width:100%}form.activity_form textarea:focus{border:1px solid #9500ff}form.activity_form .submit{align-items:center;display:flex;flex-direction:row;font-weight:600;justify-content:flex-end;margin-top:12px}form.activity_form button[type=submit]{background:#9500ff;border:none;border-radius:4px;border-bottom-right-radius:0;border-top-right-radius:0;color:#fff;font-size:16px;font-weight:800;height:38px;margin-left:12px;outline:none;padding:10px 20px}form.activity_form .count{color:hsla(0,0%,100%,.3)}form.activity_form .count.err{color:red}form.activity_form .expires_at_field{border-left:1px solid rgba(149,0,255,.7);display:flex;gap:12px;position:relative}form.activity_form .expires_at_field .icon{fill:#fff;height:14px;left:8px;position:absolute;top:12px;width:14px;z-index:2}form.activity_form select{-webkit-appearance:none;appearance:none;background-color:rgba(149,0,255,.8);background-image:url("data:image/svg+xml;utf8,");background-position-x:100%;background-position-y:50%;background-repeat:no-repeat;border:none;border-radius:4px;border-bottom-left-radius:0;border-top-left-radius:0;color:#fff;display:block;font-size:16px;font-weight:600;height:38px;outline:none;padding:10px 8px 10px 28px;position:relative;width:100%;width:120px;z-index:1}.popup_form_wrap{align-items:center;background:hsla(0,0%,100%,.1);display:flex;flex-direction:column;height:100%;justify-content:flex-start;left:0;padding-top:48px;position:fixed;top:0;width:100%}.popup_form{background:#000;border-radius:16px;box-shadow:0 0 6px rgba(190,9,190,.6);width:600px}form.replies_form{display:flex;flex-direction:column;padding:16px}.activity_wrap{padding:16px}form.replies_form textarea{background:rgba(149,0,255,.1);border:none;border:1px solid rgba(149,0,255,.1);border-radius:4px;color:#fff;display:block;font-family:Arial,Helvetica,sans-serif;font-size:16px;height:140px;outline:none;padding:16px;resize:none;width:100%}form.replies_form textarea:focus{border:1px solid #9500ff}form.replies_form .submit{align-items:center;display:flex;flex-direction:row;font-weight:600;justify-content:flex-end;margin-top:12px}form.replies_form button[type=submit]{background:#9500ff;border:none;border-radius:4px;border-bottom-right-radius:0;border-top-right-radius:0;color:#fff;font-size:16px;font-weight:800;height:38px;margin-left:12px;outline:none;padding:10px 20px}form.replies_form .count{color:hsla(0,0%,100%,.3)}form.replies_form .count.err{color:red}form.replies_form .expires_at_field{border-left:1px solid rgba(149,0,255,.7);display:flex;gap:12px;position:relative}form.replies_form .expires_at_field .icon{fill:#fff;height:14px;left:8px;position:absolute;top:12px;width:14px;z-index:2}article{display:flex;flex-direction:row;justify-content:center}article.signup-article .signup-info{flex-grow:1;padding-left:12px}article.signup-article .signup-info .logo{fill:#2021246c;height:140px;width:140px}article.signup-article{background:#8819f5;display:flex;height:100%;justify-content:flex-start;width:100%}article.signup-article .signup-wrapper{background:#202124;display:flex;flex-direction:column;padding-top:120px;width:760px}article.signup-article form{flex-grow:1;padding:0 120px}article.signup-article label{color:hsla(0,0%,100%,.6);display:block;padding:4px 0}article.signup-article input[type=password],article.signup-article input[type=text]{background:#1f1f1f;border:1px solid #555;border-radius:4px;color:#fff;display:block;font-family:Arial,Helvetica,sans-serif;font-size:16px;outline:none;padding:16px;resize:none;width:100%}.field.email,.field.username{margin-top:16px}article.signup-article input[type=password]:focus,article.signup-article input[type=text]:focus{border:1px solid #9500ff}article.signup-article .submit{align-items:center;display:flex;flex-direction:row;justify-content:space-between;margin-top:24px}article.signup-article h2{color:#fff;text-align:center}article.signup-article button[type=submit]{background:#9500ff;border:none;border-radius:6px;color:#fff;font-size:16px;font-weight:800;outline:none;padding:18px 28px}.already-have-an-account{background:#252525;color:hsla(0,0%,100%,.6);display:flex;gap:8px;justify-content:center;padding:24px}.already-have-an-account a{color:#9500ff;font-weight:600;text-decoration:none}.errors{background:rgba(255,0,0,.3);border-radius:8px;color:#fff;font-size:14px;margin-top:16px;padding:16px}article.signin-article .signin-info{flex-grow:1;padding-left:12px}article.signin-article .signin-info .logo{fill:#2021246c;height:140px;width:140px}article.signin-article{background:#8819f5;display:flex;height:100%;justify-content:flex-start;width:100%}article.signin-article .signin-wrapper{background:#202124;display:flex;flex-direction:column;padding-top:120px;width:760px}article.signin-article form{flex-grow:1;padding:0 120px}article.signin-article label{color:hsla(0,0%,100%,.6);display:block;padding:4px 0}article.signin-article input[type=password],article.signin-article input[type=text]{background:#1f1f1f;border:1px solid #555;border-radius:4px;color:#fff;display:block;font-family:Arial,Helvetica,sans-serif;font-size:16px;outline:none;padding:16px;resize:none;width:100%}article.signin-article input[type=password]:focus,article.signin-article input[type=text]:focus{border:1px solid #9500ff}article.signin-article .submit{align-items:center;display:flex;flex-direction:row;justify-content:space-between;margin-top:24px}article.signin-article h2{color:#fff;text-align:center}article.signin-article a.forgot-link{color:#fff;font-weight:600;text-decoration:none}article.signin-article button[type=submit]{background:#9500ff;border:none;border-radius:6px;color:#fff;font-size:16px;font-weight:800;outline:none;padding:18px 28px}.dont-have-an-account{background:#252525;color:hsla(0,0%,100%,.6);display:flex;gap:8px;justify-content:center;padding:24px}.dont-have-an-account a{color:#9500ff;font-weight:600;text-decoration:none}article.recover-article .recover-info .logo{fill:#2021246c;height:140px;width:140px}article.recover-article{align-items:center;background:#8819f5;display:flex;flex-direction:column;height:100%;justify-content:flex-start;width:100%}article.recover-article .recover-wrapper{background:#202124;border-radius:12px;display:flex;flex-direction:column;width:560px}article.recover-article form{flex-grow:1;padding:48px}article.recover-article label{color:hsla(0,0%,100%,.6);display:block;padding:4px 0}article.recover-article input[type=password],article.recover-article input[type=text]{background:#1f1f1f;border:1px solid #555;border-radius:4px;color:#fff;display:block;font-family:Arial,Helvetica,sans-serif;font-size:16px;outline:none;padding:16px;resize:none;width:100%}.field.password{margin-top:16px}article.recover-article input[type=password]:focus,article.recover-article input[type=text]:focus{border:1px solid #9500ff}article.recover-article .submit{align-items:center;display:flex;flex-direction:row;justify-content:space-between;margin-top:24px}article.recover-article h2{color:#fff;text-align:center}article.recover-article button[type=submit]{background:#9500ff;border:none;border-radius:6px;color:#fff;font-size:16px;font-weight:800;outline:none;padding:18px 28px}section.message_groups{background:#000;border-right:1px solid #3a3f4e;padding:0}.message_group_feed_heading{border-bottom:1px solid #3a3f4e;padding:16px 16px 48px}.message_group_feed_heading .title{color:#fff;font-size:20px;font-weight:800}.message_group_feed{display:flex;flex-direction:column;height:100%}.message_group_feed_collection{flex-grow:1;overflow:auto}.message_group_item{align-items:start;cursor:pointer;display:flex;overflow:hidden;padding:16px;text-decoration:none}.message_group_item.active,.message_group_item:hover{background:hsla(0,0%,100%,.08)}.message_group_item:last-child{border-bottom:none}.message_group_item .message_content{flex-grow:1;padding-left:12px}.message_group_item .message_group_avatar{background:#9500ff;border-radius:999px;flex-shrink:0;height:50px;margin-right:8px;width:50px}.message_group_item .message_group_meta{display:flex;justify-content:space-between}.message_group_item .message_group_identity{flex-grow:1;font-size:16px;text-decoration:none}.message_group_item .message_group_identity .display_name{color:#fff;font-weight:800}.message_group_item .message_group_identity .handle{color:hsla(0,0%,100%,.5)}.message_group_item .created_at{color:#484d5f;font-size:14px}.message_group_item .message{color:#fff;font-size:16px;margin-top:8px}.messages.content{display:flex;flex-direction:column}.message_feed{flex-grow:1;overflow:auto}.message_item{align-items:start;border-bottom:1px solid #1f2431;cursor:pointer;display:flex;overflow:hidden;padding:16px;text-decoration:none}.message_item:hover{background:hsla(0,0%,100%,.08)}.message_item:last-child{border-bottom:none}.message_item .message_content{flex-grow:1;padding-left:12px}.message_item .message_avatar{background:#9500ff;border-radius:999px;flex-shrink:0;height:50px;margin-right:8px;width:50px}.message_item .message_meta{display:flex;justify-content:space-between}.message_item .message_identity{flex-grow:1;font-size:16px;text-decoration:none}.message_item .message_identity .display_name{color:#fff;font-weight:800}.message_item .message_identity .handle{color:hsla(0,0%,100%,.5)}.message_item .created_at{color:#484d5f;font-size:14px}.message_item .message{color:#fff;font-size:16px;margin-top:8px}form.message_form{display:flex;flex-direction:column;padding:16px}form.message_form textarea{background:rgba(149,0,255,.1);border:none;border:1px solid rgba(149,0,255,.1);border-radius:4px;color:#fff;display:block;font-family:Arial,Helvetica,sans-serif;font-size:16px;height:140px;outline:none;padding:16px;resize:none;width:100%}form.message_form textarea:focus{border:1px solid #9500ff}form.message_form .submit{align-items:center;display:flex;flex-direction:row;font-weight:600;justify-content:flex-end;margin-top:12px}form.message_form button[type=submit]{background:#9500ff;border:none;border-radius:4px;color:#fff;font-size:16px;font-weight:800;height:38px;margin-left:12px;outline:none;padding:10px 20px}form.message_form .count{color:hsla(0,0%,100%,.3)}form.message_form .count.err{color:red}article.confirm-article .recover-info .logo{fill:#2021246c;height:140px;width:140px}article.confirm-article{align-items:center;background:#8819f5;display:flex;flex-direction:column;height:100%;justify-content:flex-start;width:100%}article.confirm-article .recover-wrapper{background:#202124;border-radius:12px;display:flex;flex-direction:column;width:560px}article.confirm-article form{flex-grow:1;padding:48px}article.confirm-article label{color:hsla(0,0%,100%,.6);display:block;padding:4px 0}article.confirm-article input[type=password],article.confirm-article input[type=text]{background:#1f1f1f;border:1px solid #555;border-radius:4px;color:#fff;display:block;font-family:Arial,Helvetica,sans-serif;font-size:16px;outline:none;padding:16px;resize:none;width:100%}.field.code{margin-top:16px}article.confirm-article input[type=password]:focus,article.confirm-article input[type=text]:focus{border:1px solid #9500ff}article.confirm-article .submit{align-items:center;display:flex;flex-direction:row;justify-content:space-between;margin-top:24px}article.confirm-article h2{color:#fff;margin-top:0;text-align:center}article.confirm-article button[type=submit]{background:#9500ff}article.confirm-article button.resend,article.confirm-article button[type=submit]{border:none;border-radius:6px;color:#fff;font-size:16px;font-weight:800;outline:none;padding:18px 28px}article.confirm-article button.resend{background:#562397;cursor:pointer;margin-top:24px}article.confirm-article button.resend:hover{background:#451782}article.confirm-article .sent-message{color:rgba(0,0,0,.7);font-size:18px;font-weight:600;margin-top:24px} +/*# sourceMappingURL=main.553110f2.css.map*/ \ No newline at end of file diff --git a/frontend-react-js/build/static/css/main.553110f2.css.map b/frontend-react-js/build/static/css/main.553110f2.css.map new file mode 100644 index 000000000..dfe7640af --- /dev/null +++ b/frontend-react-js/build/static/css/main.553110f2.css.map @@ -0,0 +1 @@ +{"version":3,"file":"static/css/main.553110f2.css","mappings":"AAAA,EAAI,qBAAwB,CAC5B,UAGE,kBAAwB,CAFxB,WAAY,CACZ,UAEF,CAEA,KAKE,kCAAmC,CACnC,iCAAkC,CAJlC,mIAEY,CAHZ,QAMF,CAEA,KACE,uEAEF,CCnBA,aACE,WAAY,CACZ,UACF,CAEA,SAGE,oBAAqB,CADrB,WAAY,CADZ,WAGF,CCTA,IACE,aAAc,CAGd,kBAAmB,CADnB,gBAAiB,CADjB,WAGF,CACA,UAGE,YAAoB,CADpB,WAAY,CADZ,UAGF,CAEA,cAGE,kBAAmB,CACnB,wBAA2B,CAH3B,YAAa,CAIb,cAAe,CAHf,QAAS,CAMT,YAAa,CADb,cAAe,CADf,oBAGF,CACA,oBACE,UACF,CACA,oBACE,uBAA0B,CAE1B,WAAY,CADZ,UAEF,CAEA,0BACE,SACF,CAEA,qBACE,uBAAgC,CAChC,eACF,CACA,yBACE,sBACF,CAEA,gBAEE,kBAA6B,CAE7B,WAAY,CAEZ,mBAAoB,CAHpB,UAAW,CAFX,aAAc,CAOd,cAAe,CAEf,eAAgB,CAEhB,eAAgB,CAHhB,cAAiB,CAEjB,iBAAkB,CANlB,oBAAqB,CAErB,UAMF,CCxDA,6CACE,aACF,CAMA,oCAHE,iBAYF,CATA,cAME,kBAAmB,CAHnB,mBAAoB,CAFpB,YAAa,CACb,QAAS,CAGT,eAAgB,CADhB,YAAa,CAIb,SACF,CAEA,oBACE,8BACF,CAEA,8BACE,kBAA0B,CAG1B,mBAAoB,CACpB,aAAc,CAHd,WAAY,CACZ,UAGF,CAEA,cAEE,YAAa,CACb,qBAAsB,CAFtB,WAAY,CAGZ,OACF,CAEA,oCAEE,UAAW,CACX,cAAe,CAFf,eAGF,CAEA,gCACE,wBAA2B,CAC3B,cACF,CAGA,oBACE,uBAA0B,CAE1B,WAAY,CACZ,gBAAgB,CAFhB,UAGF,CAEA,gBACE,eAAgB,CAEhB,iBAAkB,CAKlB,WAAY,CANZ,qCAA6C,CAQ7C,YAAa,CANb,eAAgB,CAGhB,eAAgB,CAEhB,eAAgB,CAHhB,iBAAkB,CADlB,SAMF,CAEA,uBAQE,eAAgB,CADhB,WAAY,CANZ,UAAW,CAQX,cAAe,CAPf,cAAe,CACf,eAAgB,CAOhB,YAAa,CANb,YAAa,CAEb,eAAgB,CADhB,UAMF,CACA,6BACE,6BACF,CCpFA,QACE,aAAc,CAEd,YAAa,CADb,WAEF,CAEA,eACE,YAAa,CACb,QAAS,CACT,YACF,CAEA,iBAEE,wBAA4B,CAC5B,cAAe,CAFf,oBAGF,CAEA,uBACE,UACF,CCpBA,+BAEE,6BAA+B,CAD/B,mCAAqC,CAIrC,iBAAkB,CAGlB,UAAW,CAJX,cAAe,CAGf,YAAa,CAJb,YAAa,CAGb,UAGF,CACA,qCACE,wBACF,CCZA,WAGE,eAAgB,CAChB,iBAAkB,CAHlB,YAAa,CACb,qBAAsB,CAGtB,eACF,CAEA,4BAIE,+BAAsC,CAFtC,UAAW,CADX,cAAe,CAEf,YAEF,CAEA,aAGE,UAAW,CADX,cAAe,CAEf,YAAa,CAHb,oBAIF,CAEA,kBACE,aACF,CAEA,wBAEE,wBAA2B,CAD3B,cAEF,CC7BA,iBAGE,eAAgB,CAChB,iBAAkB,CAHlB,YAAa,CACb,qBAAsB,CAGtB,eACF,CAEA,uBAIE,+BAAsC,CAFtC,UAAW,CADX,cAAe,CAEf,YAEF,CAEA,uBAGE,UAAW,CAEX,YAAa,CAHb,cAAe,CAEf,YAAa,CAHb,oBAKF,CAEA,4BACE,aACF,CAEA,mCAEE,wBAA2B,CAD3B,cAEF,CAEA,yBACE,kBAA0B,CAG1B,mBAAoB,CACpB,aAAc,CAHd,WAAY,CAIZ,iBAAkB,CAHlB,UAIF,CCvCA,MAGE,eAAgB,CAChB,iBAAkB,CAHlB,YAAa,CACb,qBAAsB,CAGtB,eACF,CAEA,kBAIE,+BAAsC,CAFtC,UAAW,CADX,cAAe,CAEf,YAEF,CAEA,QACE,UAAW,CACX,cAAe,CACf,iBACF,CAEA,cAEE,sBACF,CAEA,uBAEE,kBAA6B,CAE7B,WAAY,CAEZ,mBAAoB,CAHpB,UAAW,CAKX,cAAe,CAEf,eAAgB,CAEhB,eAAgB,CAHhB,cAAiB,CAFjB,UAMF,CAEA,iDAdE,aAAc,CAUd,iBAAkB,CANlB,oBAkBF,CARA,0BACE,aAAwB,CAMxB,cAAe,CADf,eAAgB,CADhB,cAGF,CCjDA,eAIE,aAAc,CADd,eAEF,CAEA,yCANE,YAAa,CACb,qBAQF,CAEA,uBAGE,+BAAsC,CADtC,sBAEF,CAEA,8BACE,UAAW,CAEX,cAAe,CADf,eAEF,CCtBA,eAGE,+BAAsC,CAFtC,YAAa,CACb,qBAAsB,CAEtB,eAAgB,CAChB,YACF,CACA,0BACE,kBACF,CAEA,iCAEE,oBAAqB,CADrB,YAAa,CAGb,iBAAkB,CADlB,gBAEF,CAEA,yCAIE,mBAAoB,CAFpB,cAAe,CAGf,YAAa,CACb,kBAAmB,CALnB,WAAY,CAMZ,QAAQ,CAJR,WAKF,CAEA,kDACE,wBAA4B,CAC5B,eACF,CAEA,uCAGE,SAAU,CAFV,WAAY,CACZ,UAEF,CAEA,+CACE,6BACF,CAEA,qDACE,YACF,CC7CA,uBAGE,iBAAkB,CAFlB,YAAa,CACb,kBAEF,CAEA,kBACE,WAAY,CACZ,iBACF,CAEA,wCACE,kBAA0B,CAG1B,mBAAoB,CACpB,aAAc,CAHd,WAAY,CACZ,UAGF,CAEA,iCACE,YAAa,CACb,6BACF,CAEA,sCACE,WAAY,CAEZ,cAAe,CADf,oBAEF,CAEA,oDAEE,UAAW,CADX,eAEF,CAEA,8CACE,wBACF,CAEA,kCAGE,oBAAqB,CAFrB,YAAa,CACb,qBAEF,CAEA,8CACE,aAAoB,CACpB,cACF,CAEA,8CACE,aAAqB,CAErB,YAAa,CADb,cAAe,CAEf,OACF,CACA,uDACE,YAAoB,CAEpB,WAAY,CADZ,UAEF,CAEA,2BACE,UAAW,CACX,cAAe,CACf,cACF,CClEA,mBAEE,YAAa,CACb,qBAAsB,CAFtB,YAGF,CAEA,4BAaE,6BAA+B,CAT/B,WAAY,CAQZ,mCAAqC,CATrC,iBAAkB,CAWlB,UAAW,CARX,aAAc,CALd,sCAAyC,CACzC,cAAe,CAQf,YAAa,CAHb,YAAa,CAIb,YAAa,CAHb,WAAY,CACZ,UAMF,CAEA,kCACE,wBACF,CAEA,2BAIE,kBAAmB,CAHnB,YAAa,CACb,kBAAmB,CAInB,eAAgB,CAHhB,wBAAyB,CAEzB,eAEF,CAEA,uCAWE,kBAA6B,CAR7B,WAAY,CACZ,iBAAkB,CAElB,4BAA6B,CAD7B,yBAA0B,CAO1B,UAAW,CAHX,cAAe,CARf,eAAgB,CAMhB,WAAY,CAGZ,gBAAiB,CARjB,YAAa,CAMb,iBAKF,CAEA,0BACE,wBACF,CAEA,8BACE,SACF,CAEA,qCAIE,wCAA0C,CAH1C,YAAa,CACb,QAAS,CACT,iBAEF,CAEA,2CAIE,SAAU,CAEV,WAAY,CAHZ,QAAS,CAFT,iBAAkB,CAClB,QAAS,CAGT,UAAW,CAEX,SACF,CAEA,0BAcE,uBAAwB,CAExB,eAAgB,CAEhB,mCAAqC,CACrC,qPAAsN,CAEtN,0BAA2B,CAC3B,yBAA0B,CAF1B,2BAA4B,CAV5B,WAAY,CACZ,iBAAkB,CAElB,2BAA4B,CAD5B,wBAAyB,CAKzB,UAAW,CAVX,aAAc,CAFd,cAAe,CACf,eAAgB,CAFhB,WAAY,CAFZ,YAAa,CAOb,0BAAkB,CAclB,iBAAkB,CAtBlB,UAAW,CAEX,WAAY,CAqBZ,SACF,CCrGA,iBASE,kBAAmB,CAEnB,6BAAgC,CALhC,YAAa,CACb,qBAAsB,CALtB,WAAY,CAMZ,0BAA2B,CAH3B,MAAO,CAKP,gBAAiB,CATjB,cAAe,CAGf,KAAM,CADN,UASF,CAEA,YACE,eAAgB,CAEhB,kBAAmB,CADnB,qCAA8C,CAE9C,WACF,CAEA,kBAEE,YAAa,CACb,qBAAsB,CAFtB,YAGF,CAEA,eACE,YACF,CAEA,2BAaE,6BAA+B,CAT/B,WAAY,CAQZ,mCAAqC,CATrC,iBAAkB,CAWlB,UAAW,CARX,aAAc,CALd,sCAAyC,CACzC,cAAe,CAQf,YAAa,CAHb,YAAa,CAIb,YAAa,CAHb,WAAY,CACZ,UAMF,CAEA,iCACE,wBACF,CAEA,0BAIE,kBAAmB,CAHnB,YAAa,CACb,kBAAmB,CAInB,eAAgB,CAHhB,wBAAyB,CAEzB,eAEF,CAEA,sCAWE,kBAA6B,CAR7B,WAAY,CACZ,iBAAkB,CAElB,4BAA6B,CAD7B,yBAA0B,CAO1B,UAAW,CAHX,cAAe,CARf,eAAgB,CAMhB,WAAY,CAGZ,gBAAiB,CARjB,YAAa,CAMb,iBAKF,CAEA,yBACE,wBACF,CAEA,6BACE,SACF,CAEA,oCAIE,wCAA0C,CAH1C,YAAa,CACb,QAAS,CACT,iBAEF,CAEA,0CAIE,SAAU,CAEV,WAAY,CAHZ,QAAS,CAFT,iBAAkB,CAClB,QAAS,CAGT,UAAW,CAEX,SACF,CCnGA,QACE,YAAa,CACb,kBAAmB,CACnB,sBACF,CCJA,oCACE,WAAY,CACZ,iBACF,CAEA,0CAGE,cAAe,CADf,YAAa,CADb,WAGF,CAEA,uBAKE,kBAAmB,CAJnB,YAAa,CAEb,WAAY,CACZ,0BAA2B,CAF3B,UAIF,CAEA,uCACE,kBAAmB,CAEnB,YAAa,CAEb,qBAAsB,CADtB,iBAAkB,CAFlB,WAIF,CACA,4BAEE,WAAY,CADZ,eAEF,CAEA,6BAGE,wBAA4B,CAD5B,aAAc,CADd,aAGF,CAEA,oFAaE,kBAAmB,CADnB,qBAAyB,CARzB,iBAAkB,CAUlB,UAAW,CAPX,aAAc,CALd,sCAAyC,CACzC,cAAe,CAKf,YAAa,CAGb,YAAa,CAFb,WAAY,CACZ,UAKF,CAKA,6BACE,eACF,CAKA,gGAEE,wBACF,CAEA,+BAKE,kBAAmB,CAHnB,YAAa,CACb,kBAAmB,CACnB,6BAA8B,CAH9B,eAKF,CAEA,0BAEE,UAAW,CADX,iBAEF,CASA,2CAOE,kBAA6B,CAJ7B,WAAY,CACZ,iBAAkB,CAIlB,UAAW,CAFX,cAAe,CALf,eAAgB,CAChB,YAAa,CAGb,iBAIF,CAEA,yBACE,kBAAmB,CAEnB,wBAA4B,CAE5B,YAAa,CADb,OAAQ,CAER,sBAAuB,CAJvB,YAKF,CACA,2BACE,aAAwB,CAExB,eAAgB,CADhB,oBAEF,CAEA,QAGE,2BAA6B,CAD7B,iBAAkB,CAElB,UAAuB,CAEvB,cAAe,CADf,eAAgB,CAJhB,YAMF,CCzHA,oCACE,WAAY,CACZ,iBACF,CAEA,0CAGE,cAAe,CADf,YAAa,CADb,WAGF,CAEA,uBAKE,kBAAmB,CAJnB,YAAa,CAEb,WAAY,CACZ,0BAA2B,CAF3B,UAIF,CAEA,uCACE,kBAAmB,CAEnB,YAAa,CAEb,qBAAsB,CADtB,iBAAkB,CAFlB,WAIF,CACA,4BAEE,WAAY,CADZ,eAEF,CAEA,6BAGE,wBAA4B,CAD5B,aAAc,CADd,aAGF,CAEA,oFAaE,kBAAmB,CADnB,qBAAyB,CARzB,iBAAkB,CAUlB,UAAW,CAPX,aAAc,CALd,sCAAyC,CACzC,cAAe,CAKf,YAAa,CAGb,YAAa,CAFb,WAAY,CACZ,UAKF,CAMA,gGAEE,wBACF,CAEA,+BAKE,kBAAmB,CAHnB,YAAa,CACb,kBAAmB,CACnB,6BAA8B,CAH9B,eAKF,CAEA,0BAEE,UAAW,CADX,iBAEF,CAEA,qCACE,UAAW,CACX,eAAgB,CAChB,oBACF,CAGA,2CAOE,kBAA6B,CAJ7B,WAAY,CACZ,iBAAkB,CAIlB,UAAW,CAFX,cAAe,CALf,eAAgB,CAChB,YAAa,CAGb,iBAIF,CAEA,sBACE,kBAAmB,CAEnB,wBAA4B,CAE5B,YAAa,CADb,OAAQ,CAER,sBAAuB,CAJvB,YAKF,CACA,wBACE,aAAwB,CAExB,eAAgB,CADhB,oBAEF,CC1GA,4CAGE,cAAe,CADf,YAAa,CADb,WAGF,CAEA,wBAME,kBAAmB,CACnB,kBAAmB,CANnB,YAAa,CACb,qBAAsB,CAEtB,WAAY,CACZ,0BAA2B,CAF3B,UAKF,CAEA,yCACE,kBAAmB,CAEnB,kBAAmB,CACnB,YAAa,CACb,qBAAsB,CAHtB,WAIF,CACA,6BAEE,WAAY,CADZ,YAEF,CAEA,8BAGE,wBAA4B,CAD5B,aAAc,CADd,aAGF,CAEA,sFAaE,kBAAmB,CADnB,qBAAyB,CARzB,iBAAkB,CAUlB,UAAW,CAPX,aAAc,CALd,sCAAyC,CACzC,cAAe,CAKf,YAAa,CAGb,YAAa,CAFb,WAAY,CACZ,UAKF,CAEA,gBACE,eACF,CAEA,kGAEE,wBACF,CAEA,gCAKE,kBAAmB,CAHnB,YAAa,CACb,kBAAmB,CACnB,6BAA8B,CAH9B,eAKF,CAEA,2BAEE,UAAW,CADX,iBAEF,CAIA,4CAOE,kBAA6B,CAJ7B,WAAY,CACZ,iBAAkB,CAIlB,UAAW,CAFX,cAAe,CALf,eAAgB,CAChB,YAAa,CAGb,iBAIF,CCpFA,uBACE,eAAgB,CAChB,8BAAqC,CACrC,SACF,CAEA,4BAGE,+BAAsC,CADtC,sBAEF,CAEA,mCACE,UAAW,CAEX,cAAe,CADf,eAEF,CAEA,oBACE,YAAa,CACb,qBAAsB,CACtB,WACF,CAEA,+BACE,WAAY,CACZ,aACF,CC3BA,oBAEE,iBAAkB,CAGlB,cAAe,CAJf,YAAa,CAEb,eAAgB,CAChB,YAAa,CAEb,oBACF,CAKA,qDACA,8BACA,CAEA,+BACE,kBACF,CAEA,qCACE,WAAY,CACZ,iBACF,CAEA,0CACE,kBAA0B,CAG1B,mBAAoB,CACpB,aAAc,CAHd,WAAY,CAIZ,gBAAiB,CAHjB,UAIF,CAEA,wCACE,YAAa,CACb,6BACF,CAEA,4CACE,WAAY,CAEZ,cAAe,CADf,oBAEF,CAEA,0DAEE,UAAW,CADX,eAEF,CAEA,oDACE,wBACF,CAGA,gCACE,aAAoB,CACpB,cACF,CAEA,6BACE,UAAW,CACX,cAAe,CACf,cACF,CChEA,kBACE,YAAa,CACb,qBACF,CAEA,cACE,WAAY,CACZ,aACF,CCRA,cAEE,iBAAkB,CAElB,+BAAsC,CAEtC,cAAe,CALf,YAAa,CAEb,eAAgB,CAEhB,YAAa,CAEb,oBACF,CACA,oBACA,8BACA,CAEA,yBACE,kBACF,CAEA,+BACE,WAAY,CACZ,iBACF,CAEA,8BACE,kBAA0B,CAG1B,mBAAoB,CACpB,aAAc,CAHd,WAAY,CAIZ,gBAAiB,CAHjB,UAIF,CAEA,4BACE,YAAa,CACb,6BACF,CAEA,gCACE,WAAY,CAEZ,cAAe,CADf,oBAEF,CAEA,8CAEE,UAAW,CADX,eAEF,CAEA,wCACE,wBACF,CAGA,0BACE,aAAoB,CACpB,cACF,CAEA,uBACE,UAAW,CACX,cAAe,CACf,cACF,CC7DA,kBAEE,YAAa,CACb,qBAAsB,CAFtB,YAGF,CAEA,2BAaE,6BAA+B,CAT/B,WAAY,CAQZ,mCAAqC,CATrC,iBAAkB,CAWlB,UAAW,CARX,aAAc,CALd,sCAAyC,CACzC,cAAe,CAQf,YAAa,CAHb,YAAa,CAIb,YAAa,CAHb,WAAY,CACZ,UAMF,CAEA,iCACE,wBACF,CAEA,0BAIE,kBAAmB,CAHnB,YAAa,CACb,kBAAmB,CAInB,eAAgB,CAHhB,wBAAyB,CAEzB,eAEF,CAEA,sCAQE,kBAA6B,CAL7B,WAAY,CACZ,iBAAkB,CAKlB,UAAW,CAFX,cAAe,CANf,eAAgB,CAIhB,WAAY,CAKZ,gBAAiB,CARjB,YAAa,CAIb,iBAKF,CAEA,yBACE,wBACF,CAEA,6BACE,SACF,CCvDA,4CAGE,cAAe,CADf,YAAa,CADb,WAGF,CAEA,wBAME,kBAAmB,CACnB,kBAAmB,CANnB,YAAa,CACb,qBAAsB,CAEtB,WAAY,CACZ,0BAA2B,CAF3B,UAKF,CAEA,yCACE,kBAAmB,CAEnB,kBAAmB,CACnB,YAAa,CACb,qBAAsB,CAHtB,WAIF,CACA,6BAEE,WAAY,CADZ,YAEF,CAEA,8BAGE,wBAA4B,CAD5B,aAAc,CADd,aAGF,CAEA,sFAaE,kBAAmB,CADnB,qBAAyB,CARzB,iBAAkB,CAUlB,UAAW,CAPX,aAAc,CALd,sCAAyC,CACzC,cAAe,CAKf,YAAa,CAGb,YAAa,CAFb,WAAY,CACZ,UAKF,CAEA,YACE,eACF,CAEA,kGAEE,wBACF,CAEA,gCAKE,kBAAmB,CAHnB,YAAa,CACb,kBAAmB,CACnB,6BAA8B,CAH9B,eAKF,CAEA,2BAEE,UAAW,CACX,YAAa,CAFb,iBAGF,CAEA,4CAOE,kBAEF,CAEA,kFARE,WAAY,CACZ,iBAAkB,CAIlB,UAAW,CAFX,cAAe,CALf,eAAgB,CAChB,YAAa,CAGb,iBAiBF,CAXA,sCAOE,kBAAmB,CAGnB,cAAe,CADf,eAEF,CACA,4CACE,kBACF,CAEA,sCAIE,oBAAqB,CAFrB,cAAe,CACf,eAAgB,CAFhB,eAIF","sources":["index.css","App.css","components/DesktopNavigation.css","components/ProfileInfo.css","components/DesktopSidebar.css","components/Search.css","components/TrendItem.css","components/SuggestedUserItem.css","components/JoinSection.css","components/ActivityFeed.css","components/ActivityItem.css","components/ActivityContent.css","components/ActivityForm.css","components/ReplyForm.css","pages/NotificationsFeedPage.css","pages/SignupPage.css","pages/SigninPage.css","pages/RecoverPage.css","components/MessageGroupFeed.css","components/MessageGroupItem.css","pages/MessageGroupPage.css","components/MessageItem.css","components/MessageForm.css","pages/ConfirmationPage.css"],"sourcesContent":["* { box-sizing: border-box; }\nhtml,body { \n height: 100%; \n width: 100%; \n background: rgb(14,3,28);\n}\n\nbody {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\n sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\n monospace;\n}\n","main, article {\n height: 100%;\n width: 100%;\n}\n\n.content {\n width: 600px;\n height: 100%;\n background: #1e25e753;\n}","nav {\n flex-shrink: 1;\n width: 280px;\n padding-top: 16px;\n padding-right: 24px;\n}\nnav .logo {\n width: 48px;\n height: 48px;\n fill: rgb(149,0,255);\n}\n\nnav a.primary {\n display: flex;\n gap: 24px;\n align-items: center;\n color: rgb(255,255,255,0.5);\n font-size: 20px;\n text-decoration: none;\n padding: 12px 0;\n margin: 8px 0;\n}\nnav a.primary:hover {\n color: rgb(255,255,255,1);\n}\nnav a.primary .icon {\n fill: rgb(255,255,255,0.5);\n width: 24px;\n height: 24px;\n}\n\nnav a.primary:hover .icon {\n fill: rgb(255,255,255,1);\n}\n\nnav a.primary.active {\n color: rgb(149,0,255) !important;\n font-weight: 600;\n}\nnav a.primary.active svg {\n fill: rgb(149,0,255) !important;\n}\n\nnav button.post {\n display: block;\n background: rgba(149,0,255,1);\n color: #fff;\n border: none;\n text-decoration: none;\n border-radius: 999px;\n width: 100%;\n font-size: 20px;\n padding: 16px 0px;\n font-weight: 600;\n text-align: center;\n margin-top: 24px;\n}",".profile-info-wrapper.popped .profile-dialog {\n display: block;\n}\n\n.profile-info-wrapper {\n position: relative;\n}\n\n.profile-info {\n display: flex;\n gap: 12px;\n border-radius: 999px;\n padding: 12px;\n margin-top: 12px;\n align-items: center;\n position: relative;\n z-index: 2;\n}\n\n.profile-info:hover {\n background: rgba(255,255,255,0.08);\n}\n\n.profile-info .profile-avatar{\n background: rgb(149,0,255);\n height: 40px;\n width: 40px;\n border-radius: 999px;\n flex-shrink: 0;\n}\n\n.profile-desc {\n flex-grow: 1;\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.profile-info .profile-display-name {\n font-weight: 800;\n color: #fff;\n font-size: 14px;\n}\n\n.profile-info .profile-username {\n color: rgb(255,255,255,0.5);\n font-size: 14px;\n}\n\n\n.profile-info .icon {\n fill: rgb(255,255,255,0.5);\n width: 18px;\n height: 18px;\n margin-right: 8px\n}\n\n.profile-dialog {\n background: #000;\n box-shadow: 0px 0px 4px rgba(255,255,255,0.6);\n border-radius: 8px;\n min-height: 60px;\n z-index: 3;\n position: absolute;\n min-width: 240px;\n bottom: 70px;\n overflow: hidden;\n display: none;\n}\n\n.profile-dialog button {\n color: #fff;\n font-size: 16px;\n font-weight: 600;\n padding: 24px;\n width: 100%;\n text-align: left;\n border: none;\n background: none;\n cursor: pointer;\n outline: none;\n}\n.profile-dialog button:hover {\n background: rgba(255,255,255,0.1)\n}","section {\n flex-shrink: 1;\n width: 380px;\n padding: 16px;\n}\n\nsection footer {\n display: flex;\n gap: 16px;\n padding: 12px;\n}\n\nsection footer a {\n text-decoration: none;\n color: rgba(255,255,255,0.2);\n font-size: 14px;\n}\n\nsection footer a:hover {\n color: rgba(255,255,255,1);\n}",".search_field input[type='text'] {\n border: solid 1px rgba(149,0,255,0.1);\n background: rgba(149,0,255,0.1);\n padding: 16px;\n font-size: 16px;\n border-radius: 8px;\n width: 100%;\n outline: none;\n color: #fff;\n}\n.search_field input[type='text']:focus {\n border: solid 1px rgb(149,0,255,1);\n}",".trendings {\n display: flex;\n flex-direction: column;\n background: #000;\n border-radius: 8px;\n margin-top: 24px;\n}\n\n.trendings .trendings-title {\n font-size: 16px;\n color: #fff;\n padding: 16px;\n border-bottom: solid 1px rgb(31,36,49);\n}\n\n.trendings a {\n text-decoration: none;\n font-size: 16px;\n color: #fff;\n padding: 16px;\n}\n\n.trendings a span {\n display: block;\n}\n\n.trendings a span.count {\n font-size: 13px;\n color: rgba(255,255,255,0.5)\n}",".suggested_users {\n display: flex;\n flex-direction: column;\n background: #000;\n border-radius: 8px;\n margin-top: 24px;\n}\n\n.suggested_users_title {\n font-size: 16px;\n color: #fff;\n padding: 16px;\n border-bottom: solid 1px rgb(31,36,49);\n}\n\n.suggested_users .user {\n text-decoration: none;\n font-size: 16px;\n color: #fff;\n padding: 16px;\n display: flex;\n}\n\n.suggested_users .user span {\n display: block;\n}\n\n.suggested_users .user span.handle {\n font-size: 13px;\n color: rgba(255,255,255,0.5)\n}\n\n.suggested_users .avatar {\n background: rgb(149,0,255);\n height: 36px;\n width: 36px;\n border-radius: 999px;\n flex-shrink: 0;\n margin-right: 12px;\n}\n",".join {\n display: flex;\n flex-direction: column;\n background: #000;\n border-radius: 8px;\n margin-top: 24px;\n}\n\n.join .join-title {\n font-size: 16px;\n color: #fff;\n padding: 16px;\n border-bottom: solid 1px rgb(31,36,49);\n}\n\n.join p {\n color: #fff;\n font-size: 16px;\n text-align: center;\n}\n\n.join-content {\n padding: 24px;\n padding-top: 12px;\n}\n\n.join-content a.action {\n display: block;\n background: rgba(149,0,255,1);\n color: #fff;\n border: none;\n text-decoration: none;\n border-radius: 999px;\n width: 100%;\n font-size: 20px;\n padding: 16px 0px;\n font-weight: 600;\n text-align: center;\n margin-top: 24px;\n}\n\n.join-content a.subaction {\n color: rgba(149,0,255,1);\n text-align: center;\n text-decoration: none;\n display: block;\n padding: 12px 0px;\n margin-top: 12px;\n font-size: 16px;\n}",".activity_feed {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n flex-shrink: 0;\n}\n\n.activity_feed_collection {\n display: flex;\n flex-direction: column;\n}\n\n.activity_feed_heading {\n padding: 16px;\n padding-bottom: 48px;\n border-bottom: solid 1px rgb(58,63,78);\n}\n\n.activity_feed_heading .title{\n color: #fff;\n font-weight: 800;\n font-size: 20px;\n}\n",".activity_item {\n display: flex;\n flex-direction: column;\n border-bottom: solid 1px rgb(31,36,49);\n overflow: hidden;\n padding: 16px;\n}\n.activity_item:last-child {\n border-bottom: none;\n}\n\n.activity_item .activity_actions {\n display: flex;\n align-items: flex-end;\n padding-top: 16px;\n padding-left: 60px;\n}\n\n.activity_item .activity_actions .action{\n flex-grow: 1;\n cursor: pointer;\n padding: 8px;\n border-radius: 999px;\n display: flex;\n flex-direction: row;\n gap: 12px\n}\n\n.activity_item .activity_actions .action .counter{\n color: rgba(255,255,255,0.8);\n font-weight: 800;\n}\n\n.activity_item .activity_actions .icon{\n height: 18px;\n width: 18px;\n fill: #fff;\n}\n\n.activity_item .activity_actions .action:hover {\n background: rgba(149,0,255,0.2);\n}\n\n.activity_item .activity_actions .action:hover .icon {\n fill: rgb(149,0,255);\n}\n",".activity_content_wrap {\n display: flex;\n flex-direction: row;\n align-items: start;\n}\n\n.activity_content {\n flex-grow: 1;\n padding-left: 12px;\n}\n\n.activity_content_wrap .activity_avatar {\n background: rgb(149,0,255);\n height: 60px;\n width: 60px;\n border-radius: 999px;\n flex-shrink: 0;\n}\n\n.activity_content .activity_meta {\n display: flex;\n justify-content: space-between;\n}\n\n.activity_content a.activity_identity {\n flex-grow: 1;\n text-decoration: none;\n font-size: 16px;\n}\n\n.activity_content a.activity_identity .display_name {\n font-weight: 800;\n color: #fff;\n}\n\n.activity_content a.activity_identity .handle {\n color: rgb(255,255,255,0.5);\n}\n\n.activity_content .activity_times {\n display: flex;\n flex-direction: column;\n align-items: flex-end;\n}\n\n.activity_content .activity_times .created_at {\n color: rgb(72,77,95);\n font-size: 14px;\n}\n\n.activity_content .activity_times .expires_at {\n color: rgb(149,0,255);\n font-size: 14px;\n display: flex;\n gap: 2px;\n}\n.activity_content .activity_times .expires_at svg.icon {\n fill: rgb(149,0,255);\n width: 14px;\n height: 14px;\n}\n\n.activity_content .message {\n color: #fff;\n font-size: 16px;\n margin-top: 8px;\n}","form.activity_form {\n padding: 16px;\n display: flex;\n flex-direction: column;\n}\n\nform.activity_form textarea {\n font-family: Arial, Helvetica, sans-serif;\n font-size: 16px;\n border-radius: 4px;\n border: none;\n outline: none;\n display: block;\n outline: none;\n resize: none;\n width: 100%;\n height: 140px;\n padding: 16px;\n border: solid 1px rgba(149,0,255,0.1);\n background: rgba(149,0,255,0.1);\n color: #fff;\n}\n\nform.activity_form textarea:focus {\n border: solid 1px rgb(149,0,255,1);\n}\n\nform.activity_form .submit {\n display: flex;\n flex-direction: row;\n justify-content: flex-end;\n align-items: center;\n margin-top: 12px;\n font-weight: 600;\n}\n\nform.activity_form button[type='submit'] {\n font-weight: 800;\n outline: none;\n border: none;\n border-radius: 4px;\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n height: 38px;\n padding: 10px 20px;\n font-size: 16px;\n margin-left: 12px;\n background: rgba(149,0,255,1);\n color: #fff;\n}\n\nform.activity_form .count {\n color: rgba(255,255,255,0.3)\n}\n\nform.activity_form .count.err {\n color: rgb(255, 0, 0)\n}\n\nform.activity_form .expires_at_field {\n display: flex;\n gap: 12px;\n position: relative;\n border-left: solid 1px rgba(149,0,255,0.7);\n}\n\nform.activity_form .expires_at_field .icon {\n position: absolute;\n top: 12px;\n left: 8px;\n fill: #fff;\n width: 14px;\n height: 14px;\n z-index: 2;\n}\n\nform.activity_form select {\n width: 100%;\n outline: none;\n width: 120px;\n height: 38px;\n font-size: 16px;\n font-weight: 600;\n display: block;\n padding: 10px 8px;\n padding-left: 28px;\n border: none;\n border-radius: 4px;\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n color: #fff;\n background-color: rgba(149,0,255,0.8);\n background-image: url(\"data:image/svg+xml;utf8,\");\n background-repeat: no-repeat;\n background-position-x: 100%;\n background-position-y: 50%;\n position: relative;\n z-index: 1;\n}",".popup_form_wrap {\n position: fixed;\n height: 100%;\n width: 100%;\n top: 0;\n left: 0;\n display: flex;\n flex-direction: column;\n justify-content: flex-start;\n align-items: center;\n padding-top: 48px;\n background: rgba(255,255,255,0.1)\n}\n\n.popup_form {\n background: #000;\n box-shadow: 0px 0px 6px rgba(190, 9, 190, 0.6);\n border-radius: 16px;\n width: 600px;\n}\n\nform.replies_form {\n padding: 16px;\n display: flex;\n flex-direction: column;\n}\n\n.activity_wrap {\n padding: 16px;\n}\n\nform.replies_form textarea {\n font-family: Arial, Helvetica, sans-serif;\n font-size: 16px;\n border-radius: 4px;\n border: none;\n outline: none;\n display: block;\n outline: none;\n resize: none;\n width: 100%;\n height: 140px;\n padding: 16px;\n border: solid 1px rgba(149,0,255,0.1);\n background: rgba(149,0,255,0.1);\n color: #fff;\n}\n\nform.replies_form textarea:focus {\n border: solid 1px rgb(149,0,255,1);\n}\n\nform.replies_form .submit {\n display: flex;\n flex-direction: row;\n justify-content: flex-end;\n align-items: center;\n margin-top: 12px;\n font-weight: 600;\n}\n\nform.replies_form button[type='submit'] {\n font-weight: 800;\n outline: none;\n border: none;\n border-radius: 4px;\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n height: 38px;\n padding: 10px 20px;\n font-size: 16px;\n margin-left: 12px;\n background: rgba(149,0,255,1);\n color: #fff;\n}\n\nform.replies_form .count {\n color: rgba(255,255,255,0.3)\n}\n\nform.replies_form .count.err {\n color: rgb(255, 0, 0)\n}\n\nform.replies_form .expires_at_field {\n display: flex;\n gap: 12px;\n position: relative;\n border-left: solid 1px rgba(149,0,255,0.7);\n}\n\nform.replies_form .expires_at_field .icon {\n position: absolute;\n top: 12px;\n left: 8px;\n fill: #fff;\n width: 14px;\n height: 14px;\n z-index: 2;\n}","article {\n display: flex;\n flex-direction: row;\n justify-content: center;\n}","article.signup-article .signup-info {\n flex-grow: 1;\n padding-left: 12px;\n}\n\narticle.signup-article .signup-info .logo {\n width: 140px;\n height: 140px;\n fill: #2021246c;\n}\n\narticle.signup-article {\n display: flex;\n width: 100%;\n height: 100%;\n justify-content: flex-start;\n background: #8819f5;\n}\n\narticle.signup-article .signup-wrapper {\n background: #202124;\n width: 760px;\n display: flex;\n padding-top: 120px;\n flex-direction: column;\n}\narticle.signup-article form {\n padding: 0px 120px;\n flex-grow: 1;\n}\n\narticle.signup-article label {\n padding: 4px 0px;\n display: block;\n color: rgba(255,255,255,0.6);\n}\n\narticle.signup-article input[type='text'],\narticle.signup-article input[type='password'] {\n font-family: Arial, Helvetica, sans-serif;\n font-size: 16px;\n border-radius: 4px;\n border: none;\n outline: none;\n display: block;\n outline: none;\n resize: none;\n width: 100%;\n padding: 16px;\n border: solid 1px #555555;\n background: #1f1f1f;\n color: #fff;\n}\n\n.field.email {\n margin-top: 16px;\n}\n.field.username {\n margin-top: 16px;\n}\n.field.password {\n margin-top: 16px;\n}\n\narticle.signup-article input[type='text']:focus ,\narticle.signup-article input[type='password']:focus {\n border: solid 1px rgb(149,0,255,1);\n}\n\narticle.signup-article .submit {\n margin-top: 24px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n align-items: center;\n}\n\narticle.signup-article h2 {\n text-align: center;\n color: #fff;\n}\n\narticle.signin-article a.forgot-link {\n color: #fff;\n font-weight: 600;\n text-decoration: none;\n}\n\n\narticle.signup-article button[type='submit'] {\n font-weight: 800;\n outline: none;\n border: none;\n border-radius: 6px;\n padding: 18px 28px;\n font-size: 16px;\n background: rgba(149,0,255,1);\n color: #fff;\n}\n\n.already-have-an-account {\n background: #252525;\n padding: 24px;\n color: rgba(255,255,255,0.6);\n gap: 8px;\n display: flex;\n justify-content: center;\n}\n.already-have-an-account a {\n color: rgba(149,0,255,1);\n text-decoration: none;\n font-weight: 600;\n}\n\n.errors {\n padding: 16px;\n border-radius: 8px;\n background: rgba(255,0,0,0.3);\n color: rgb(255,255,255);\n margin-top: 16px;\n font-size: 14px;\n}","article.signin-article .signin-info {\n flex-grow: 1;\n padding-left: 12px;\n}\n\narticle.signin-article .signin-info .logo {\n width: 140px;\n height: 140px;\n fill: #2021246c;\n}\n\narticle.signin-article {\n display: flex;\n width: 100%;\n height: 100%;\n justify-content: flex-start;\n background: #8819f5;\n}\n\narticle.signin-article .signin-wrapper {\n background: #202124;\n width: 760px;\n display: flex;\n padding-top: 120px;\n flex-direction: column;\n}\narticle.signin-article form {\n padding: 0px 120px;\n flex-grow: 1;\n}\n\narticle.signin-article label {\n padding: 4px 0px;\n display: block;\n color: rgba(255,255,255,0.6);\n}\n\narticle.signin-article input[type='text'],\narticle.signin-article input[type='password'] {\n font-family: Arial, Helvetica, sans-serif;\n font-size: 16px;\n border-radius: 4px;\n border: none;\n outline: none;\n display: block;\n outline: none;\n resize: none;\n width: 100%;\n padding: 16px;\n border: solid 1px #555555;\n background: #1f1f1f;\n color: #fff;\n}\n\n.field.password {\n margin-top: 16px;\n}\n\narticle.signin-article input[type='text']:focus ,\narticle.signin-article input[type='password']:focus {\n border: solid 1px rgb(149,0,255,1);\n}\n\narticle.signin-article .submit {\n margin-top: 24px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n align-items: center;\n}\n\narticle.signin-article h2 {\n text-align: center;\n color: #fff;\n}\n\narticle.signin-article a.forgot-link {\n color: #fff;\n font-weight: 600;\n text-decoration: none;\n}\n\n\narticle.signin-article button[type='submit'] {\n font-weight: 800;\n outline: none;\n border: none;\n border-radius: 6px;\n padding: 18px 28px;\n font-size: 16px;\n background: rgba(149,0,255,1);\n color: #fff;\n}\n\n.dont-have-an-account {\n background: #252525;\n padding: 24px;\n color: rgba(255,255,255,0.6);\n gap: 8px;\n display: flex;\n justify-content: center;\n}\n.dont-have-an-account a {\n color: rgba(149,0,255,1);\n text-decoration: none;\n font-weight: 600;\n}","article.recover-article .recover-info .logo {\n width: 140px;\n height: 140px;\n fill: #2021246c;\n}\n\narticle.recover-article {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n justify-content: flex-start;\n align-items: center;\n background: #8819f5;\n}\n\narticle.recover-article .recover-wrapper {\n background: #202124;\n width: 560px;\n border-radius: 12px;\n display: flex;\n flex-direction: column;\n}\narticle.recover-article form {\n padding: 48px;\n flex-grow: 1;\n}\n\narticle.recover-article label {\n padding: 4px 0px;\n display: block;\n color: rgba(255,255,255,0.6);\n}\n\narticle.recover-article input[type='text'],\narticle.recover-article input[type='password'] {\n font-family: Arial, Helvetica, sans-serif;\n font-size: 16px;\n border-radius: 4px;\n border: none;\n outline: none;\n display: block;\n outline: none;\n resize: none;\n width: 100%;\n padding: 16px;\n border: solid 1px #555555;\n background: #1f1f1f;\n color: #fff;\n}\n\n.field.password {\n margin-top: 16px;\n}\n\narticle.recover-article input[type='text']:focus ,\narticle.recover-article input[type='password']:focus {\n border: solid 1px rgb(149,0,255,1);\n}\n\narticle.recover-article .submit {\n margin-top: 24px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n align-items: center;\n}\n\narticle.recover-article h2 {\n text-align: center;\n color: #fff;\n}\n\n\n\narticle.recover-article button[type='submit'] {\n font-weight: 800;\n outline: none;\n border: none;\n border-radius: 6px;\n padding: 18px 28px;\n font-size: 16px;\n background: rgba(149,0,255,1);\n color: #fff;\n}","section.message_groups {\n background: #000;\n border-right: solid 1px rgb(58,63,78);\n padding: 0px;\n}\n\n.message_group_feed_heading {\n padding: 16px;\n padding-bottom: 48px;\n border-bottom: solid 1px rgb(58,63,78);\n}\n\n.message_group_feed_heading .title{\n color: #fff;\n font-weight: 800;\n font-size: 20px;\n}\n\n.message_group_feed {\n display: flex;\n flex-direction: column;\n height: 100%;\n}\n\n.message_group_feed_collection {\n flex-grow: 1;\n overflow: auto;\n}",".message_group_item {\n display: flex;\n align-items: start;\n overflow: hidden;\n padding: 16px;\n cursor: pointer;\n text-decoration: none;\n}\n.message_group_item:hover {\nbackground: rgba(255,255,255,0.08)\n}\n\n.message_group_item.active {\nbackground: rgba(255,255,255,0.08)\n}\n\n.message_group_item:last-child {\n border-bottom: none;\n}\n\n.message_group_item .message_content {\n flex-grow: 1;\n padding-left: 12px;\n}\n\n.message_group_item .message_group_avatar {\n background: rgb(149,0,255);\n height: 50px;\n width: 50px;\n border-radius: 999px;\n flex-shrink: 0;\n margin-right: 8px;\n}\n\n.message_group_item .message_group_meta {\n display: flex;\n justify-content: space-between;\n}\n\n.message_group_item .message_group_identity {\n flex-grow: 1;\n text-decoration: none;\n font-size: 16px;\n}\n\n.message_group_item .message_group_identity .display_name {\n font-weight: 800;\n color: #fff;\n}\n\n.message_group_item .message_group_identity .handle {\n color: rgb(255,255,255,0.5);\n}\n\n\n.message_group_item .created_at {\n color: rgb(72,77,95);\n font-size: 14px;\n}\n\n.message_group_item .message {\n color: #fff;\n font-size: 16px;\n margin-top: 8px;\n}",".messages.content {\n display: flex;\n flex-direction: column;\n}\n\n.message_feed {\n flex-grow: 1;\n overflow: auto;\n}",".message_item {\n display: flex;\n align-items: start;\n overflow: hidden;\n border-bottom: solid 1px rgb(31,36,49);\n padding: 16px;\n cursor: pointer;\n text-decoration: none;\n}\n.message_item:hover {\nbackground: rgba(255,255,255,0.08)\n}\n\n.message_item:last-child {\n border-bottom: none;\n}\n\n.message_item .message_content {\n flex-grow: 1;\n padding-left: 12px;\n}\n\n.message_item .message_avatar {\n background: rgb(149,0,255);\n height: 50px;\n width: 50px;\n border-radius: 999px;\n flex-shrink: 0;\n margin-right: 8px;\n}\n\n.message_item .message_meta {\n display: flex;\n justify-content: space-between;\n}\n\n.message_item .message_identity {\n flex-grow: 1;\n text-decoration: none;\n font-size: 16px;\n}\n\n.message_item .message_identity .display_name {\n font-weight: 800;\n color: #fff;\n}\n\n.message_item .message_identity .handle {\n color: rgb(255,255,255,0.5);\n}\n\n\n.message_item .created_at {\n color: rgb(72,77,95);\n font-size: 14px;\n}\n\n.message_item .message {\n color: #fff;\n font-size: 16px;\n margin-top: 8px;\n}","form.message_form {\n padding: 16px;\n display: flex;\n flex-direction: column;\n}\n\nform.message_form textarea {\n font-family: Arial, Helvetica, sans-serif;\n font-size: 16px;\n border-radius: 4px;\n border: none;\n outline: none;\n display: block;\n outline: none;\n resize: none;\n width: 100%;\n height: 140px;\n padding: 16px;\n border: solid 1px rgba(149,0,255,0.1);\n background: rgba(149,0,255,0.1);\n color: #fff;\n}\n\nform.message_form textarea:focus {\n border: solid 1px rgb(149,0,255,1);\n}\n\nform.message_form .submit {\n display: flex;\n flex-direction: row;\n justify-content: flex-end;\n align-items: center;\n margin-top: 12px;\n font-weight: 600;\n}\n\nform.message_form button[type='submit'] {\n font-weight: 800;\n outline: none;\n border: none;\n border-radius: 4px;\n height: 38px;\n padding: 10px 20px;\n font-size: 16px;\n background: rgba(149,0,255,1);\n color: #fff;\n margin-left: 12px;\n}\n\nform.message_form .count {\n color: rgba(255,255,255,0.3)\n}\n\nform.message_form .count.err {\n color: rgb(255, 0, 0)\n}\n","article.confirm-article .recover-info .logo {\n width: 140px;\n height: 140px;\n fill: #2021246c;\n}\n\narticle.confirm-article {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n justify-content: flex-start;\n align-items: center;\n background: #8819f5;\n}\n\narticle.confirm-article .recover-wrapper {\n background: #202124;\n width: 560px;\n border-radius: 12px;\n display: flex;\n flex-direction: column;\n}\narticle.confirm-article form {\n padding: 48px;\n flex-grow: 1;\n}\n\narticle.confirm-article label {\n padding: 4px 0px;\n display: block;\n color: rgba(255,255,255,0.6);\n}\n\narticle.confirm-article input[type='text'],\narticle.confirm-article input[type='password'] {\n font-family: Arial, Helvetica, sans-serif;\n font-size: 16px;\n border-radius: 4px;\n border: none;\n outline: none;\n display: block;\n outline: none;\n resize: none;\n width: 100%;\n padding: 16px;\n border: solid 1px #555555;\n background: #1f1f1f;\n color: #fff;\n}\n\n.field.code {\n margin-top: 16px;\n}\n\narticle.confirm-article input[type='text']:focus ,\narticle.confirm-article input[type='password']:focus {\n border: solid 1px rgb(149,0,255,1);\n}\n\narticle.confirm-article .submit {\n margin-top: 24px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n align-items: center;\n}\n\narticle.confirm-article h2 {\n text-align: center;\n color: #fff;\n margin-top: 0;\n}\n\narticle.confirm-article button[type='submit'] {\n font-weight: 800;\n outline: none;\n border: none;\n border-radius: 6px;\n padding: 18px 28px;\n font-size: 16px;\n background: rgba(149,0,255,1);\n color: #fff;\n}\n\narticle.confirm-article button.resend {\n font-weight: 800;\n outline: none;\n border: none;\n border-radius: 6px;\n padding: 18px 28px;\n font-size: 16px;\n background: #562397;\n color: #fff;\n margin-top: 24px;\n cursor: pointer;\n}\narticle.confirm-article button.resend:hover {\n background: #451782;\n}\n\narticle.confirm-article .sent-message {\n margin-top: 24px;\n font-size: 18px;\n font-weight: 600;\n color: rgba(0,0,0,0.7)\n}"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/frontend-react-js/build/static/js/787.cda612ba.chunk.js b/frontend-react-js/build/static/js/787.cda612ba.chunk.js new file mode 100644 index 000000000..95bd57d3b --- /dev/null +++ b/frontend-react-js/build/static/js/787.cda612ba.chunk.js @@ -0,0 +1,2 @@ +"use strict";(self.webpackChunkfrontend=self.webpackChunkfrontend||[]).push([[787],{787:function(e,n,t){t.r(n),t.d(n,{getCLS:function(){return y},getFCP:function(){return g},getFID:function(){return C},getLCP:function(){return P},getTTFB:function(){return D}});var i,r,a,o,u=function(e,n){return{name:e,value:void 0===n?-1:n,delta:0,entries:[],id:"v2-".concat(Date.now(),"-").concat(Math.floor(8999999999999*Math.random())+1e12)}},c=function(e,n){try{if(PerformanceObserver.supportedEntryTypes.includes(e)){if("first-input"===e&&!("PerformanceEventTiming"in self))return;var t=new PerformanceObserver((function(e){return e.getEntries().map(n)}));return t.observe({type:e,buffered:!0}),t}}catch(e){}},f=function(e,n){var t=function t(i){"pagehide"!==i.type&&"hidden"!==document.visibilityState||(e(i),n&&(removeEventListener("visibilitychange",t,!0),removeEventListener("pagehide",t,!0)))};addEventListener("visibilitychange",t,!0),addEventListener("pagehide",t,!0)},s=function(e){addEventListener("pageshow",(function(n){n.persisted&&e(n)}),!0)},m=function(e,n,t){var i;return function(r){n.value>=0&&(r||t)&&(n.delta=n.value-(i||0),(n.delta||void 0===i)&&(i=n.value,e(n)))}},v=-1,d=function(){return"hidden"===document.visibilityState?0:1/0},p=function(){f((function(e){var n=e.timeStamp;v=n}),!0)},l=function(){return v<0&&(v=d(),p(),s((function(){setTimeout((function(){v=d(),p()}),0)}))),{get firstHiddenTime(){return v}}},g=function(e,n){var t,i=l(),r=u("FCP"),a=function(e){"first-contentful-paint"===e.name&&(f&&f.disconnect(),e.startTime-1&&e(n)},r=u("CLS",0),a=0,o=[],v=function(e){if(!e.hadRecentInput){var n=o[0],i=o[o.length-1];a&&e.startTime-i.startTime<1e3&&e.startTime-n.startTime<5e3?(a+=e.value,o.push(e)):(a=e.value,o=[e]),a>r.value&&(r.value=a,r.entries=o,t())}},d=c("layout-shift",v);d&&(t=m(i,r,n),f((function(){d.takeRecords().map(v),t(!0)})),s((function(){a=0,T=-1,r=u("CLS",0),t=m(i,r,n)})))},E={passive:!0,capture:!0},w=new Date,L=function(e,n){i||(i=n,r=e,a=new Date,F(removeEventListener),S())},S=function(){if(r>=0&&r1e12?new Date:performance.now())-e.timeStamp;"pointerdown"==e.type?function(e,n){var t=function(){L(e,n),r()},i=function(){r()},r=function(){removeEventListener("pointerup",t,E),removeEventListener("pointercancel",i,E)};addEventListener("pointerup",t,E),addEventListener("pointercancel",i,E)}(n,e):L(n,e)}},F=function(e){["mousedown","keydown","touchstart","pointerdown"].forEach((function(n){return e(n,b,E)}))},C=function(e,n){var t,a=l(),v=u("FID"),d=function(e){e.startTimeperformance.now())return;t.entries=[n],e(t)}catch(e){}},"complete"===document.readyState?setTimeout(n,0):addEventListener("load",(function(){return setTimeout(n,0)}))}}}]); +//# sourceMappingURL=787.cda612ba.chunk.js.map \ No newline at end of file diff --git a/frontend-react-js/build/static/js/787.cda612ba.chunk.js.map b/frontend-react-js/build/static/js/787.cda612ba.chunk.js.map new file mode 100644 index 000000000..d40a3bbd0 --- /dev/null +++ b/frontend-react-js/build/static/js/787.cda612ba.chunk.js.map @@ -0,0 +1 @@ +{"version":3,"file":"static/js/787.cda612ba.chunk.js","mappings":"qQAAA,IAAIA,EAAEC,EAAEC,EAAEC,EAAEC,EAAE,SAASJ,EAAEC,GAAG,MAAM,CAACI,KAAKL,EAAEM,WAAM,IAASL,GAAG,EAAEA,EAAEM,MAAM,EAAEC,QAAQ,GAAGC,GAAG,MAAMC,OAAOC,KAAKC,MAAM,KAAKF,OAAOG,KAAKC,MAAM,cAAcD,KAAKE,UAAU,MAAM,EAAEC,EAAE,SAAShB,EAAEC,GAAG,IAAI,GAAGgB,oBAAoBC,oBAAoBC,SAASnB,GAAG,CAAC,GAAG,gBAAgBA,KAAK,2BAA2BoB,MAAM,OAAO,IAAIlB,EAAE,IAAIe,qBAAqB,SAASjB,GAAG,OAAOA,EAAEqB,aAAaC,IAAIrB,EAAE,IAAI,OAAOC,EAAEqB,QAAQ,CAACC,KAAKxB,EAAEyB,UAAS,IAAKvB,CAAC,CAAW,CAAT,MAAMF,GAAG,CAAC,EAAE0B,EAAE,SAAS1B,EAAEC,GAAG,IAAIC,EAAE,SAASA,EAAEC,GAAG,aAAaA,EAAEqB,MAAM,WAAWG,SAASC,kBAAkB5B,EAAEG,GAAGF,IAAI4B,oBAAoB,mBAAmB3B,GAAE,GAAI2B,oBAAoB,WAAW3B,GAAE,IAAK,EAAE4B,iBAAiB,mBAAmB5B,GAAE,GAAI4B,iBAAiB,WAAW5B,GAAE,EAAG,EAAE6B,EAAE,SAAS/B,GAAG8B,iBAAiB,YAAY,SAAS7B,GAAGA,EAAE+B,WAAWhC,EAAEC,EAAE,IAAG,EAAG,EAAEgC,EAAE,SAASjC,EAAEC,EAAEC,GAAG,IAAIC,EAAE,OAAO,SAASC,GAAGH,EAAEK,OAAO,IAAIF,GAAGF,KAAKD,EAAEM,MAAMN,EAAEK,OAAOH,GAAG,IAAIF,EAAEM,YAAO,IAASJ,KAAKA,EAAEF,EAAEK,MAAMN,EAAEC,IAAI,CAAC,EAAEiC,GAAG,EAAEC,EAAE,WAAW,MAAM,WAAWR,SAASC,gBAAgB,EAAE,GAAG,EAAEQ,EAAE,WAAWV,GAAG,SAAS1B,GAAG,IAAIC,EAAED,EAAEqC,UAAUH,EAAEjC,CAAC,IAAG,EAAG,EAAEqC,EAAE,WAAW,OAAOJ,EAAE,IAAIA,EAAEC,IAAIC,IAAIL,GAAG,WAAWQ,YAAY,WAAWL,EAAEC,IAAIC,GAAG,GAAG,EAAE,KAAK,CAAKI,sBAAkB,OAAON,CAAC,EAAE,EAAEO,EAAE,SAASzC,EAAEC,GAAG,IAAIC,EAAEC,EAAEmC,IAAIZ,EAAEtB,EAAE,OAAO8B,EAAE,SAASlC,GAAG,2BAA2BA,EAAEK,OAAO+B,GAAGA,EAAEM,aAAa1C,EAAE2C,UAAUxC,EAAEqC,kBAAkBd,EAAEpB,MAAMN,EAAE2C,UAAUjB,EAAElB,QAAQoC,KAAK5C,GAAGE,GAAE,IAAK,EAAEiC,EAAEU,OAAOC,aAAaA,YAAYC,kBAAkBD,YAAYC,iBAAiB,0BAA0B,GAAGX,EAAED,EAAE,KAAKnB,EAAE,QAAQkB,IAAIC,GAAGC,KAAKlC,EAAE+B,EAAEjC,EAAE0B,EAAEzB,GAAGkC,GAAGD,EAAEC,GAAGJ,GAAG,SAAS5B,GAAGuB,EAAEtB,EAAE,OAAOF,EAAE+B,EAAEjC,EAAE0B,EAAEzB,GAAG+C,uBAAuB,WAAWA,uBAAuB,WAAWtB,EAAEpB,MAAMwC,YAAYlC,MAAMT,EAAEkC,UAAUnC,GAAE,EAAG,GAAG,GAAG,IAAI,EAAE+C,GAAE,EAAGC,GAAG,EAAEC,EAAE,SAASnD,EAAEC,GAAGgD,IAAIR,GAAG,SAASzC,GAAGkD,EAAElD,EAAEM,KAAK,IAAI2C,GAAE,GAAI,IAAI/C,EAAEC,EAAE,SAASF,GAAGiD,GAAG,GAAGlD,EAAEC,EAAE,EAAEiC,EAAE9B,EAAE,MAAM,GAAG+B,EAAE,EAAEC,EAAE,GAAGE,EAAE,SAAStC,GAAG,IAAIA,EAAEoD,eAAe,CAAC,IAAInD,EAAEmC,EAAE,GAAGjC,EAAEiC,EAAEA,EAAEiB,OAAO,GAAGlB,GAAGnC,EAAE2C,UAAUxC,EAAEwC,UAAU,KAAK3C,EAAE2C,UAAU1C,EAAE0C,UAAU,KAAKR,GAAGnC,EAAEM,MAAM8B,EAAEQ,KAAK5C,KAAKmC,EAAEnC,EAAEM,MAAM8B,EAAE,CAACpC,IAAImC,EAAED,EAAE5B,QAAQ4B,EAAE5B,MAAM6B,EAAED,EAAE1B,QAAQ4B,EAAElC,IAAI,CAAC,EAAEiD,EAAEnC,EAAE,eAAesB,GAAGa,IAAIjD,EAAE+B,EAAE9B,EAAE+B,EAAEjC,GAAGyB,GAAG,WAAWyB,EAAEG,cAAchC,IAAIgB,GAAGpC,GAAE,EAAG,IAAI6B,GAAG,WAAWI,EAAE,EAAEe,GAAG,EAAEhB,EAAE9B,EAAE,MAAM,GAAGF,EAAE+B,EAAE9B,EAAE+B,EAAEjC,EAAE,IAAI,EAAEsD,EAAE,CAACC,SAAQ,EAAGC,SAAQ,GAAIC,EAAE,IAAI/C,KAAKgD,EAAE,SAASxD,EAAEC,GAAGJ,IAAIA,EAAEI,EAAEH,EAAEE,EAAED,EAAE,IAAIS,KAAKiD,EAAE/B,qBAAqBgC,IAAI,EAAEA,EAAE,WAAW,GAAG5D,GAAG,GAAGA,EAAEC,EAAEwD,EAAE,CAAC,IAAItD,EAAE,CAAC0D,UAAU,cAAczD,KAAKL,EAAEwB,KAAKuC,OAAO/D,EAAE+D,OAAOC,WAAWhE,EAAEgE,WAAWrB,UAAU3C,EAAEqC,UAAU4B,gBAAgBjE,EAAEqC,UAAUpC,GAAGE,EAAE+D,SAAS,SAASlE,GAAGA,EAAEI,EAAE,IAAID,EAAE,EAAE,CAAC,EAAEgE,EAAE,SAASnE,GAAG,GAAGA,EAAEgE,WAAW,CAAC,IAAI/D,GAAGD,EAAEqC,UAAU,KAAK,IAAI1B,KAAKmC,YAAYlC,OAAOZ,EAAEqC,UAAU,eAAerC,EAAEwB,KAAK,SAASxB,EAAEC,GAAG,IAAIC,EAAE,WAAWyD,EAAE3D,EAAEC,GAAGG,GAAG,EAAED,EAAE,WAAWC,GAAG,EAAEA,EAAE,WAAWyB,oBAAoB,YAAY3B,EAAEqD,GAAG1B,oBAAoB,gBAAgB1B,EAAEoD,EAAE,EAAEzB,iBAAiB,YAAY5B,EAAEqD,GAAGzB,iBAAiB,gBAAgB3B,EAAEoD,EAAE,CAAhO,CAAkOtD,EAAED,GAAG2D,EAAE1D,EAAED,EAAE,CAAC,EAAE4D,EAAE,SAAS5D,GAAG,CAAC,YAAY,UAAU,aAAa,eAAekE,SAAS,SAASjE,GAAG,OAAOD,EAAEC,EAAEkE,EAAEZ,EAAE,GAAG,EAAEa,EAAE,SAASlE,EAAEgC,GAAG,IAAIC,EAAEC,EAAEE,IAAIG,EAAErC,EAAE,OAAO6C,EAAE,SAASjD,GAAGA,EAAE2C,UAAUP,EAAEI,kBAAkBC,EAAEnC,MAAMN,EAAEiE,gBAAgBjE,EAAE2C,UAAUF,EAAEjC,QAAQoC,KAAK5C,GAAGmC,GAAE,GAAI,EAAEe,EAAElC,EAAE,cAAciC,GAAGd,EAAEF,EAAE/B,EAAEuC,EAAEP,GAAGgB,GAAGxB,GAAG,WAAWwB,EAAEI,cAAchC,IAAI2B,GAAGC,EAAER,YAAY,IAAG,GAAIQ,GAAGnB,GAAG,WAAW,IAAIf,EAAEyB,EAAErC,EAAE,OAAO+B,EAAEF,EAAE/B,EAAEuC,EAAEP,GAAG/B,EAAE,GAAGF,GAAG,EAAED,EAAE,KAAK4D,EAAE9B,kBAAkBd,EAAEiC,EAAE9C,EAAEyC,KAAK5B,GAAG6C,GAAG,GAAG,EAAEQ,EAAE,CAAC,EAAEC,EAAE,SAAStE,EAAEC,GAAG,IAAIC,EAAEC,EAAEmC,IAAIJ,EAAE9B,EAAE,OAAO+B,EAAE,SAASnC,GAAG,IAAIC,EAAED,EAAE2C,UAAU1C,EAAEE,EAAEqC,kBAAkBN,EAAE5B,MAAML,EAAEiC,EAAE1B,QAAQoC,KAAK5C,GAAGE,IAAI,EAAEkC,EAAEpB,EAAE,2BAA2BmB,GAAG,GAAGC,EAAE,CAAClC,EAAE+B,EAAEjC,EAAEkC,EAAEjC,GAAG,IAAIwC,EAAE,WAAW4B,EAAEnC,EAAEzB,MAAM2B,EAAEkB,cAAchC,IAAIa,GAAGC,EAAEM,aAAa2B,EAAEnC,EAAEzB,KAAI,EAAGP,GAAE,GAAI,EAAE,CAAC,UAAU,SAASgE,SAAS,SAASlE,GAAG8B,iBAAiB9B,EAAEyC,EAAE,CAAC8B,MAAK,EAAGd,SAAQ,GAAI,IAAI/B,EAAEe,GAAE,GAAIV,GAAG,SAAS5B,GAAG+B,EAAE9B,EAAE,OAAOF,EAAE+B,EAAEjC,EAAEkC,EAAEjC,GAAG+C,uBAAuB,WAAWA,uBAAuB,WAAWd,EAAE5B,MAAMwC,YAAYlC,MAAMT,EAAEkC,UAAUgC,EAAEnC,EAAEzB,KAAI,EAAGP,GAAE,EAAG,GAAG,GAAG,GAAG,CAAC,EAAEsE,EAAE,SAASxE,GAAG,IAAIC,EAAEC,EAAEE,EAAE,QAAQH,EAAE,WAAW,IAAI,IAAIA,EAAE6C,YAAY2B,iBAAiB,cAAc,IAAI,WAAW,IAAIzE,EAAE8C,YAAY4B,OAAOzE,EAAE,CAAC6D,UAAU,aAAanB,UAAU,GAAG,IAAI,IAAIzC,KAAKF,EAAE,oBAAoBE,GAAG,WAAWA,IAAID,EAAEC,GAAGW,KAAK8D,IAAI3E,EAAEE,GAAGF,EAAE4E,gBAAgB,IAAI,OAAO3E,CAAC,CAAjL,GAAqL,GAAGC,EAAEI,MAAMJ,EAAEK,MAAMN,EAAE4E,cAAc3E,EAAEI,MAAM,GAAGJ,EAAEI,MAAMwC,YAAYlC,MAAM,OAAOV,EAAEM,QAAQ,CAACP,GAAGD,EAAEE,EAAY,CAAT,MAAMF,GAAG,CAAC,EAAE,aAAa2B,SAASmD,WAAWvC,WAAWtC,EAAE,GAAG6B,iBAAiB,QAAQ,WAAW,OAAOS,WAAWtC,EAAE,EAAE,GAAG,C","sources":["../node_modules/web-vitals/dist/web-vitals.js"],"sourcesContent":["var e,t,n,i,r=function(e,t){return{name:e,value:void 0===t?-1:t,delta:0,entries:[],id:\"v2-\".concat(Date.now(),\"-\").concat(Math.floor(8999999999999*Math.random())+1e12)}},a=function(e,t){try{if(PerformanceObserver.supportedEntryTypes.includes(e)){if(\"first-input\"===e&&!(\"PerformanceEventTiming\"in self))return;var n=new PerformanceObserver((function(e){return e.getEntries().map(t)}));return n.observe({type:e,buffered:!0}),n}}catch(e){}},o=function(e,t){var n=function n(i){\"pagehide\"!==i.type&&\"hidden\"!==document.visibilityState||(e(i),t&&(removeEventListener(\"visibilitychange\",n,!0),removeEventListener(\"pagehide\",n,!0)))};addEventListener(\"visibilitychange\",n,!0),addEventListener(\"pagehide\",n,!0)},u=function(e){addEventListener(\"pageshow\",(function(t){t.persisted&&e(t)}),!0)},c=function(e,t,n){var i;return function(r){t.value>=0&&(r||n)&&(t.delta=t.value-(i||0),(t.delta||void 0===i)&&(i=t.value,e(t)))}},f=-1,s=function(){return\"hidden\"===document.visibilityState?0:1/0},m=function(){o((function(e){var t=e.timeStamp;f=t}),!0)},v=function(){return f<0&&(f=s(),m(),u((function(){setTimeout((function(){f=s(),m()}),0)}))),{get firstHiddenTime(){return f}}},d=function(e,t){var n,i=v(),o=r(\"FCP\"),f=function(e){\"first-contentful-paint\"===e.name&&(m&&m.disconnect(),e.startTime-1&&e(t)},f=r(\"CLS\",0),s=0,m=[],v=function(e){if(!e.hadRecentInput){var t=m[0],i=m[m.length-1];s&&e.startTime-i.startTime<1e3&&e.startTime-t.startTime<5e3?(s+=e.value,m.push(e)):(s=e.value,m=[e]),s>f.value&&(f.value=s,f.entries=m,n())}},h=a(\"layout-shift\",v);h&&(n=c(i,f,t),o((function(){h.takeRecords().map(v),n(!0)})),u((function(){s=0,l=-1,f=r(\"CLS\",0),n=c(i,f,t)})))},T={passive:!0,capture:!0},y=new Date,g=function(i,r){e||(e=r,t=i,n=new Date,w(removeEventListener),E())},E=function(){if(t>=0&&t1e12?new Date:performance.now())-e.timeStamp;\"pointerdown\"==e.type?function(e,t){var n=function(){g(e,t),r()},i=function(){r()},r=function(){removeEventListener(\"pointerup\",n,T),removeEventListener(\"pointercancel\",i,T)};addEventListener(\"pointerup\",n,T),addEventListener(\"pointercancel\",i,T)}(t,e):g(t,e)}},w=function(e){[\"mousedown\",\"keydown\",\"touchstart\",\"pointerdown\"].forEach((function(t){return e(t,S,T)}))},L=function(n,f){var s,m=v(),d=r(\"FID\"),p=function(e){e.startTimeperformance.now())return;n.entries=[t],e(n)}catch(e){}},\"complete\"===document.readyState?setTimeout(t,0):addEventListener(\"load\",(function(){return setTimeout(t,0)}))};export{h as getCLS,d as getFCP,L as getFID,F as getLCP,P as getTTFB};\n"],"names":["e","t","n","i","r","name","value","delta","entries","id","concat","Date","now","Math","floor","random","a","PerformanceObserver","supportedEntryTypes","includes","self","getEntries","map","observe","type","buffered","o","document","visibilityState","removeEventListener","addEventListener","u","persisted","c","f","s","m","timeStamp","v","setTimeout","firstHiddenTime","d","disconnect","startTime","push","window","performance","getEntriesByName","requestAnimationFrame","p","l","h","hadRecentInput","length","takeRecords","T","passive","capture","y","g","w","E","entryType","target","cancelable","processingStart","forEach","S","L","b","F","once","P","getEntriesByType","timing","max","navigationStart","responseStart","readyState"],"sourceRoot":""} \ No newline at end of file diff --git a/frontend-react-js/build/static/js/main.09063207.js b/frontend-react-js/build/static/js/main.09063207.js new file mode 100644 index 000000000..77d3c7409 --- /dev/null +++ b/frontend-react-js/build/static/js/main.09063207.js @@ -0,0 +1,3 @@ +/*! For license information please see main.09063207.js.LICENSE.txt */ +!function(){var e={3025:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0})},9160:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0})},5218:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0})},3755:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0})},268:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.isMsWindow=void 0;var n=["decrypt","digest","encrypt","exportKey","generateKey","importKey","sign","verify"];t.isMsWindow=function(e){if(function(e){return"MSInputMethodContext"in e&&"msCrypto"in e}(e)&&void 0!==e.msCrypto.subtle){var t=e.msCrypto,r=t.getRandomValues,i=t.subtle;return n.map((function(e){return i[e]})).concat(r).every((function(e){return"function"===typeof e}))}return!1}},5062:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(2907);r.__exportStar(n(3025),t),r.__exportStar(n(9160),t),r.__exportStar(n(5218),t),r.__exportStar(n(3755),t),r.__exportStar(n(268),t)},2907:function(e,t,n){"use strict";n.r(t),n.d(t,{__assign:function(){return o},__asyncDelegator:function(){return w},__asyncGenerator:function(){return b},__asyncValues:function(){return S},__await:function(){return y},__awaiter:function(){return l},__classPrivateFieldGet:function(){return x},__classPrivateFieldSet:function(){return C},__createBinding:function(){return d},__decorate:function(){return s},__exportStar:function(){return h},__extends:function(){return i},__generator:function(){return f},__importDefault:function(){return E},__importStar:function(){return k},__makeTemplateObject:function(){return _},__metadata:function(){return c},__param:function(){return u},__read:function(){return v},__rest:function(){return a},__spread:function(){return m},__spreadArrays:function(){return g},__values:function(){return p}});var r=function(e,t){return r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])},r(e,t)};function i(e,t){function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}var o=function(){return o=Object.assign||function(e){for(var t,n=1,r=arguments.length;n=0;s--)(i=e[s])&&(a=(o<3?i(a):o>3?i(t,n,a):i(t,n))||a);return o>3&&a&&Object.defineProperty(t,n,a),a}function u(e,t){return function(n,r){t(n,r,e)}}function c(e,t){if("object"===typeof Reflect&&"function"===typeof Reflect.metadata)return Reflect.metadata(e,t)}function l(e,t,n,r){return new(n||(n=Promise))((function(i,o){function a(e){try{u(r.next(e))}catch(t){o(t)}}function s(e){try{u(r.throw(e))}catch(t){o(t)}}function u(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(a,s)}u((r=r.apply(e,t||[])).next())}))}function f(e,t){var n,r,i,o,a={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return o={next:s(0),throw:s(1),return:s(2)},"function"===typeof Symbol&&(o[Symbol.iterator]=function(){return this}),o;function s(o){return function(s){return function(o){if(n)throw new TypeError("Generator is already executing.");for(;a;)try{if(n=1,r&&(i=2&o[0]?r.return:o[0]?r.throw||((i=r.return)&&i.call(r),0):r.next)&&!(i=i.call(r,o[1])).done)return i;switch(r=0,i&&(o=[2&o[0],i.value]),o[0]){case 0:case 1:i=o;break;case 4:return a.label++,{value:o[1],done:!1};case 5:a.label++,r=o[1],o=[0];continue;case 7:o=a.ops.pop(),a.trys.pop();continue;default:if(!(i=(i=a.trys).length>0&&i[i.length-1])&&(6===o[0]||2===o[0])){a=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]=e.length&&(e=void 0),{value:e&&e[r++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function v(e,t){var n="function"===typeof Symbol&&e[Symbol.iterator];if(!n)return e;var r,i,o=n.call(e),a=[];try{for(;(void 0===t||t-- >0)&&!(r=o.next()).done;)a.push(r.value)}catch(s){i={error:s}}finally{try{r&&!r.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}return a}function m(){for(var e=[],t=0;t1||s(e,t)}))})}function s(e,t){try{(n=i[e](t)).value instanceof y?Promise.resolve(n.value.v).then(u,c):l(o[0][2],n)}catch(r){l(o[0][3],r)}var n}function u(e){s("next",e)}function c(e){s("throw",e)}function l(e,t){e(t),o.shift(),o.length&&s(o[0][0],o[0][1])}}function w(e){var t,n;return t={},r("next"),r("throw",(function(e){throw e})),r("return"),t[Symbol.iterator]=function(){return this},t;function r(r,i){t[r]=e[r]?function(t){return(n=!n)?{value:y(e[r](t)),done:"return"===r}:i?i(t):t}:i}}function S(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t,n=e[Symbol.asyncIterator];return n?n.call(e):(e=p(e),t={},r("next"),r("throw"),r("return"),t[Symbol.asyncIterator]=function(){return this},t);function r(n){t[n]=e[n]&&function(t){return new Promise((function(r,i){(function(e,t,n,r){Promise.resolve(r).then((function(t){e({value:t,done:n})}),t)})(r,i,(t=e[n](t)).done,t.value)}))}}}function _(e,t){return Object.defineProperty?Object.defineProperty(e,"raw",{value:t}):e.raw=t,e}function k(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)Object.hasOwnProperty.call(e,n)&&(t[n]=e[n]);return t.default=e,t}function E(e){return e&&e.__esModule?e:{default:e}}function x(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)}function C(e,t,n){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,n),n}},9867:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.EMPTY_DATA_SHA_256=t.SHA_256_HMAC_ALGO=t.SHA_256_HASH=void 0,t.SHA_256_HASH={name:"SHA-256"},t.SHA_256_HMAC_ALGO={name:"HMAC",hash:t.SHA_256_HASH},t.EMPTY_DATA_SHA_256=new Uint8Array([227,176,196,66,152,252,28,20,154,251,244,200,153,111,185,36,39,174,65,228,100,155,147,76,164,149,153,27,120,82,184,85])},1716:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.Sha256=void 0;var r=n(7591),i=n(3722),o=n(9335),a=n(712),s=n(5062),u=n(3090),c=function(){function e(e){(0,a.supportsWebCrypto)((0,u.locateWindow)())?this.hash=new i.Sha256(e):(0,s.isMsWindow)((0,u.locateWindow)())?this.hash=new r.Sha256(e):this.hash=new o.Sha256(e)}return e.prototype.update=function(e,t){this.hash.update(e,t)},e.prototype.digest=function(){return this.hash.digest()},e}();t.Sha256=c},7591:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.Sha256=void 0;var r=n(9558),i=n(9867),o=n(8071),a=n(3090),s=function(){function e(e){e?(this.operation=function(e){return new Promise((function(t,n){var r=(0,a.locateWindow)().msCrypto.subtle.importKey("raw",u(e),i.SHA_256_HMAC_ALGO,!1,["sign"]);r.oncomplete=function(){r.result&&t(r.result),n(new Error("ImportKey completed without importing key."))},r.onerror=function(){n(new Error("ImportKey failed to import key."))}}))}(e).then((function(e){return(0,a.locateWindow)().msCrypto.subtle.sign(i.SHA_256_HMAC_ALGO,e)})),this.operation.catch((function(){}))):this.operation=Promise.resolve((0,a.locateWindow)().msCrypto.subtle.digest("SHA-256"))}return e.prototype.update=function(e){var t=this;(0,r.isEmptyData)(e)||(this.operation=this.operation.then((function(n){return n.onerror=function(){t.operation=Promise.reject(new Error("Error encountered updating hash"))},n.process(u(e)),n})),this.operation.catch((function(){})))},e.prototype.digest=function(){return this.operation.then((function(e){return new Promise((function(t,n){e.onerror=function(){n(new Error("Error encountered finalizing hash"))},e.oncomplete=function(){e.result&&t(new Uint8Array(e.result)),n(new Error("Error encountered finalizing hash"))},e.finish()}))}))},e}();function u(e){return"string"===typeof e?(0,o.fromUtf8)(e):ArrayBuffer.isView(e)?new Uint8Array(e.buffer,e.byteOffset,e.byteLength/Uint8Array.BYTES_PER_ELEMENT):new Uint8Array(e)}t.Sha256=s},2843:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.WebCryptoSha256=t.Ie11Sha256=void 0,(0,n(8689).__exportStar)(n(1716),t);var r=n(7591);Object.defineProperty(t,"Ie11Sha256",{enumerable:!0,get:function(){return r.Sha256}});var i=n(3722);Object.defineProperty(t,"WebCryptoSha256",{enumerable:!0,get:function(){return i.Sha256}})},9558:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.isEmptyData=void 0,t.isEmptyData=function(e){return"string"===typeof e?0===e.length:0===e.byteLength}},3722:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.Sha256=void 0;var r=n(7865),i=n(9867),o=n(3090),a=function(){function e(e){this.toHash=new Uint8Array(0),void 0!==e&&(this.key=new Promise((function(t,n){(0,o.locateWindow)().crypto.subtle.importKey("raw",(0,r.convertToBuffer)(e),i.SHA_256_HMAC_ALGO,!1,["sign"]).then(t,n)})),this.key.catch((function(){})))}return e.prototype.update=function(e){if(!(0,r.isEmptyData)(e)){var t=(0,r.convertToBuffer)(e),n=new Uint8Array(this.toHash.byteLength+t.byteLength);n.set(this.toHash,0),n.set(t,this.toHash.byteLength),this.toHash=n}},e.prototype.digest=function(){var e=this;return this.key?this.key.then((function(t){return(0,o.locateWindow)().crypto.subtle.sign(i.SHA_256_HMAC_ALGO,t,e.toHash).then((function(e){return new Uint8Array(e)}))})):(0,r.isEmptyData)(this.toHash)?Promise.resolve(i.EMPTY_DATA_SHA_256):Promise.resolve().then((function(){return(0,o.locateWindow)().crypto.subtle.digest(i.SHA_256_HASH,e.toHash)})).then((function(e){return Promise.resolve(new Uint8Array(e))}))},e}();t.Sha256=a},8689:function(e,t,n){"use strict";n.r(t),n.d(t,{__assign:function(){return o},__asyncDelegator:function(){return w},__asyncGenerator:function(){return b},__asyncValues:function(){return S},__await:function(){return y},__awaiter:function(){return l},__classPrivateFieldGet:function(){return x},__classPrivateFieldSet:function(){return C},__createBinding:function(){return d},__decorate:function(){return s},__exportStar:function(){return h},__extends:function(){return i},__generator:function(){return f},__importDefault:function(){return E},__importStar:function(){return k},__makeTemplateObject:function(){return _},__metadata:function(){return c},__param:function(){return u},__read:function(){return v},__rest:function(){return a},__spread:function(){return m},__spreadArrays:function(){return g},__values:function(){return p}});var r=function(e,t){return r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])},r(e,t)};function i(e,t){function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}var o=function(){return o=Object.assign||function(e){for(var t,n=1,r=arguments.length;n=0;s--)(i=e[s])&&(a=(o<3?i(a):o>3?i(t,n,a):i(t,n))||a);return o>3&&a&&Object.defineProperty(t,n,a),a}function u(e,t){return function(n,r){t(n,r,e)}}function c(e,t){if("object"===typeof Reflect&&"function"===typeof Reflect.metadata)return Reflect.metadata(e,t)}function l(e,t,n,r){return new(n||(n=Promise))((function(i,o){function a(e){try{u(r.next(e))}catch(t){o(t)}}function s(e){try{u(r.throw(e))}catch(t){o(t)}}function u(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(a,s)}u((r=r.apply(e,t||[])).next())}))}function f(e,t){var n,r,i,o,a={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return o={next:s(0),throw:s(1),return:s(2)},"function"===typeof Symbol&&(o[Symbol.iterator]=function(){return this}),o;function s(o){return function(s){return function(o){if(n)throw new TypeError("Generator is already executing.");for(;a;)try{if(n=1,r&&(i=2&o[0]?r.return:o[0]?r.throw||((i=r.return)&&i.call(r),0):r.next)&&!(i=i.call(r,o[1])).done)return i;switch(r=0,i&&(o=[2&o[0],i.value]),o[0]){case 0:case 1:i=o;break;case 4:return a.label++,{value:o[1],done:!1};case 5:a.label++,r=o[1],o=[0];continue;case 7:o=a.ops.pop(),a.trys.pop();continue;default:if(!(i=(i=a.trys).length>0&&i[i.length-1])&&(6===o[0]||2===o[0])){a=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]=e.length&&(e=void 0),{value:e&&e[r++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function v(e,t){var n="function"===typeof Symbol&&e[Symbol.iterator];if(!n)return e;var r,i,o=n.call(e),a=[];try{for(;(void 0===t||t-- >0)&&!(r=o.next()).done;)a.push(r.value)}catch(s){i={error:s}}finally{try{r&&!r.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}return a}function m(){for(var e=[],t=0;t1||s(e,t)}))})}function s(e,t){try{(n=i[e](t)).value instanceof y?Promise.resolve(n.value.v).then(u,c):l(o[0][2],n)}catch(r){l(o[0][3],r)}var n}function u(e){s("next",e)}function c(e){s("throw",e)}function l(e,t){e(t),o.shift(),o.length&&s(o[0][0],o[0][1])}}function w(e){var t,n;return t={},r("next"),r("throw",(function(e){throw e})),r("return"),t[Symbol.iterator]=function(){return this},t;function r(r,i){t[r]=e[r]?function(t){return(n=!n)?{value:y(e[r](t)),done:"return"===r}:i?i(t):t}:i}}function S(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t,n=e[Symbol.asyncIterator];return n?n.call(e):(e=p(e),t={},r("next"),r("throw"),r("return"),t[Symbol.asyncIterator]=function(){return this},t);function r(n){t[n]=e[n]&&function(t){return new Promise((function(r,i){(function(e,t,n,r){Promise.resolve(r).then((function(t){e({value:t,done:n})}),t)})(r,i,(t=e[n](t)).done,t.value)}))}}}function _(e,t){return Object.defineProperty?Object.defineProperty(e,"raw",{value:t}):e.raw=t,e}function k(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)Object.hasOwnProperty.call(e,n)&&(t[n]=e[n]);return t.default=e,t}function E(e){return e&&e.__esModule?e:{default:e}}function x(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)}function C(e,t,n){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,n),n}},2071:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.RawSha256=void 0;var r=n(2136),i=function(){function e(){this.state=Int32Array.from(r.INIT),this.temp=new Int32Array(64),this.buffer=new Uint8Array(64),this.bufferLength=0,this.bytesHashed=0,this.finished=!1}return e.prototype.update=function(e){if(this.finished)throw new Error("Attempted to update an already finished hash.");var t=0,n=e.byteLength;if(this.bytesHashed+=n,8*this.bytesHashed>r.MAX_HASHABLE_LENGTH)throw new Error("Cannot hash more than 2^53 - 1 bits");for(;n>0;)this.buffer[this.bufferLength++]=e[t++],n--,this.bufferLength===r.BLOCK_SIZE&&(this.hashBuffer(),this.bufferLength=0)},e.prototype.digest=function(){if(!this.finished){var e=8*this.bytesHashed,t=new DataView(this.buffer.buffer,this.buffer.byteOffset,this.buffer.byteLength),n=this.bufferLength;if(t.setUint8(this.bufferLength++,128),n%r.BLOCK_SIZE>=r.BLOCK_SIZE-8){for(var i=this.bufferLength;i>>24&255,o[4*i+1]=this.state[i]>>>16&255,o[4*i+2]=this.state[i]>>>8&255,o[4*i+3]=this.state[i]>>>0&255;return o},e.prototype.hashBuffer=function(){for(var e=this.buffer,t=this.state,n=t[0],i=t[1],o=t[2],a=t[3],s=t[4],u=t[5],c=t[6],l=t[7],f=0;f>>17|d<<15)^(d>>>19|d<<13)^d>>>10,p=((d=this.temp[f-15])>>>7|d<<25)^(d>>>18|d<<14)^d>>>3;this.temp[f]=(h+this.temp[f-7]|0)+(p+this.temp[f-16]|0)}var v=(((s>>>6|s<<26)^(s>>>11|s<<21)^(s>>>25|s<<7))+(s&u^~s&c)|0)+(l+(r.KEY[f]+this.temp[f]|0)|0)|0,m=((n>>>2|n<<30)^(n>>>13|n<<19)^(n>>>22|n<<10))+(n&i^n&o^i&o)|0;l=c,c=u,u=s,s=a+v|0,a=o,o=i,i=n,n=v+m|0}t[0]+=n,t[1]+=i,t[2]+=o,t[3]+=a,t[4]+=s,t[5]+=u,t[6]+=c,t[7]+=l},e}();t.RawSha256=i},2136:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.MAX_HASHABLE_LENGTH=t.INIT=t.KEY=t.DIGEST_LENGTH=t.BLOCK_SIZE=void 0,t.BLOCK_SIZE=64,t.DIGEST_LENGTH=32,t.KEY=new Uint32Array([1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298]),t.INIT=[1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225],t.MAX_HASHABLE_LENGTH=Math.pow(2,53)-1},9335:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),(0,n(3670).__exportStar)(n(7792),t)},7792:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.Sha256=void 0;var r=n(3670),i=n(2136),o=n(2071),a=n(7865),s=function(){function e(e){if(this.hash=new o.RawSha256,e){this.outer=new o.RawSha256;var t=function(e){var t=(0,a.convertToBuffer)(e);if(t.byteLength>i.BLOCK_SIZE){var n=new o.RawSha256;n.update(t),t=n.digest()}var r=new Uint8Array(i.BLOCK_SIZE);return r.set(t),r}(e),n=new Uint8Array(i.BLOCK_SIZE);n.set(t);for(var r=0;r=0;s--)(i=e[s])&&(a=(o<3?i(a):o>3?i(t,n,a):i(t,n))||a);return o>3&&a&&Object.defineProperty(t,n,a),a}function u(e,t){return function(n,r){t(n,r,e)}}function c(e,t){if("object"===typeof Reflect&&"function"===typeof Reflect.metadata)return Reflect.metadata(e,t)}function l(e,t,n,r){return new(n||(n=Promise))((function(i,o){function a(e){try{u(r.next(e))}catch(t){o(t)}}function s(e){try{u(r.throw(e))}catch(t){o(t)}}function u(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(a,s)}u((r=r.apply(e,t||[])).next())}))}function f(e,t){var n,r,i,o,a={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return o={next:s(0),throw:s(1),return:s(2)},"function"===typeof Symbol&&(o[Symbol.iterator]=function(){return this}),o;function s(o){return function(s){return function(o){if(n)throw new TypeError("Generator is already executing.");for(;a;)try{if(n=1,r&&(i=2&o[0]?r.return:o[0]?r.throw||((i=r.return)&&i.call(r),0):r.next)&&!(i=i.call(r,o[1])).done)return i;switch(r=0,i&&(o=[2&o[0],i.value]),o[0]){case 0:case 1:i=o;break;case 4:return a.label++,{value:o[1],done:!1};case 5:a.label++,r=o[1],o=[0];continue;case 7:o=a.ops.pop(),a.trys.pop();continue;default:if(!(i=(i=a.trys).length>0&&i[i.length-1])&&(6===o[0]||2===o[0])){a=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]=e.length&&(e=void 0),{value:e&&e[r++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function v(e,t){var n="function"===typeof Symbol&&e[Symbol.iterator];if(!n)return e;var r,i,o=n.call(e),a=[];try{for(;(void 0===t||t-- >0)&&!(r=o.next()).done;)a.push(r.value)}catch(s){i={error:s}}finally{try{r&&!r.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}return a}function m(){for(var e=[],t=0;t1||s(e,t)}))})}function s(e,t){try{(n=i[e](t)).value instanceof y?Promise.resolve(n.value.v).then(u,c):l(o[0][2],n)}catch(r){l(o[0][3],r)}var n}function u(e){s("next",e)}function c(e){s("throw",e)}function l(e,t){e(t),o.shift(),o.length&&s(o[0][0],o[0][1])}}function w(e){var t,n;return t={},r("next"),r("throw",(function(e){throw e})),r("return"),t[Symbol.iterator]=function(){return this},t;function r(r,i){t[r]=e[r]?function(t){return(n=!n)?{value:y(e[r](t)),done:"return"===r}:i?i(t):t}:i}}function S(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t,n=e[Symbol.asyncIterator];return n?n.call(e):(e=p(e),t={},r("next"),r("throw"),r("return"),t[Symbol.asyncIterator]=function(){return this},t);function r(n){t[n]=e[n]&&function(t){return new Promise((function(r,i){(function(e,t,n,r){Promise.resolve(r).then((function(t){e({value:t,done:n})}),t)})(r,i,(t=e[n](t)).done,t.value)}))}}}function _(e,t){return Object.defineProperty?Object.defineProperty(e,"raw",{value:t}):e.raw=t,e}function k(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)Object.hasOwnProperty.call(e,n)&&(t[n]=e[n]);return t.default=e,t}function E(e){return e&&e.__esModule?e:{default:e}}function x(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)}function C(e,t,n){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,n),n}},712:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),n(1049).__exportStar(n(362),t)},362:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.supportsZeroByteGCM=t.supportsSubtleCrypto=t.supportsSecureRandom=t.supportsWebCrypto=void 0;var r=n(1049),i=["decrypt","digest","encrypt","exportKey","generateKey","importKey","sign","verify"];function o(e){return"object"===typeof e&&"object"===typeof e.crypto&&"function"===typeof e.crypto.getRandomValues}function a(e){return e&&i.every((function(t){return"function"===typeof e[t]}))}t.supportsWebCrypto=function(e){return!(!o(e)||"object"!==typeof e.crypto.subtle)&&a(e.crypto.subtle)},t.supportsSecureRandom=o,t.supportsSubtleCrypto=a,t.supportsZeroByteGCM=function(e){return r.__awaiter(this,void 0,void 0,(function(){var t;return r.__generator(this,(function(n){switch(n.label){case 0:if(!a(e))return[2,!1];n.label=1;case 1:return n.trys.push([1,4,,5]),[4,e.generateKey({name:"AES-GCM",length:128},!1,["encrypt"])];case 2:return t=n.sent(),[4,e.encrypt({name:"AES-GCM",iv:new Uint8Array(Array(12)),additionalData:new Uint8Array(Array(16)),tagLength:128},t,new Uint8Array(0))];case 3:return[2,16===n.sent().byteLength];case 4:return n.sent(),[2,!1];case 5:return[2]}}))}))}},1049:function(e,t,n){"use strict";n.r(t),n.d(t,{__assign:function(){return o},__asyncDelegator:function(){return w},__asyncGenerator:function(){return b},__asyncValues:function(){return S},__await:function(){return y},__awaiter:function(){return l},__classPrivateFieldGet:function(){return x},__classPrivateFieldSet:function(){return C},__createBinding:function(){return d},__decorate:function(){return s},__exportStar:function(){return h},__extends:function(){return i},__generator:function(){return f},__importDefault:function(){return E},__importStar:function(){return k},__makeTemplateObject:function(){return _},__metadata:function(){return c},__param:function(){return u},__read:function(){return v},__rest:function(){return a},__spread:function(){return m},__spreadArrays:function(){return g},__values:function(){return p}});var r=function(e,t){return r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])},r(e,t)};function i(e,t){function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}var o=function(){return o=Object.assign||function(e){for(var t,n=1,r=arguments.length;n=0;s--)(i=e[s])&&(a=(o<3?i(a):o>3?i(t,n,a):i(t,n))||a);return o>3&&a&&Object.defineProperty(t,n,a),a}function u(e,t){return function(n,r){t(n,r,e)}}function c(e,t){if("object"===typeof Reflect&&"function"===typeof Reflect.metadata)return Reflect.metadata(e,t)}function l(e,t,n,r){return new(n||(n=Promise))((function(i,o){function a(e){try{u(r.next(e))}catch(t){o(t)}}function s(e){try{u(r.throw(e))}catch(t){o(t)}}function u(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(a,s)}u((r=r.apply(e,t||[])).next())}))}function f(e,t){var n,r,i,o,a={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return o={next:s(0),throw:s(1),return:s(2)},"function"===typeof Symbol&&(o[Symbol.iterator]=function(){return this}),o;function s(o){return function(s){return function(o){if(n)throw new TypeError("Generator is already executing.");for(;a;)try{if(n=1,r&&(i=2&o[0]?r.return:o[0]?r.throw||((i=r.return)&&i.call(r),0):r.next)&&!(i=i.call(r,o[1])).done)return i;switch(r=0,i&&(o=[2&o[0],i.value]),o[0]){case 0:case 1:i=o;break;case 4:return a.label++,{value:o[1],done:!1};case 5:a.label++,r=o[1],o=[0];continue;case 7:o=a.ops.pop(),a.trys.pop();continue;default:if(!(i=(i=a.trys).length>0&&i[i.length-1])&&(6===o[0]||2===o[0])){a=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]=e.length&&(e=void 0),{value:e&&e[r++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function v(e,t){var n="function"===typeof Symbol&&e[Symbol.iterator];if(!n)return e;var r,i,o=n.call(e),a=[];try{for(;(void 0===t||t-- >0)&&!(r=o.next()).done;)a.push(r.value)}catch(s){i={error:s}}finally{try{r&&!r.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}return a}function m(){for(var e=[],t=0;t1||s(e,t)}))})}function s(e,t){try{(n=i[e](t)).value instanceof y?Promise.resolve(n.value.v).then(u,c):l(o[0][2],n)}catch(r){l(o[0][3],r)}var n}function u(e){s("next",e)}function c(e){s("throw",e)}function l(e,t){e(t),o.shift(),o.length&&s(o[0][0],o[0][1])}}function w(e){var t,n;return t={},r("next"),r("throw",(function(e){throw e})),r("return"),t[Symbol.iterator]=function(){return this},t;function r(r,i){t[r]=e[r]?function(t){return(n=!n)?{value:y(e[r](t)),done:"return"===r}:i?i(t):t}:i}}function S(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t,n=e[Symbol.asyncIterator];return n?n.call(e):(e=p(e),t={},r("next"),r("throw"),r("return"),t[Symbol.asyncIterator]=function(){return this},t);function r(n){t[n]=e[n]&&function(t){return new Promise((function(r,i){(function(e,t,n,r){Promise.resolve(r).then((function(t){e({value:t,done:n})}),t)})(r,i,(t=e[n](t)).done,t.value)}))}}}function _(e,t){return Object.defineProperty?Object.defineProperty(e,"raw",{value:t}):e.raw=t,e}function k(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)Object.hasOwnProperty.call(e,n)&&(t[n]=e[n]);return t.default=e,t}function E(e){return e&&e.__esModule?e:{default:e}}function x(e,t){if(!t.has(e))throw new TypeError("attempted to get private field on non-instance");return t.get(e)}function C(e,t,n){if(!t.has(e))throw new TypeError("attempted to set private field on non-instance");return t.set(e,n),n}},499:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.convertToBuffer=void 0;var r=n(8071),i="undefined"!==typeof Buffer&&Buffer.from?function(e){return Buffer.from(e,"utf8")}:r.fromUtf8;t.convertToBuffer=function(e){return e instanceof Uint8Array?e:"string"===typeof e?i(e):ArrayBuffer.isView(e)?new Uint8Array(e.buffer,e.byteOffset,e.byteLength/Uint8Array.BYTES_PER_ELEMENT):new Uint8Array(e)}},7865:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.uint32ArrayFrom=t.numToUint8=t.isEmptyData=t.convertToBuffer=void 0;var r=n(499);Object.defineProperty(t,"convertToBuffer",{enumerable:!0,get:function(){return r.convertToBuffer}});var i=n(2498);Object.defineProperty(t,"isEmptyData",{enumerable:!0,get:function(){return i.isEmptyData}});var o=n(186);Object.defineProperty(t,"numToUint8",{enumerable:!0,get:function(){return o.numToUint8}});var a=n(1329);Object.defineProperty(t,"uint32ArrayFrom",{enumerable:!0,get:function(){return a.uint32ArrayFrom}})},2498:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.isEmptyData=void 0,t.isEmptyData=function(e){return"string"===typeof e?0===e.length:0===e.byteLength}},186:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.numToUint8=void 0,t.numToUint8=function(e){return new Uint8Array([(4278190080&e)>>24,(16711680&e)>>16,(65280&e)>>8,255&e])}},1329:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.uint32ArrayFrom=void 0,t.uint32ArrayFrom=function(e){if(!Array.from){for(var t=new Uint32Array(e.length);0>>((3&t)<<3)&255;return r}}},3484:function(e,t,n){var r,i,o=n(9133),a=n(372),s=0,u=0;e.exports=function(e,t,n){var c=t&&n||0,l=t||[],f=(e=e||{}).node||r,d=void 0!==e.clockseq?e.clockseq:i;if(null==f||null==d){var h=o();null==f&&(f=r=[1|h[0],h[1],h[2],h[3],h[4],h[5]]),null==d&&(d=i=16383&(h[6]<<8|h[7]))}var p=void 0!==e.msecs?e.msecs:(new Date).getTime(),v=void 0!==e.nsecs?e.nsecs:u+1,m=p-s+(v-u)/1e4;if(m<0&&void 0===e.clockseq&&(d=d+1&16383),(m<0||p>s)&&void 0===e.nsecs&&(v=0),v>=1e4)throw new Error("uuid.v1(): Can't create more than 10M uuids/sec");s=p,u=v,i=d;var g=(1e4*(268435455&(p+=122192928e5))+v)%4294967296;l[c++]=g>>>24&255,l[c++]=g>>>16&255,l[c++]=g>>>8&255,l[c++]=255&g;var y=p/4294967296*1e4&268435455;l[c++]=y>>>8&255,l[c++]=255&y,l[c++]=y>>>24&15|16,l[c++]=y>>>16&255,l[c++]=d>>>8|128,l[c++]=255&d;for(var b=0;b<6;++b)l[c+b]=f[b];return t||a(l)}},821:function(e,t,n){var r=n(9133),i=n(372);e.exports=function(e,t,n){var o=t&&n||0;"string"==typeof e&&(t="binary"===e?new Array(16):null,e=null);var a=(e=e||{}).random||(e.rng||r)();if(a[6]=15&a[6]|64,a[8]=63&a[8]|128,t)for(var s=0;s<16;++s)t[o+s]=a[s];return t||i(a)}},3090:function(e,t,n){"use strict";n.r(t),n.d(t,{locateWindow:function(){return i}});var r={};function i(){return"undefined"!==typeof window?window:"undefined"!==typeof self?self:r}},8071:function(e,t,n){"use strict";n.r(t),n.d(t,{fromUtf8:function(){return r},toUtf8:function(){return i}});var r=function(e){return"function"===typeof TextEncoder?function(e){return(new TextEncoder).encode(e)}(e):function(e){for(var t=[],n=0,r=e.length;n>6|192,63&i|128);else if(n+1>18|240,o>>12&63|128,o>>6&63|128,63&o|128)}else t.push(i>>12|224,i>>6&63|128,63&i|128)}return Uint8Array.from(t)}(e)},i=function(e){return"function"===typeof TextDecoder?function(e){return new TextDecoder("utf-8").decode(e)}(e):function(e){for(var t="",n=0,r=e.length;n0?a-4:a;for(n=0;n>16&255,c[l++]=t>>8&255,c[l++]=255&t;2===s&&(t=r[e.charCodeAt(n)]<<2|r[e.charCodeAt(n+1)]>>4,c[l++]=255&t);1===s&&(t=r[e.charCodeAt(n)]<<10|r[e.charCodeAt(n+1)]<<4|r[e.charCodeAt(n+2)]>>2,c[l++]=t>>8&255,c[l++]=255&t);return c},t.fromByteArray=function(e){for(var t,r=e.length,i=r%3,o=[],a=16383,s=0,u=r-i;su?u:s+a));1===i?(t=e[r-1],o.push(n[t>>2]+n[t<<4&63]+"==")):2===i&&(t=(e[r-2]<<8)+e[r-1],o.push(n[t>>10]+n[t>>4&63]+n[t<<2&63]+"="));return o.join("")};for(var n=[],r=[],i="undefined"!==typeof Uint8Array?Uint8Array:Array,o="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",a=0,s=o.length;a0)throw new Error("Invalid string. Length must be a multiple of 4");var n=e.indexOf("=");return-1===n&&(n=t),[n,n===t?0:4-n%4]}function c(e,t,r){for(var i,o,a=[],s=t;s>18&63]+n[o>>12&63]+n[o>>6&63]+n[63&o]);return a.join("")}r["-".charCodeAt(0)]=62,r["_".charCodeAt(0)]=63},5524:function(e){e.exports=function(e){var t={};function n(r){if(t[r])return t[r].exports;var i=t[r]={i:r,l:!1,exports:{}};return e[r].call(i.exports,i,i.exports,n),i.l=!0,i.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)n.d(r,i,function(t){return e[t]}.bind(null,i));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=90)}({17:function(e,t,n){"use strict";t.__esModule=!0,t.default=void 0;var r=n(18),i=function(){function e(){}return e.getFirstMatch=function(e,t){var n=t.match(e);return n&&n.length>0&&n[1]||""},e.getSecondMatch=function(e,t){var n=t.match(e);return n&&n.length>1&&n[2]||""},e.matchAndReturnConst=function(e,t,n){if(e.test(t))return n},e.getWindowsVersionName=function(e){switch(e){case"NT":return"NT";case"XP":case"NT 5.1":return"XP";case"NT 5.0":return"2000";case"NT 5.2":return"2003";case"NT 6.0":return"Vista";case"NT 6.1":return"7";case"NT 6.2":return"8";case"NT 6.3":return"8.1";case"NT 10.0":return"10";default:return}},e.getMacOSVersionName=function(e){var t=e.split(".").splice(0,2).map((function(e){return parseInt(e,10)||0}));if(t.push(0),10===t[0])switch(t[1]){case 5:return"Leopard";case 6:return"Snow Leopard";case 7:return"Lion";case 8:return"Mountain Lion";case 9:return"Mavericks";case 10:return"Yosemite";case 11:return"El Capitan";case 12:return"Sierra";case 13:return"High Sierra";case 14:return"Mojave";case 15:return"Catalina";default:return}},e.getAndroidVersionName=function(e){var t=e.split(".").splice(0,2).map((function(e){return parseInt(e,10)||0}));if(t.push(0),!(1===t[0]&&t[1]<5))return 1===t[0]&&t[1]<6?"Cupcake":1===t[0]&&t[1]>=6?"Donut":2===t[0]&&t[1]<2?"Eclair":2===t[0]&&2===t[1]?"Froyo":2===t[0]&&t[1]>2?"Gingerbread":3===t[0]?"Honeycomb":4===t[0]&&t[1]<1?"Ice Cream Sandwich":4===t[0]&&t[1]<4?"Jelly Bean":4===t[0]&&t[1]>=4?"KitKat":5===t[0]?"Lollipop":6===t[0]?"Marshmallow":7===t[0]?"Nougat":8===t[0]?"Oreo":9===t[0]?"Pie":void 0},e.getVersionPrecision=function(e){return e.split(".").length},e.compareVersions=function(t,n,r){void 0===r&&(r=!1);var i=e.getVersionPrecision(t),o=e.getVersionPrecision(n),a=Math.max(i,o),s=0,u=e.map([t,n],(function(t){var n=a-e.getVersionPrecision(t),r=t+new Array(n+1).join(".0");return e.map(r.split("."),(function(e){return new Array(20-e.length).join("0")+e})).reverse()}));for(r&&(s=a-Math.min(i,o)),a-=1;a>=s;){if(u[0][a]>u[1][a])return 1;if(u[0][a]===u[1][a]){if(a===s)return 0;a-=1}else if(u[0][a]1?i-1:0),a=1;a0){var a=Object.keys(n),u=s.default.find(a,(function(e){return t.isOS(e)}));if(u){var c=this.satisfies(n[u]);if(void 0!==c)return c}var l=s.default.find(a,(function(e){return t.isPlatform(e)}));if(l){var f=this.satisfies(n[l]);if(void 0!==f)return f}}if(o>0){var d=Object.keys(i),h=s.default.find(d,(function(e){return t.isBrowser(e,!0)}));if(void 0!==h)return this.compareVersion(i[h])}},t.isBrowser=function(e,t){void 0===t&&(t=!1);var n=this.getBrowserName().toLowerCase(),r=e.toLowerCase(),i=s.default.getBrowserTypeByAlias(r);return t&&i&&(r=i.toLowerCase()),r===n},t.compareVersion=function(e){var t=[0],n=e,r=!1,i=this.getBrowserVersion();if("string"==typeof i)return">"===e[0]||"<"===e[0]?(n=e.substr(1),"="===e[1]?(r=!0,n=e.substr(2)):t=[],">"===e[0]?t.push(1):t.push(-1)):"="===e[0]?n=e.substr(1):"~"===e[0]&&(r=!0,n=e.substr(1)),t.indexOf(s.default.compareVersions(i,n,r))>-1},t.isOS=function(e){return this.getOSName(!0)===String(e).toLowerCase()},t.isPlatform=function(e){return this.getPlatformType(!0)===String(e).toLowerCase()},t.isEngine=function(e){return this.getEngineName(!0)===String(e).toLowerCase()},t.is=function(e,t){return void 0===t&&(t=!1),this.isBrowser(e,t)||this.isOS(e)||this.isPlatform(e)},t.some=function(e){var t=this;return void 0===e&&(e=[]),e.some((function(e){return t.is(e)}))},e}();t.default=c,e.exports=t.default},92:function(e,t,n){"use strict";t.__esModule=!0,t.default=void 0;var r,i=(r=n(17))&&r.__esModule?r:{default:r},o=/version\/(\d+(\.?_?\d+)+)/i,a=[{test:[/googlebot/i],describe:function(e){var t={name:"Googlebot"},n=i.default.getFirstMatch(/googlebot\/(\d+(\.\d+))/i,e)||i.default.getFirstMatch(o,e);return n&&(t.version=n),t}},{test:[/opera/i],describe:function(e){var t={name:"Opera"},n=i.default.getFirstMatch(o,e)||i.default.getFirstMatch(/(?:opera)[\s/](\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/opr\/|opios/i],describe:function(e){var t={name:"Opera"},n=i.default.getFirstMatch(/(?:opr|opios)[\s/](\S+)/i,e)||i.default.getFirstMatch(o,e);return n&&(t.version=n),t}},{test:[/SamsungBrowser/i],describe:function(e){var t={name:"Samsung Internet for Android"},n=i.default.getFirstMatch(o,e)||i.default.getFirstMatch(/(?:SamsungBrowser)[\s/](\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/Whale/i],describe:function(e){var t={name:"NAVER Whale Browser"},n=i.default.getFirstMatch(o,e)||i.default.getFirstMatch(/(?:whale)[\s/](\d+(?:\.\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/MZBrowser/i],describe:function(e){var t={name:"MZ Browser"},n=i.default.getFirstMatch(/(?:MZBrowser)[\s/](\d+(?:\.\d+)+)/i,e)||i.default.getFirstMatch(o,e);return n&&(t.version=n),t}},{test:[/focus/i],describe:function(e){var t={name:"Focus"},n=i.default.getFirstMatch(/(?:focus)[\s/](\d+(?:\.\d+)+)/i,e)||i.default.getFirstMatch(o,e);return n&&(t.version=n),t}},{test:[/swing/i],describe:function(e){var t={name:"Swing"},n=i.default.getFirstMatch(/(?:swing)[\s/](\d+(?:\.\d+)+)/i,e)||i.default.getFirstMatch(o,e);return n&&(t.version=n),t}},{test:[/coast/i],describe:function(e){var t={name:"Opera Coast"},n=i.default.getFirstMatch(o,e)||i.default.getFirstMatch(/(?:coast)[\s/](\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/opt\/\d+(?:.?_?\d+)+/i],describe:function(e){var t={name:"Opera Touch"},n=i.default.getFirstMatch(/(?:opt)[\s/](\d+(\.?_?\d+)+)/i,e)||i.default.getFirstMatch(o,e);return n&&(t.version=n),t}},{test:[/yabrowser/i],describe:function(e){var t={name:"Yandex Browser"},n=i.default.getFirstMatch(/(?:yabrowser)[\s/](\d+(\.?_?\d+)+)/i,e)||i.default.getFirstMatch(o,e);return n&&(t.version=n),t}},{test:[/ucbrowser/i],describe:function(e){var t={name:"UC Browser"},n=i.default.getFirstMatch(o,e)||i.default.getFirstMatch(/(?:ucbrowser)[\s/](\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/Maxthon|mxios/i],describe:function(e){var t={name:"Maxthon"},n=i.default.getFirstMatch(o,e)||i.default.getFirstMatch(/(?:Maxthon|mxios)[\s/](\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/epiphany/i],describe:function(e){var t={name:"Epiphany"},n=i.default.getFirstMatch(o,e)||i.default.getFirstMatch(/(?:epiphany)[\s/](\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/puffin/i],describe:function(e){var t={name:"Puffin"},n=i.default.getFirstMatch(o,e)||i.default.getFirstMatch(/(?:puffin)[\s/](\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/sleipnir/i],describe:function(e){var t={name:"Sleipnir"},n=i.default.getFirstMatch(o,e)||i.default.getFirstMatch(/(?:sleipnir)[\s/](\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/k-meleon/i],describe:function(e){var t={name:"K-Meleon"},n=i.default.getFirstMatch(o,e)||i.default.getFirstMatch(/(?:k-meleon)[\s/](\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/micromessenger/i],describe:function(e){var t={name:"WeChat"},n=i.default.getFirstMatch(/(?:micromessenger)[\s/](\d+(\.?_?\d+)+)/i,e)||i.default.getFirstMatch(o,e);return n&&(t.version=n),t}},{test:[/qqbrowser/i],describe:function(e){var t={name:/qqbrowserlite/i.test(e)?"QQ Browser Lite":"QQ Browser"},n=i.default.getFirstMatch(/(?:qqbrowserlite|qqbrowser)[/](\d+(\.?_?\d+)+)/i,e)||i.default.getFirstMatch(o,e);return n&&(t.version=n),t}},{test:[/msie|trident/i],describe:function(e){var t={name:"Internet Explorer"},n=i.default.getFirstMatch(/(?:msie |rv:)(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/\sedg\//i],describe:function(e){var t={name:"Microsoft Edge"},n=i.default.getFirstMatch(/\sedg\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/edg([ea]|ios)/i],describe:function(e){var t={name:"Microsoft Edge"},n=i.default.getSecondMatch(/edg([ea]|ios)\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/vivaldi/i],describe:function(e){var t={name:"Vivaldi"},n=i.default.getFirstMatch(/vivaldi\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/seamonkey/i],describe:function(e){var t={name:"SeaMonkey"},n=i.default.getFirstMatch(/seamonkey\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/sailfish/i],describe:function(e){var t={name:"Sailfish"},n=i.default.getFirstMatch(/sailfish\s?browser\/(\d+(\.\d+)?)/i,e);return n&&(t.version=n),t}},{test:[/silk/i],describe:function(e){var t={name:"Amazon Silk"},n=i.default.getFirstMatch(/silk\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/phantom/i],describe:function(e){var t={name:"PhantomJS"},n=i.default.getFirstMatch(/phantomjs\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/slimerjs/i],describe:function(e){var t={name:"SlimerJS"},n=i.default.getFirstMatch(/slimerjs\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/blackberry|\bbb\d+/i,/rim\stablet/i],describe:function(e){var t={name:"BlackBerry"},n=i.default.getFirstMatch(o,e)||i.default.getFirstMatch(/blackberry[\d]+\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/(web|hpw)[o0]s/i],describe:function(e){var t={name:"WebOS Browser"},n=i.default.getFirstMatch(o,e)||i.default.getFirstMatch(/w(?:eb)?[o0]sbrowser\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/bada/i],describe:function(e){var t={name:"Bada"},n=i.default.getFirstMatch(/dolfin\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/tizen/i],describe:function(e){var t={name:"Tizen"},n=i.default.getFirstMatch(/(?:tizen\s?)?browser\/(\d+(\.?_?\d+)+)/i,e)||i.default.getFirstMatch(o,e);return n&&(t.version=n),t}},{test:[/qupzilla/i],describe:function(e){var t={name:"QupZilla"},n=i.default.getFirstMatch(/(?:qupzilla)[\s/](\d+(\.?_?\d+)+)/i,e)||i.default.getFirstMatch(o,e);return n&&(t.version=n),t}},{test:[/firefox|iceweasel|fxios/i],describe:function(e){var t={name:"Firefox"},n=i.default.getFirstMatch(/(?:firefox|iceweasel|fxios)[\s/](\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/electron/i],describe:function(e){var t={name:"Electron"},n=i.default.getFirstMatch(/(?:electron)\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/MiuiBrowser/i],describe:function(e){var t={name:"Miui"},n=i.default.getFirstMatch(/(?:MiuiBrowser)[\s/](\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/chromium/i],describe:function(e){var t={name:"Chromium"},n=i.default.getFirstMatch(/(?:chromium)[\s/](\d+(\.?_?\d+)+)/i,e)||i.default.getFirstMatch(o,e);return n&&(t.version=n),t}},{test:[/chrome|crios|crmo/i],describe:function(e){var t={name:"Chrome"},n=i.default.getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/GSA/i],describe:function(e){var t={name:"Google Search"},n=i.default.getFirstMatch(/(?:GSA)\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:function(e){var t=!e.test(/like android/i),n=e.test(/android/i);return t&&n},describe:function(e){var t={name:"Android Browser"},n=i.default.getFirstMatch(o,e);return n&&(t.version=n),t}},{test:[/playstation 4/i],describe:function(e){var t={name:"PlayStation 4"},n=i.default.getFirstMatch(o,e);return n&&(t.version=n),t}},{test:[/safari|applewebkit/i],describe:function(e){var t={name:"Safari"},n=i.default.getFirstMatch(o,e);return n&&(t.version=n),t}},{test:[/.*/i],describe:function(e){var t=-1!==e.search("\\(")?/^(.*)\/(.*)[ \t]\((.*)/:/^(.*)\/(.*) /;return{name:i.default.getFirstMatch(t,e),version:i.default.getSecondMatch(t,e)}}}];t.default=a,e.exports=t.default},93:function(e,t,n){"use strict";t.__esModule=!0,t.default=void 0;var r,i=(r=n(17))&&r.__esModule?r:{default:r},o=n(18),a=[{test:[/Roku\/DVP/],describe:function(e){var t=i.default.getFirstMatch(/Roku\/DVP-(\d+\.\d+)/i,e);return{name:o.OS_MAP.Roku,version:t}}},{test:[/windows phone/i],describe:function(e){var t=i.default.getFirstMatch(/windows phone (?:os)?\s?(\d+(\.\d+)*)/i,e);return{name:o.OS_MAP.WindowsPhone,version:t}}},{test:[/windows /i],describe:function(e){var t=i.default.getFirstMatch(/Windows ((NT|XP)( \d\d?.\d)?)/i,e),n=i.default.getWindowsVersionName(t);return{name:o.OS_MAP.Windows,version:t,versionName:n}}},{test:[/Macintosh(.*?) FxiOS(.*?)\//],describe:function(e){var t={name:o.OS_MAP.iOS},n=i.default.getSecondMatch(/(Version\/)(\d[\d.]+)/,e);return n&&(t.version=n),t}},{test:[/macintosh/i],describe:function(e){var t=i.default.getFirstMatch(/mac os x (\d+(\.?_?\d+)+)/i,e).replace(/[_\s]/g,"."),n=i.default.getMacOSVersionName(t),r={name:o.OS_MAP.MacOS,version:t};return n&&(r.versionName=n),r}},{test:[/(ipod|iphone|ipad)/i],describe:function(e){var t=i.default.getFirstMatch(/os (\d+([_\s]\d+)*) like mac os x/i,e).replace(/[_\s]/g,".");return{name:o.OS_MAP.iOS,version:t}}},{test:function(e){var t=!e.test(/like android/i),n=e.test(/android/i);return t&&n},describe:function(e){var t=i.default.getFirstMatch(/android[\s/-](\d+(\.\d+)*)/i,e),n=i.default.getAndroidVersionName(t),r={name:o.OS_MAP.Android,version:t};return n&&(r.versionName=n),r}},{test:[/(web|hpw)[o0]s/i],describe:function(e){var t=i.default.getFirstMatch(/(?:web|hpw)[o0]s\/(\d+(\.\d+)*)/i,e),n={name:o.OS_MAP.WebOS};return t&&t.length&&(n.version=t),n}},{test:[/blackberry|\bbb\d+/i,/rim\stablet/i],describe:function(e){var t=i.default.getFirstMatch(/rim\stablet\sos\s(\d+(\.\d+)*)/i,e)||i.default.getFirstMatch(/blackberry\d+\/(\d+([_\s]\d+)*)/i,e)||i.default.getFirstMatch(/\bbb(\d+)/i,e);return{name:o.OS_MAP.BlackBerry,version:t}}},{test:[/bada/i],describe:function(e){var t=i.default.getFirstMatch(/bada\/(\d+(\.\d+)*)/i,e);return{name:o.OS_MAP.Bada,version:t}}},{test:[/tizen/i],describe:function(e){var t=i.default.getFirstMatch(/tizen[/\s](\d+(\.\d+)*)/i,e);return{name:o.OS_MAP.Tizen,version:t}}},{test:[/linux/i],describe:function(){return{name:o.OS_MAP.Linux}}},{test:[/CrOS/],describe:function(){return{name:o.OS_MAP.ChromeOS}}},{test:[/PlayStation 4/],describe:function(e){var t=i.default.getFirstMatch(/PlayStation 4[/\s](\d+(\.\d+)*)/i,e);return{name:o.OS_MAP.PlayStation4,version:t}}}];t.default=a,e.exports=t.default},94:function(e,t,n){"use strict";t.__esModule=!0,t.default=void 0;var r,i=(r=n(17))&&r.__esModule?r:{default:r},o=n(18),a=[{test:[/googlebot/i],describe:function(){return{type:"bot",vendor:"Google"}}},{test:[/huawei/i],describe:function(e){var t=i.default.getFirstMatch(/(can-l01)/i,e)&&"Nova",n={type:o.PLATFORMS_MAP.mobile,vendor:"Huawei"};return t&&(n.model=t),n}},{test:[/nexus\s*(?:7|8|9|10).*/i],describe:function(){return{type:o.PLATFORMS_MAP.tablet,vendor:"Nexus"}}},{test:[/ipad/i],describe:function(){return{type:o.PLATFORMS_MAP.tablet,vendor:"Apple",model:"iPad"}}},{test:[/Macintosh(.*?) FxiOS(.*?)\//],describe:function(){return{type:o.PLATFORMS_MAP.tablet,vendor:"Apple",model:"iPad"}}},{test:[/kftt build/i],describe:function(){return{type:o.PLATFORMS_MAP.tablet,vendor:"Amazon",model:"Kindle Fire HD 7"}}},{test:[/silk/i],describe:function(){return{type:o.PLATFORMS_MAP.tablet,vendor:"Amazon"}}},{test:[/tablet(?! pc)/i],describe:function(){return{type:o.PLATFORMS_MAP.tablet}}},{test:function(e){var t=e.test(/ipod|iphone/i),n=e.test(/like (ipod|iphone)/i);return t&&!n},describe:function(e){var t=i.default.getFirstMatch(/(ipod|iphone)/i,e);return{type:o.PLATFORMS_MAP.mobile,vendor:"Apple",model:t}}},{test:[/nexus\s*[0-6].*/i,/galaxy nexus/i],describe:function(){return{type:o.PLATFORMS_MAP.mobile,vendor:"Nexus"}}},{test:[/[^-]mobi/i],describe:function(){return{type:o.PLATFORMS_MAP.mobile}}},{test:function(e){return"blackberry"===e.getBrowserName(!0)},describe:function(){return{type:o.PLATFORMS_MAP.mobile,vendor:"BlackBerry"}}},{test:function(e){return"bada"===e.getBrowserName(!0)},describe:function(){return{type:o.PLATFORMS_MAP.mobile}}},{test:function(e){return"windows phone"===e.getBrowserName()},describe:function(){return{type:o.PLATFORMS_MAP.mobile,vendor:"Microsoft"}}},{test:function(e){var t=Number(String(e.getOSVersion()).split(".")[0]);return"android"===e.getOSName(!0)&&t>=3},describe:function(){return{type:o.PLATFORMS_MAP.tablet}}},{test:function(e){return"android"===e.getOSName(!0)},describe:function(){return{type:o.PLATFORMS_MAP.mobile}}},{test:function(e){return"macos"===e.getOSName(!0)},describe:function(){return{type:o.PLATFORMS_MAP.desktop,vendor:"Apple"}}},{test:function(e){return"windows"===e.getOSName(!0)},describe:function(){return{type:o.PLATFORMS_MAP.desktop}}},{test:function(e){return"linux"===e.getOSName(!0)},describe:function(){return{type:o.PLATFORMS_MAP.desktop}}},{test:function(e){return"playstation 4"===e.getOSName(!0)},describe:function(){return{type:o.PLATFORMS_MAP.tv}}},{test:function(e){return"roku"===e.getOSName(!0)},describe:function(){return{type:o.PLATFORMS_MAP.tv}}}];t.default=a,e.exports=t.default},95:function(e,t,n){"use strict";t.__esModule=!0,t.default=void 0;var r,i=(r=n(17))&&r.__esModule?r:{default:r},o=n(18),a=[{test:function(e){return"microsoft edge"===e.getBrowserName(!0)},describe:function(e){if(/\sedg\//i.test(e))return{name:o.ENGINE_MAP.Blink};var t=i.default.getFirstMatch(/edge\/(\d+(\.?_?\d+)+)/i,e);return{name:o.ENGINE_MAP.EdgeHTML,version:t}}},{test:[/trident/i],describe:function(e){var t={name:o.ENGINE_MAP.Trident},n=i.default.getFirstMatch(/trident\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:function(e){return e.test(/presto/i)},describe:function(e){var t={name:o.ENGINE_MAP.Presto},n=i.default.getFirstMatch(/presto\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:function(e){var t=e.test(/gecko/i),n=e.test(/like gecko/i);return t&&!n},describe:function(e){var t={name:o.ENGINE_MAP.Gecko},n=i.default.getFirstMatch(/gecko\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/(apple)?webkit\/537\.36/i],describe:function(){return{name:o.ENGINE_MAP.Blink}}},{test:[/(apple)?webkit/i],describe:function(e){var t={name:o.ENGINE_MAP.WebKit},n=i.default.getFirstMatch(/webkit\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}}];t.default=a,e.exports=t.default}})},9778:function(e,t,n){"use strict";var r=n(2009),i=n(4038),o=n(1571);function a(){return u.TYPED_ARRAY_SUPPORT?2147483647:1073741823}function s(e,t){if(a()=a())throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+a().toString(16)+" bytes");return 0|e}function p(e,t){if(u.isBuffer(e))return e.length;if("undefined"!==typeof ArrayBuffer&&"function"===typeof ArrayBuffer.isView&&(ArrayBuffer.isView(e)||e instanceof ArrayBuffer))return e.byteLength;"string"!==typeof e&&(e=""+e);var n=e.length;if(0===n)return 0;for(var r=!1;;)switch(t){case"ascii":case"latin1":case"binary":return n;case"utf8":case"utf-8":case void 0:return B(e).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*n;case"hex":return n>>>1;case"base64":return V(e).length;default:if(r)return B(e).length;t=(""+t).toLowerCase(),r=!0}}function v(e,t,n){var r=!1;if((void 0===t||t<0)&&(t=0),t>this.length)return"";if((void 0===n||n>this.length)&&(n=this.length),n<=0)return"";if((n>>>=0)<=(t>>>=0))return"";for(e||(e="utf8");;)switch(e){case"hex":return T(this,t,n);case"utf8":case"utf-8":return C(this,t,n);case"ascii":return A(this,t,n);case"latin1":case"binary":return P(this,t,n);case"base64":return x(this,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return I(this,t,n);default:if(r)throw new TypeError("Unknown encoding: "+e);e=(e+"").toLowerCase(),r=!0}}function m(e,t,n){var r=e[t];e[t]=e[n],e[n]=r}function g(e,t,n,r,i){if(0===e.length)return-1;if("string"===typeof n?(r=n,n=0):n>2147483647?n=2147483647:n<-2147483648&&(n=-2147483648),n=+n,isNaN(n)&&(n=i?0:e.length-1),n<0&&(n=e.length+n),n>=e.length){if(i)return-1;n=e.length-1}else if(n<0){if(!i)return-1;n=0}if("string"===typeof t&&(t=u.from(t,r)),u.isBuffer(t))return 0===t.length?-1:y(e,t,n,r,i);if("number"===typeof t)return t&=255,u.TYPED_ARRAY_SUPPORT&&"function"===typeof Uint8Array.prototype.indexOf?i?Uint8Array.prototype.indexOf.call(e,t,n):Uint8Array.prototype.lastIndexOf.call(e,t,n):y(e,[t],n,r,i);throw new TypeError("val must be string, number or Buffer")}function y(e,t,n,r,i){var o,a=1,s=e.length,u=t.length;if(void 0!==r&&("ucs2"===(r=String(r).toLowerCase())||"ucs-2"===r||"utf16le"===r||"utf-16le"===r)){if(e.length<2||t.length<2)return-1;a=2,s/=2,u/=2,n/=2}function c(e,t){return 1===a?e[t]:e.readUInt16BE(t*a)}if(i){var l=-1;for(o=n;os&&(n=s-u),o=n;o>=0;o--){for(var f=!0,d=0;di&&(r=i):r=i;var o=t.length;if(o%2!==0)throw new TypeError("Invalid hex string");r>o/2&&(r=o/2);for(var a=0;a>8,i=n%256,o.push(i),o.push(r);return o}(t,e.length-n),e,n,r)}function x(e,t,n){return 0===t&&n===e.length?r.fromByteArray(e):r.fromByteArray(e.slice(t,n))}function C(e,t,n){n=Math.min(e.length,n);for(var r=[],i=t;i239?4:c>223?3:c>191?2:1;if(i+f<=n)switch(f){case 1:c<128&&(l=c);break;case 2:128===(192&(o=e[i+1]))&&(u=(31&c)<<6|63&o)>127&&(l=u);break;case 3:o=e[i+1],a=e[i+2],128===(192&o)&&128===(192&a)&&(u=(15&c)<<12|(63&o)<<6|63&a)>2047&&(u<55296||u>57343)&&(l=u);break;case 4:o=e[i+1],a=e[i+2],s=e[i+3],128===(192&o)&&128===(192&a)&&128===(192&s)&&(u=(15&c)<<18|(63&o)<<12|(63&a)<<6|63&s)>65535&&u<1114112&&(l=u)}null===l?(l=65533,f=1):l>65535&&(l-=65536,r.push(l>>>10&1023|55296),l=56320|1023&l),r.push(l),i+=f}return function(e){var t=e.length;if(t<=O)return String.fromCharCode.apply(String,e);var n="",r=0;for(;r0&&(e=this.toString("hex",0,n).match(/.{2}/g).join(" "),this.length>n&&(e+=" ... ")),""},u.prototype.compare=function(e,t,n,r,i){if(!u.isBuffer(e))throw new TypeError("Argument must be a Buffer");if(void 0===t&&(t=0),void 0===n&&(n=e?e.length:0),void 0===r&&(r=0),void 0===i&&(i=this.length),t<0||n>e.length||r<0||i>this.length)throw new RangeError("out of range index");if(r>=i&&t>=n)return 0;if(r>=i)return-1;if(t>=n)return 1;if(this===e)return 0;for(var o=(i>>>=0)-(r>>>=0),a=(n>>>=0)-(t>>>=0),s=Math.min(o,a),c=this.slice(r,i),l=e.slice(t,n),f=0;fi)&&(n=i),e.length>0&&(n<0||t<0)||t>this.length)throw new RangeError("Attempt to write outside buffer bounds");r||(r="utf8");for(var o=!1;;)switch(r){case"hex":return b(this,e,t,n);case"utf8":case"utf-8":return w(this,e,t,n);case"ascii":return S(this,e,t,n);case"latin1":case"binary":return _(this,e,t,n);case"base64":return k(this,e,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return E(this,e,t,n);default:if(o)throw new TypeError("Unknown encoding: "+r);r=(""+r).toLowerCase(),o=!0}},u.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};var O=4096;function A(e,t,n){var r="";n=Math.min(e.length,n);for(var i=t;ir)&&(n=r);for(var i="",o=t;on)throw new RangeError("Trying to access beyond buffer length")}function j(e,t,n,r,i,o){if(!u.isBuffer(e))throw new TypeError('"buffer" argument must be a Buffer instance');if(t>i||te.length)throw new RangeError("Index out of range")}function M(e,t,n,r){t<0&&(t=65535+t+1);for(var i=0,o=Math.min(e.length-n,2);i>>8*(r?i:1-i)}function R(e,t,n,r){t<0&&(t=4294967295+t+1);for(var i=0,o=Math.min(e.length-n,4);i>>8*(r?i:3-i)&255}function U(e,t,n,r,i,o){if(n+r>e.length)throw new RangeError("Index out of range");if(n<0)throw new RangeError("Index out of range")}function D(e,t,n,r,o){return o||U(e,0,n,4),i.write(e,t,n,r,23,4),n+4}function F(e,t,n,r,o){return o||U(e,0,n,8),i.write(e,t,n,r,52,8),n+8}u.prototype.slice=function(e,t){var n,r=this.length;if((e=~~e)<0?(e+=r)<0&&(e=0):e>r&&(e=r),(t=void 0===t?r:~~t)<0?(t+=r)<0&&(t=0):t>r&&(t=r),t0&&(i*=256);)r+=this[e+--t]*i;return r},u.prototype.readUInt8=function(e,t){return t||N(e,1,this.length),this[e]},u.prototype.readUInt16LE=function(e,t){return t||N(e,2,this.length),this[e]|this[e+1]<<8},u.prototype.readUInt16BE=function(e,t){return t||N(e,2,this.length),this[e]<<8|this[e+1]},u.prototype.readUInt32LE=function(e,t){return t||N(e,4,this.length),(this[e]|this[e+1]<<8|this[e+2]<<16)+16777216*this[e+3]},u.prototype.readUInt32BE=function(e,t){return t||N(e,4,this.length),16777216*this[e]+(this[e+1]<<16|this[e+2]<<8|this[e+3])},u.prototype.readIntLE=function(e,t,n){e|=0,t|=0,n||N(e,t,this.length);for(var r=this[e],i=1,o=0;++o=(i*=128)&&(r-=Math.pow(2,8*t)),r},u.prototype.readIntBE=function(e,t,n){e|=0,t|=0,n||N(e,t,this.length);for(var r=t,i=1,o=this[e+--r];r>0&&(i*=256);)o+=this[e+--r]*i;return o>=(i*=128)&&(o-=Math.pow(2,8*t)),o},u.prototype.readInt8=function(e,t){return t||N(e,1,this.length),128&this[e]?-1*(255-this[e]+1):this[e]},u.prototype.readInt16LE=function(e,t){t||N(e,2,this.length);var n=this[e]|this[e+1]<<8;return 32768&n?4294901760|n:n},u.prototype.readInt16BE=function(e,t){t||N(e,2,this.length);var n=this[e+1]|this[e]<<8;return 32768&n?4294901760|n:n},u.prototype.readInt32LE=function(e,t){return t||N(e,4,this.length),this[e]|this[e+1]<<8|this[e+2]<<16|this[e+3]<<24},u.prototype.readInt32BE=function(e,t){return t||N(e,4,this.length),this[e]<<24|this[e+1]<<16|this[e+2]<<8|this[e+3]},u.prototype.readFloatLE=function(e,t){return t||N(e,4,this.length),i.read(this,e,!0,23,4)},u.prototype.readFloatBE=function(e,t){return t||N(e,4,this.length),i.read(this,e,!1,23,4)},u.prototype.readDoubleLE=function(e,t){return t||N(e,8,this.length),i.read(this,e,!0,52,8)},u.prototype.readDoubleBE=function(e,t){return t||N(e,8,this.length),i.read(this,e,!1,52,8)},u.prototype.writeUIntLE=function(e,t,n,r){(e=+e,t|=0,n|=0,r)||j(this,e,t,n,Math.pow(2,8*n)-1,0);var i=1,o=0;for(this[t]=255&e;++o=0&&(o*=256);)this[t+i]=e/o&255;return t+n},u.prototype.writeUInt8=function(e,t,n){return e=+e,t|=0,n||j(this,e,t,1,255,0),u.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),this[t]=255&e,t+1},u.prototype.writeUInt16LE=function(e,t,n){return e=+e,t|=0,n||j(this,e,t,2,65535,0),u.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):M(this,e,t,!0),t+2},u.prototype.writeUInt16BE=function(e,t,n){return e=+e,t|=0,n||j(this,e,t,2,65535,0),u.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):M(this,e,t,!1),t+2},u.prototype.writeUInt32LE=function(e,t,n){return e=+e,t|=0,n||j(this,e,t,4,4294967295,0),u.TYPED_ARRAY_SUPPORT?(this[t+3]=e>>>24,this[t+2]=e>>>16,this[t+1]=e>>>8,this[t]=255&e):R(this,e,t,!0),t+4},u.prototype.writeUInt32BE=function(e,t,n){return e=+e,t|=0,n||j(this,e,t,4,4294967295,0),u.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):R(this,e,t,!1),t+4},u.prototype.writeIntLE=function(e,t,n,r){if(e=+e,t|=0,!r){var i=Math.pow(2,8*n-1);j(this,e,t,n,i-1,-i)}var o=0,a=1,s=0;for(this[t]=255&e;++o>0)-s&255;return t+n},u.prototype.writeIntBE=function(e,t,n,r){if(e=+e,t|=0,!r){var i=Math.pow(2,8*n-1);j(this,e,t,n,i-1,-i)}var o=n-1,a=1,s=0;for(this[t+o]=255&e;--o>=0&&(a*=256);)e<0&&0===s&&0!==this[t+o+1]&&(s=1),this[t+o]=(e/a>>0)-s&255;return t+n},u.prototype.writeInt8=function(e,t,n){return e=+e,t|=0,n||j(this,e,t,1,127,-128),u.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),e<0&&(e=255+e+1),this[t]=255&e,t+1},u.prototype.writeInt16LE=function(e,t,n){return e=+e,t|=0,n||j(this,e,t,2,32767,-32768),u.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):M(this,e,t,!0),t+2},u.prototype.writeInt16BE=function(e,t,n){return e=+e,t|=0,n||j(this,e,t,2,32767,-32768),u.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):M(this,e,t,!1),t+2},u.prototype.writeInt32LE=function(e,t,n){return e=+e,t|=0,n||j(this,e,t,4,2147483647,-2147483648),u.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8,this[t+2]=e>>>16,this[t+3]=e>>>24):R(this,e,t,!0),t+4},u.prototype.writeInt32BE=function(e,t,n){return e=+e,t|=0,n||j(this,e,t,4,2147483647,-2147483648),e<0&&(e=4294967295+e+1),u.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):R(this,e,t,!1),t+4},u.prototype.writeFloatLE=function(e,t,n){return D(this,e,t,!0,n)},u.prototype.writeFloatBE=function(e,t,n){return D(this,e,t,!1,n)},u.prototype.writeDoubleLE=function(e,t,n){return F(this,e,t,!0,n)},u.prototype.writeDoubleBE=function(e,t,n){return F(this,e,t,!1,n)},u.prototype.copy=function(e,t,n,r){if(n||(n=0),r||0===r||(r=this.length),t>=e.length&&(t=e.length),t||(t=0),r>0&&r=this.length)throw new RangeError("sourceStart out of bounds");if(r<0)throw new RangeError("sourceEnd out of bounds");r>this.length&&(r=this.length),e.length-t=0;--i)e[i+t]=this[i+n];else if(o<1e3||!u.TYPED_ARRAY_SUPPORT)for(i=0;i>>=0,n=void 0===n?this.length:n>>>0,e||(e=0),"number"===typeof e)for(o=t;o55295&&n<57344){if(!i){if(n>56319){(t-=3)>-1&&o.push(239,191,189);continue}if(a+1===r){(t-=3)>-1&&o.push(239,191,189);continue}i=n;continue}if(n<56320){(t-=3)>-1&&o.push(239,191,189),i=n;continue}n=65536+(i-55296<<10|n-56320)}else i&&(t-=3)>-1&&o.push(239,191,189);if(i=null,n<128){if((t-=1)<0)break;o.push(n)}else if(n<2048){if((t-=2)<0)break;o.push(n>>6|192,63&n|128)}else if(n<65536){if((t-=3)<0)break;o.push(n>>12|224,n>>6&63|128,63&n|128)}else{if(!(n<1114112))throw new Error("Invalid code point");if((t-=4)<0)break;o.push(n>>18|240,n>>12&63|128,n>>6&63|128,63&n|128)}}return o}function V(e){return r.toByteArray(function(e){if((e=function(e){return e.trim?e.trim():e.replace(/^\s+|\s+$/g,"")}(e).replace(L,"")).length<2)return"";for(;e.length%4!==0;)e+="=";return e}(e))}function H(e,t,n,r){for(var i=0;i=t.length||i>=e.length);++i)t[i+n]=e[i];return i}},4038:function(e,t){t.read=function(e,t,n,r,i){var o,a,s=8*i-r-1,u=(1<>1,l=-7,f=n?i-1:0,d=n?-1:1,h=e[t+f];for(f+=d,o=h&(1<<-l)-1,h>>=-l,l+=s;l>0;o=256*o+e[t+f],f+=d,l-=8);for(a=o&(1<<-l)-1,o>>=-l,l+=r;l>0;a=256*a+e[t+f],f+=d,l-=8);if(0===o)o=1-c;else{if(o===u)return a?NaN:1/0*(h?-1:1);a+=Math.pow(2,r),o-=c}return(h?-1:1)*a*Math.pow(2,o-r)},t.write=function(e,t,n,r,i,o){var a,s,u,c=8*o-i-1,l=(1<>1,d=23===i?Math.pow(2,-24)-Math.pow(2,-77):0,h=r?0:o-1,p=r?1:-1,v=t<0||0===t&&1/t<0?1:0;for(t=Math.abs(t),isNaN(t)||t===1/0?(s=isNaN(t)?1:0,a=l):(a=Math.floor(Math.log(t)/Math.LN2),t*(u=Math.pow(2,-a))<1&&(a--,u*=2),(t+=a+f>=1?d/u:d*Math.pow(2,1-f))*u>=2&&(a++,u/=2),a+f>=l?(s=0,a=l):a+f>=1?(s=(t*u-1)*Math.pow(2,i),a+=f):(s=t*Math.pow(2,f-1)*Math.pow(2,i),a=0));i>=8;e[n+h]=255&s,h+=p,s/=256,i-=8);for(a=a<0;e[n+h]=255&a,h+=p,a/=256,c-=8);e[n+h-p]|=128*v}},1571:function(e){var t={}.toString;e.exports=Array.isArray||function(e){return"[object Array]"==t.call(e)}},2558:function(e,t,n){e.exports=self.fetch||(self.fetch=n(4175).default||n(4175))},1426:function(e){var t,n,r=e.exports={};function i(){throw new Error("setTimeout has not been defined")}function o(){throw new Error("clearTimeout has not been defined")}function a(e){if(t===setTimeout)return setTimeout(e,0);if((t===i||!t)&&setTimeout)return t=setTimeout,setTimeout(e,0);try{return t(e,0)}catch(n){try{return t.call(null,e,0)}catch(n){return t.call(this,e,0)}}}!function(){try{t="function"===typeof setTimeout?setTimeout:i}catch(e){t=i}try{n="function"===typeof clearTimeout?clearTimeout:o}catch(e){n=o}}();var s,u=[],c=!1,l=-1;function f(){c&&s&&(c=!1,s.length?u=s.concat(u):l=-1,u.length&&d())}function d(){if(!c){var e=a(f);c=!0;for(var t=u.length;t;){for(s=u,u=[];++l1)for(var n=1;n0&&u>s&&(u=s);for(var c=0;c=0?(l=p.substr(0,v),f=p.substr(v+1)):(l=p,f=""),d=decodeURIComponent(l),h=decodeURIComponent(f),t(o,d)?Array.isArray(o[d])?o[d].push(h):o[d]=[o[d],h]:o[d]=h}return o}},4297:function(e){"use strict";var t=function(e){switch(typeof e){case"string":return e;case"boolean":return e?"true":"false";case"number":return isFinite(e)?e:"";default:return""}};e.exports=function(e,n,r,i){return n=n||"&",r=r||"=",null===e&&(e=void 0),"object"===typeof e?Object.keys(e).map((function(i){var o=encodeURIComponent(t(i))+r;return Array.isArray(e[i])?e[i].map((function(e){return o+encodeURIComponent(t(e))})).join(n):o+encodeURIComponent(t(e[i]))})).join(n):i?encodeURIComponent(t(i))+r+encodeURIComponent(t(e)):""}},863:function(e,t,n){"use strict";t.decode=t.parse=n(7059),t.encode=t.stringify=n(4297)},4463:function(e,t,n){"use strict";var r=n(2791),i=n(5296);function o(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n