diff --git a/.docker/caddy/Caddyfile b/.docker/caddy/Caddyfile new file mode 100644 index 0000000..9704d1a --- /dev/null +++ b/.docker/caddy/Caddyfile @@ -0,0 +1,54 @@ +{ + # Global options + auto_https off # We'll handle HTTPS at ingress level in K8s + admin off # Disable admin API for security +} + +:80 { + # Root directory + root * /var/www/html/public + + # PHP-FPM configuration + php_fastcgi php:9000 { + split .php + index index.php + resolve_root_symlink + } + + # Serve static files + file_server + + # Logging + log { + output file /var/log/caddy/access.log + format json + } + + # CORS headers + @options { + method OPTIONS + } + handle @options { + header Access-Control-Allow-Origin "*" + header Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE, PATCH" + header Access-Control-Allow-Headers "Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,Keep-Alive,X-Requested-With,If-Modified-Since" + header Access-Control-Max-Age "1728000" + respond 204 + } + + # Apply CORS to all responses + header Access-Control-Allow-Origin "*" + + # Gzip compression + encode gzip + + # Max body size (for file uploads) + request_body { + max_size 50MB + } + + @health path /health + handle @health { + respond "OK" 200 + } +} \ No newline at end of file diff --git a/.docker/caddy/Caddyfile.not-google b/.docker/caddy/Caddyfile.not-google new file mode 100644 index 0000000..87e4535 --- /dev/null +++ b/.docker/caddy/Caddyfile.not-google @@ -0,0 +1,68 @@ +{ + servers { + trusted_proxies static 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 + } +} + +:80 { + # 1. Health check (stays separate) + handle /health { + respond "OK" 200 + } + + # 2. Wrap everything else in a route + route { + # Dynamic CORS for multiple origins (flags and capitals apps) + @flags_origin header Origin https://flags.izeebot.top + @capitals_origin header Origin https://capitals.izeebot.top + + # Set CORS headers based on origin + handle @flags_origin { + header Access-Control-Allow-Origin "https://flags.izeebot.top" + header Access-Control-Allow-Credentials "true" + header Access-Control-Expose-Headers "Authorization" + header Vary Origin + } + handle @capitals_origin { + header Access-Control-Allow-Origin "https://capitals.izeebot.top" + header Access-Control-Allow-Credentials "true" + header Access-Control-Expose-Headers "Authorization" + header Vary Origin + } + + # Handle Preflight separately + @options method OPTIONS + handle @options { + @options_flags header Origin https://flags.izeebot.top + @options_capitals header Origin https://capitals.izeebot.top + header @options_flags Access-Control-Allow-Origin "https://flags.izeebot.top" + header @options_capitals Access-Control-Allow-Origin "https://capitals.izeebot.top" + header Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE, PATCH" + header Access-Control-Allow-Credentials "true" + header Access-Control-Max-Age "1728000" + header Access-Control-Allow-Headers "Authorization, Content-Type, Accept, Origin, User-Agent, DNT, Cache-Control, X-Mx-ReqToken, Keep-Alive, X-Requested-With, If-Modified-Since, X-API-KEY" + respond 204 + } + + # 3. Security & Compression + encode gzip zstd + header { + X-Content-Type-Options "nosniff" + X-Frame-Options "SAMEORIGIN" + Referrer-Policy "strict-origin-when-cross-origin" + } + + # 4. Proxy to Symfony + reverse_proxy php:59000 { + transport fastcgi { + root /var/www/html/public + env SCRIPT_FILENAME /var/www/html/public/index.php + } + } + } + + log { + output stdout + format json + } +} diff --git a/.docker/caddy/Caddyfile.prod b/.docker/caddy/Caddyfile.prod new file mode 100644 index 0000000..a6aaa80 --- /dev/null +++ b/.docker/caddy/Caddyfile.prod @@ -0,0 +1,62 @@ +{ + servers { + trusted_proxies static 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 + } +} + +:80 { +# 1. Health check (stays separate) + handle /health { + respond "OK" 200 + } + +# 2. Wrap everything else in a route + route { + # Match allowed origins using a Regex with backticks for safety + @allowed_origins header_regexp origin Origin `^https://(flags|capitals)\.izeebot\.top$` + + # Handle Preflight (OPTIONS) + @options { + method OPTIONS + header_regexp origin Origin `^https://(flags|capitals)\.izeebot\.top$` + } + handle @options { + header Access-Control-Allow-Origin "{header.Origin}" + header Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE, PATCH" + header Access-Control-Allow-Credentials "true" + header Access-Control-Max-Age "1728000" + header Access-Control-Allow-Headers "Authorization, Content-Type, Accept, Origin, User-Agent, DNT, Cache-Control, X-Mx-ReqToken, Keep-Alive, X-Requested-With, If-Modified-Since, X-API-KEY" + respond 204 + } + + # Apply CORS headers for actual requests (GET, POST, etc.) + # {re.origin.0} refers to the first capture of the 'origin' regex above + header @allowed_origins { + Access-Control-Allow-Origin "{re.origin.0}" + Access-Control-Allow-Credentials "true" + Access-Control-Expose-Headers "Authorization" + Vary Origin + } + + # 3. Security & Compression + encode gzip zstd + header { + X-Content-Type-Options "nosniff" + X-Frame-Options "SAMEORIGIN" + Referrer-Policy "strict-origin-when-cross-origin" + } + + # 4. Proxy to Symfony + reverse_proxy php:59000 { + transport fastcgi { + root /var/www/html/public + env SCRIPT_FILENAME /var/www/html/public/index.php + } + } + } + + log { + output stdout + format json + } +} \ No newline at end of file diff --git a/.docker/caddy/Dockerfile b/.docker/caddy/Dockerfile new file mode 100644 index 0000000..5c16ed9 --- /dev/null +++ b/.docker/caddy/Dockerfile @@ -0,0 +1,34 @@ +FROM caddy:2.7-alpine + +# Install necessary packages +RUN apk add --no-cache \ + shadow \ + && rm -rf /var/cache/apk/* + +# Modify existing www-data group/user to match PHP-FPM (UID/GID 1000) +RUN deluser www-data 2>/dev/null || true && \ + delgroup www-data 2>/dev/null || true && \ + addgroup -g 1000 www-data && \ + adduser -D -u 1000 -G www-data www-data + +# Copy Caddyfile +COPY .docker/caddy/Caddyfile /etc/caddy/Caddyfile + +COPY ../../app /var/www/html + +# Create log directory +RUN mkdir -p /var/log/caddy && \ + chown -R www-data:www-data /var/log/caddy + +# Expose ports +EXPOSE 80 443 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:80 || exit 1 + +# Caddy runs as root by default, which is fine for binding to port 80/443 +# If you want to run as www-data, you'd need to use ports >1024 + +# Caddy runs in foreground by default +CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"] \ No newline at end of file diff --git a/.docker/caddy/Dockerfile.prod b/.docker/caddy/Dockerfile.prod new file mode 100644 index 0000000..19e3ba1 --- /dev/null +++ b/.docker/caddy/Dockerfile.prod @@ -0,0 +1,34 @@ +FROM caddy:2.7-alpine + +# Install necessary packages +RUN apk add --no-cache \ + shadow \ + && rm -rf /var/cache/apk/* + +# Modify existing www-data group/user to match PHP-FPM (UID/GID 1000) +RUN deluser www-data 2>/dev/null || true && \ + delgroup www-data 2>/dev/null || true && \ + addgroup -g 1000 www-data && \ + adduser -D -u 1000 -G www-data www-data + +# Copy Caddyfile +COPY .docker/caddy/Caddyfile.prod /etc/caddy/Caddyfile + +COPY ../../app /var/www/webapp + +# Create log directory +RUN mkdir -p /var/log/caddy && \ + chown -R www-data:www-data /var/log/caddy + +# Expose ports +EXPOSE 80 443 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:80 || exit 1 + +# Caddy runs as root by default, which is fine for binding to port 80/443 +# If you want to run as www-data, you'd need to use ports >1024 + +# Caddy runs in foreground by default +CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"] \ No newline at end of file diff --git a/.docker/mysql/Dockerfile b/.docker/mysql/Dockerfile index be285d4..fddc855 100644 --- a/.docker/mysql/Dockerfile +++ b/.docker/mysql/Dockerfile @@ -1,11 +1,5 @@ -FROM mysql:8.0 +FROM mysql:9.5 RUN ln -snf /usr/share/zoneinfo/UTC /etc/localtime && echo UTC > /etc/timezone -RUN chown -R mysql:root /var/lib/mysql/ - -ADD .docker/mysql/my.cnf /etc/mysql/conf.d/my.cnf - -CMD ["mysqld"] - EXPOSE 3306 \ No newline at end of file diff --git a/.docker/mysql/my.cnf b/.docker/mysql/my.cnf deleted file mode 100644 index d901304..0000000 --- a/.docker/mysql/my.cnf +++ /dev/null @@ -1,4 +0,0 @@ -[mysql] - -[mysqld] -default-authentication-plugin=mysql_native_password \ No newline at end of file diff --git a/.docker/mysql57/Dockerfile b/.docker/mysql57/Dockerfile deleted file mode 100644 index 4f62d4d..0000000 --- a/.docker/mysql57/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM mysql:5.7.20 - -RUN ln -snf /usr/share/zoneinfo/UTC /etc/localtime && echo UTC > /etc/timezone - -RUN chown -R mysql:root /var/lib/mysql/ - -ADD .docker/mysql57/my.cnf /etc/mysql/conf.d/my.cnf - -CMD ["mysqld"] - -EXPOSE 3306 \ No newline at end of file diff --git a/.docker/mysql57/my.cnf b/.docker/mysql57/my.cnf deleted file mode 100644 index 3df2eab..0000000 --- a/.docker/mysql57/my.cnf +++ /dev/null @@ -1,5 +0,0 @@ -[mysql] - -[mysqld] -sql-mode="STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION" -character-set-server=utf8 diff --git a/.docker/nginx/Dockerfile b/.docker/nginx/Dockerfile index 8524617..d912b8b 100644 --- a/.docker/nginx/Dockerfile +++ b/.docker/nginx/Dockerfile @@ -6,10 +6,10 @@ RUN apk add --update \ RUN rm -rf /var/cache/apk/* && rm -rf /tmp/* -ADD .docker/nginx/nginx.conf /etc/nginx/ -ADD .docker/nginx/host.conf /etc/nginx/conf.d/ -ADD .docker/nginx/cert.crt /etc/ssl/cert.crt -ADD .docker/nginx/cert.key /etc/ssl/cert.key +ADD ../.docker/nginx/nginx.conf /etc/nginx/ +ADD ../.docker/nginx/host.conf /etc/nginx/conf.d/ +ADD ../.docker/nginx/cert.crt /etc/ssl/cert.crt +ADD ../.docker/nginx/cert.key /etc/ssl/cert.key RUN rm /etc/nginx/conf.d/default.conf diff --git a/.docker/nginx/dev.conf b/.docker/nginx/dev.conf new file mode 100644 index 0000000..87db9b3 --- /dev/null +++ b/.docker/nginx/dev.conf @@ -0,0 +1,20 @@ +# .docker/nginx/dev.conf +server { + listen 80; + root /var/www/webapp/public; + index index.php; + + location / { + try_files $uri $uri/ /index.php$is_args$args; + } + + location ~ \.php$ { + fastcgi_pass php:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + } + + error_log /var/log/nginx/error.log; + access_log /var/log/nginx/access.log; +} diff --git a/.docker/nginx/host.conf b/.docker/nginx/host.conf index 25ae095..d7cfd63 100644 --- a/.docker/nginx/host.conf +++ b/.docker/nginx/host.conf @@ -12,6 +12,17 @@ server { } location ~ ^/index\.php(/|$) { + # 1. Add this Preflight check + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE, PATCH' always; + add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,Keep-Alive,X-Requested-With,If-Modified-Since' always; + add_header 'Access-Control-Max-Age' 1728000; + add_header 'Content-Type' 'text/plain charset=UTF-8'; + add_header 'Content-Length' 0; + return 204; + } + fastcgi_pass php-upstream; fastcgi_split_path_info ^(.+\.php)(/.*)$; include fastcgi_params; diff --git a/.docker/php-fpm/Dockerfile b/.docker/php-fpm/Dockerfile index 215a2e5..de9ec67 100644 --- a/.docker/php-fpm/Dockerfile +++ b/.docker/php-fpm/Dockerfile @@ -1,13 +1,165 @@ -FROM swiftcode/flags:php-fpm-latest +# ========================================== +# Base stage - shared foundation +# ========================================== +FROM php:8.4-fpm-alpine AS base -ARG KEY +# Copy the PHP extension installer (uses pre-built binaries when available) +COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/ + +# Install all extensions in one command - MUCH faster +RUN install-php-extensions \ + pdo_mysql \ + intl \ + opcache \ + gd \ + xml \ + mbstring \ + zip \ + redis + +# Install Composer +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +WORKDIR /var/www/html + +# ========================================== +# Development stage +# ========================================== +FROM base AS development + +ARG USER_ID=1000 +ARG GROUP_ID=1000 +WORKDIR /var/www/html + +RUN apk add --no-cache git curl wget vim bash + +RUN { \ + echo 'memory_limit = 256M'; \ + echo 'upload_max_filesize = 2M'; \ + echo 'post_max_size = 8M'; \ + echo 'max_execution_time = 30'; \ + echo 'date.timezone = UTC'; \ + echo 'opcache.enable = 1'; \ + echo 'opcache.enable_cli = 1'; \ + echo 'opcache.validate_timestamps = 1'; \ + echo 'opcache.revalidate_freq = 0'; \ + echo 'session.save_handler = files'; \ + echo 'session.save_path = "/tmp"'; \ + echo 'display_errors = On'; \ + echo 'error_reporting = E_ALL'; \ + echo 'log_errors = On'; \ +} > /usr/local/etc/php/conf.d/custom.ini + +RUN deluser www-data 2>/dev/null || true && \ + addgroup -g ${GROUP_ID} www-data && \ + adduser -u ${USER_ID} -G www-data -s /bin/sh -D www-data && \ + chown -R www-data:www-data /var/www + +COPY .docker/php-fpm/docker-entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh && \ + chown www-data:www-data /entrypoint.sh -RUN echo "$KEY" | base64 -d > /home/www-data/.ssh/id_rsa -RUN chmod 0600 /home/www-data/.ssh/id_rsa -RUN chown -R www-data:www-data /home/www-data/.ssh USER www-data -WORKDIR /var/www/webapp -RUN export GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no" && git clone git@github.com:mainstreamer/flags-api.git . -RUN cp .env.prod .env -RUN composer install --no-ansi --no-dev --no-interaction --no-plugins --no-progress --no-scripts --optimize-autoloader --classmap-authoritative -RUN rm /home/www-data/.ssh/id_rsa + +RUN wget https://get.symfony.com/cli/installer -O - | bash || true && \ + if [ -f ~/.symfony5/bin/symfony ]; then \ + mkdir -p ~/bin && mv ~/.symfony5/bin/symfony ~/bin/symfony; \ + fi + +ENV PATH="/home/www-data/bin:${PATH}" + +EXPOSE 9000 + +ENTRYPOINT ["/entrypoint.sh"] +CMD ["php-fpm"] + +# ========================================== +# Builder stage +# ========================================== +FROM base AS builder + +RUN apk add --no-cache git + +RUN { \ + echo 'memory_limit = 256M'; \ + echo 'opcache.enable = 1'; \ + echo 'opcache.enable_cli = 0'; \ + echo 'session.save_handler = files'; \ + echo 'session.save_path = "/tmp"'; \ + echo 'display_errors = Off'; \ + echo 'log_errors = On'; \ +} > /usr/local/etc/php/conf.d/custom.ini + +# Copy composer files for layer caching +COPY --chown=www-data:www-data ./app/composer.json ./app/composer.lock* ./app/symfony.lock* ./ + +# USE BUILDKIT CACHE for Composer - HUGE speedup +RUN --mount=type=cache,target=/tmp/composer \ + COMPOSER_CACHE_DIR=/tmp/composer \ + composer install \ + --no-dev \ + --no-scripts \ + --no-interaction \ + --prefer-dist \ + --optimize-autoloader \ + --classmap-authoritative + +# Copy application code +COPY --chown=www-data:www-data app/ . + +RUN composer dump-autoload --optimize --classmap-authoritative + +# Skip cache warmup - do it at runtime instead (saves build time) +RUN chown -R www-data:www-data var/ + +# ========================================== +# Production stage +# ========================================== +FROM base AS production + +ARG USER_ID=1000 +ARG GROUP_ID=1000 + +# Match UIDs in production too +RUN deluser www-data 2>/dev/null || true && \ + addgroup -g ${GROUP_ID} www-data && \ + adduser -u ${USER_ID} -G www-data -s /bin/sh -D www-data + +RUN { \ + echo 'memory_limit = 256M'; \ + echo 'opcache.enable = 1'; \ + echo 'opcache.memory_consumption = 256'; \ + echo 'opcache.validate_timestamps = 0'; \ + echo 'session.save_handler = redis'; \ + echo 'session.save_path = "tcp://redis.flaux.svc.cluster.local:6379"'; \ + echo 'display_errors = Off'; \ + echo 'log_errors = On'; \ +} > /usr/local/etc/php/conf.d/custom.ini + +# Copy from builder +COPY --from=builder --chown=www-data:www-data /var/www/html ./ + +# Remove dev files +RUN rm -rf tests/ .git/ .github/ .env.local .env.*.local \ + docker-compose.yml Dockerfile Makefile *.md phpunit.xml* \ + /usr/local/bin/composer \ + && mkdir -p var/cache var/log \ + && bin/console cache:clear --env=prod --no-warmup \ + && php bin/console cache:warmup --env=prod \ + && chown -R www-data:www-data var/ + +# Add entrypoint +COPY .docker/php-fpm/docker-entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +ENV APP_ENV=prod APP_DEBUG=0 + +# Warmup cache as root before switching to www-data +#RUN php bin/console cache:clear --no-warmup \ +# && php bin/console cache:warmup + +USER www-data +EXPOSE 9000 + +ENTRYPOINT ["/entrypoint.sh"] +CMD ["php-fpm"] \ No newline at end of file diff --git a/.docker/php-fpm/Dockerfile-base b/.docker/php-fpm/Dockerfile-base deleted file mode 100644 index 154d2a4..0000000 --- a/.docker/php-fpm/Dockerfile-base +++ /dev/null @@ -1,58 +0,0 @@ -FROM php:8.2.1-fpm-alpine3.17 - -RUN echo http://nl.alpinelinux.org/alpine/edge/testing >> /etc/apk/repositories; \ - echo http://dl-2.alpinelinux.org/alpine/edge/community >> /etc/apk/repositories; \ - echo http://dl-3.alpinelinux.org/alpine/edge/community >> /etc/apk/repositories; \ - echo http://dl-4.alpinelinux.org/alpine/edge/community >> /etc/apk/repositories; \ - echo http://dl-5.alpinelinux.org/alpine/edge/community >> /etc/apk/repositories - -RUN apk add --update \ - acl \ - sudo \ - bash \ - shadow \ - postgresql-dev \ - mc \ - openssl \ - mysql-client \ - libpng-dev \ - grep \ - git \ - tcpdump \ - libzip-dev \ - openrc \ - openssh - -RUN docker-php-ext-configure pcntl --enable-pcntl - -RUN docker-php-ext-install pdo pdo_mysql pdo_pgsql \ - calendar \ - zip \ - intl \ - sodium - -RUN apk add --no-cache $PHPIZE_DEPS \ - && apk del --purge autoconf g++ make \ - && apk add --no-cache --update rabbitmq-c-dev \ - && apk add --no-cache --update --virtual .phpize-deps $PHPIZE_DEPS \ - && pecl install -o -f amqp \ - && docker-php-ext-enable amqp \ - && apk del .phpize-deps - -RUN apk add --no-cache pcre-dev $PHPIZE_DEPS \ - && pecl install redis \ - && docker-php-ext-enable redis.so - -RUN rm -rf /var/cache/apk/* && rm -rf /tmp/* -COPY .docker/php-fpm/php.ini /usr/local/etc/php/php.ini -RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin/ --filename=composer -RUN groupmod -g 1000 www-data -RUN usermod -u 1000 www-data -RUN mkdir -p /var/www/webapp -RUN chown -R 1000:1000 /var/www/webapp -RUN mkdir -p /home/www-data/.ssh \ - && chmod 0700 /home/www-data/.ssh -RUN chown -R www-data:www-data /home/www-data -RUN useradd -g root www-data & groups - -EXPOSE 9000 \ No newline at end of file diff --git a/.docker/php-fpm/Dockerfile.dev b/.docker/php-fpm/Dockerfile.dev new file mode 100644 index 0000000..bdb0d36 --- /dev/null +++ b/.docker/php-fpm/Dockerfile.dev @@ -0,0 +1,29 @@ +# .docker/php-fpm/Dockerfile.dev +FROM php:8.2-fpm-alpine + +# Install system dependencies +RUN apk add --no-cache \ + bash \ + git \ + mysql-client \ + libpng-dev \ + libzip-dev \ + postgresql-dev \ + $PHPIZE_DEPS + +# Install PHP extensions +RUN docker-php-ext-install \ + pdo \ + pdo_mysql \ + pdo_pgsql \ + zip \ + intl + +# Install Composer +RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin/ --filename=composer + +# Create user matching host +RUN adduser -u 1000 -D -s /bin/bash appuser +USER appuser + +WORKDIR /var/www/webapp diff --git a/.docker/php-fpm/README.md b/.docker/php-fpm/README.md new file mode 100644 index 0000000..2c35c22 --- /dev/null +++ b/.docker/php-fpm/README.md @@ -0,0 +1,11 @@ +# UPDATING IMAGE IN REGISTRY + +### 0. Login and registry key + +### 1. Rebuild +make build-prod + or +docker build -t ghcr.io/mainstreamer/flaux:latest --target production -f docker/php/Dockerfile . + +### 2. Push +docker push ghcr.io/mainstreamer/flaux:latest \ No newline at end of file diff --git a/.docker/php-fpm/build.md b/.docker/php-fpm/build.md deleted file mode 100644 index b372746..0000000 --- a/.docker/php-fpm/build.md +++ /dev/null @@ -1,3 +0,0 @@ -docker build -f .docker/php-fpm/Dockerfile-base -t swiftcode/flags:php-fpm-1.2 -t swiftcode/flags:php-fpm-latest . -docker push swiftcode/flags:php-fpm-1.2 -docker push swiftcode/flags:php-fpm-latest diff --git a/.docker/php-fpm/docker-entrypoint.sh b/.docker/php-fpm/docker-entrypoint.sh new file mode 100644 index 0000000..20ac62e --- /dev/null +++ b/.docker/php-fpm/docker-entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -e + +# Ensure Symfony directories are writable +if [ -d /var/www/webapp/var ]; then +mkdir -p /var/www/webapp/var/cache /var/www/webapp/var/log +chmod -R 775 /var/www/webapp/var 2>/dev/null || true +fi + +# Execute CMD +exec "$@" \ No newline at end of file diff --git a/.docker/php-fpm/php.ini b/.docker/php-fpm/php.ini deleted file mode 100644 index 222d4c5..0000000 --- a/.docker/php-fpm/php.ini +++ /dev/null @@ -1,4 +0,0 @@ -memory_limit=-1 -max_execution_time=0 -upload_max_filesize=20M -post_max_size=20M \ No newline at end of file diff --git a/.env.staging b/.env.staging deleted file mode 100644 index ec4f0ac..0000000 --- a/.env.staging +++ /dev/null @@ -1,37 +0,0 @@ -# In all environments, the following files are loaded if they exist, -# the latter taking precedence over the former: -# -# * .env contains default values for the environment variables needed by the app -# * .env.local uncommitted file with local overrides -# * .env.$APP_ENV committed environment-specific defaults -# * .env.$APP_ENV.local uncommitted environment-specific overrides -# -# Real environment variables win over .env files. -# -# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. -# -# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). -# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration - -GITH_KEY= -IMAGE=swiftcode/flags -PROJECT_NAME=flags-api -GIT_URL=git@github.com:mainstreamer/flags-api.git -BRANCH='feat/secrets' -GIT_REMOTE_URL=git@github.com:mainstreamer/docker-config.git -CODE_PATH=code -APP_ENV=staging -APP_SECRET=2b7d3cdbcb5e5a8117488547044f9e7b -CORS_ALLOW_ORIGIN=* - -###> doctrine/doctrine-bundle ### -# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url -# For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db" -# For a PostgreSQL database, use: "postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=11&charset=utf8" -# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml -###> lexik/jwt-authentication-bundle ### -JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem -JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem -JWT_PASSPHRASE=c6be11833f7971fab179b813c964d616 -JWT_TOKEN_TTL=3600000 -###< lexik/jwt-authentication-bundle ### diff --git a/.gitignore b/.gitignore index e10db2e..760ae8c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,33 +1,6 @@ - -###> symfony/framework-bundle ### -/env/* -/.env -/.env.local -/.env.local.php -/.env.*.local -/config/secrets/prod/prod.decrypt.private.php -/config/secrets/dev/dev.decrypt.private.php -/public/bundles/ -/var/ -/vendor/ -###< symfony/framework-bundle ### - -###> lexik/jwt-authentication-bundle ### -/config/jwt/*.pem -###< lexik/jwt-authentication-bundle ### -.idea -@auto-scripts -*.decrypt.*.php -###> phpunit/phpunit ### -/phpunit.xml -.phpunit.result.cache -###< phpunit/phpunit ### - -deploy.php +.idea/* .DS_Store -/.docker/certs/*.pem -/.docker/certs/*.srl -###> symfony/phpunit-bridge ### -.phpunit.result.cache -/phpunit.xml -###< symfony/phpunit-bridge ### +.docker/certs/*.pem +.docker/certs/*.srl +*.local +.env.prod diff --git a/.phpactor.json b/.phpactor.json new file mode 100644 index 0000000..c399e9c --- /dev/null +++ b/.phpactor.json @@ -0,0 +1,11 @@ +{ + "$schema": "/Users/artem/.composer/vendor/phpactor/phpactor/phpactor.schema.json", + "language_server_psalm.enabled": true, + "symfony.enabled": true, + "indexer.exclude_patterns": [ + "/vendor/**/Tests/**/*", + "/vendor/**/tests/**/*", + "/var/cache/**/*", + "/vendor/composer/**/*" + ] +} \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3bc48c6 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,100 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Flags Quiz API - A Symfony 6.x REST API for a multiplayer flags and capitals quiz game with OAuth2/JWT authentication and Telegram login support. Fully containerized with Docker. + +## Common Commands + +All commands run from the `app/` directory via the makefile: + +```bash +# Full initialization (build, start containers, composer install, run API) +make init + +# Build and run +make build # Docker build +make run-containers # Start containers (docker compose up -d) +make run-api # Start Symfony server + +# Testing and code quality +make test # Run PHPUnit tests +make psalm # Static analysis +make fix # Format code with php-cs-fixer + +# Utilities +make sh # Shell into PHP container +make composer # Run composer install in container +make dumper # Start var-dump server +``` + +## Architecture + +``` +app/ +├── src/Flags/ # Main feature module +│ ├── Controller/ # API endpoints (GameController, CapitalsController, SecurityController) +│ ├── Entity/ # Doctrine entities (User, Game, Answer, Flag, Capital, Score) +│ ├── Repository/ # Doctrine repositories +│ ├── Service/ # Business logic (CapitalsGameService, HqAuthProvider) +│ ├── Security/ # Custom authenticator (HqAuthAuthenticator) +│ ├── DTO/ # Data transfer objects +│ └── ConsoleCommand/ # CLI commands +├── config/ +│ ├── packages/ # Bundle configs (doctrine, security, jwt, cors) +│ ├── jwt/pair/ # JWT public/private keys +│ └── secrets/ # Encrypted secrets per environment (dev/staging/prod) +├── migrations/ # Doctrine migrations +└── tests/Unit/ # PHPUnit tests +``` + +## Docker Architecture + +Four containers: PHP-FPM (8.4-alpine), Caddy (2.7-alpine web server), MySQL (9.5), optional Redis. + +Docker files in `.docker/` with multi-stage builds: +- `docker-compose.yml` - Base config +- `docker-compose.override.yml` - Dev overrides +- `docker-compose-prod.yml` / `docker-compose-staging.yml` - Deployment configs + +Network: `backend-flags` (external) + +## Authentication + +- **JWT** via Lexik JWT Authentication Bundle - stateless API auth +- **OAuth2** via KnpU OAuth2 Client Bundle with custom HqAuthProvider +- **Telegram Login** - validated in SecurityController + +Security firewall configured in `config/packages/secrity.yaml` (note: typo in filename). + +## Key API Routes + +- `/api/login` - Telegram login +- `/login`, `/oauth/check` - OAuth2 flow +- `/flags/correct/{flags}`, `/flags/scores` - Flags game +- `/capitals/game-start/{type}`, `/capitals/question/{game}`, `/capitals/answer/{game}/{country}/{answer}` - Capitals game +- `/api/incorrect`, `/api/correct` - User statistics (protected) + +## Game Types (WorldRegions enum) + +CAPITALS_EUROPE, CAPITALS_ASIA, CAPITALS_AFRICA, CAPITALS_AMERICAS, CAPITALS_OCEANIA + +## Environment Configuration + +- Secrets encrypted in `config/secrets/{env}/` +- JWT keys in `config/jwt/pair/` +- Environment files: `.env.prod`, `.env.staging`, `.env.test` + +## Testing + +```bash +# Run all tests +make test + +# Run single test file +docker compose exec php vendor/bin/phpunit tests/Unit/Entity/GameTest.php +``` + +Test files in `app/tests/Unit/`. HTTP request examples in `app/http-requests/` for IDE testing. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..34dbb5e --- /dev/null +++ b/Makefile @@ -0,0 +1,54 @@ +.DEFAULT_GOAL := help +init: build-containers run-containers composer import-db run-api +run: run-containers run-api +build: ## Build docker comoppse project + @docker compose build +up: ## Run docker compose project + @docker compose up -d +run-api: + @symfony server:start +import-db: + @bin/console d:d:i flags.sql +composer: + @docker compose exec php composer install +deploy: + @./vendor/bin/dep deploy production +welcome: + @echo hi +run-tests: + @docker compose exec php vendor/bin/phpunit +psalm: + @docker compose exec php vendor/bin/psalm --no-cache +fix: + @docker compose exec php vendor/bin/php-cs-fixer fix +--: + @docker compose exec php sh -c "$(filter-out $@,$(MAKECMDGOALS) $(MAKEFLAGS))" +sh: ## Shell access into php container + @docker compose exec -u 1000 php sh +dumper: + @docker compose exec php vendor/bin/var-dump-server +domain-upd: ## Set webhook url for bot, needs named argument e.g. domain-upd url=https://url.com + @curl -X POST https://api.telegram.org/bot$(BOT_TOKEN)/setWebhook -d "url=$(url)" +domain-check: ## Check domain + @curl -s "https://api.telegram.org/bot$(BOT_TOKEN)/getWebhookInfo" +domain-me: ## Check domain + @curl -s "https://api.telegram.org/bot$(BOT_TOKEN)/getMe" +%: + @ + +help: + @echo "Usage: make target" + @echo "Available targets:" + @awk '/^[a-zA-Z0-9\-_]+:/ { \ + match($$0, /^[a-zA-Z0-9\-_]+:/, target); \ + target_name = substr(target[0], 1, length(target[0]) - 1); \ + if (match($$0, /##[[:space:]]*(.*)/, desc)) { \ + printf "\033[34m%-20s\033[0m %s\n", target_name, desc[1]; \ + } else { \ + printf "\033[34m%-20s\033[0m \033[30mn/a \033[0m\n", target_name; \ + } \ + }' Makefile | sort + +# BOT_TOKEN received from there +include .env.local + diff --git a/.env.prod b/app/.env.prod similarity index 93% rename from .env.prod rename to app/.env.prod index 8058a9a..c790d18 100644 --- a/.env.prod +++ b/app/.env.prod @@ -23,6 +23,8 @@ CODE_PATH=code APP_ENV=prod APP_SECRET=2b7d3cdbcb5e5a8117488547044f9e7b CORS_ALLOW_ORIGIN=* +# Trust proxies for HTTPS detection (Traefik/Caddy -> PHP) +TRUSTED_PROXIES=127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 ###> doctrine/doctrine-bundle ### # Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url diff --git a/.env.test b/app/.env.test similarity index 100% rename from .env.test rename to app/.env.test diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..14db547 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,34 @@ + +###> symfony/framework-bundle ### +/env/* +/.env +/.env.local +/.env.local.php +/.env.*.local +/config/secrets/prod/prod.decrypt.private.php +/config/secrets/dev/dev.decrypt.private.php +/public/bundles/ +/var/ +/vendor/ +###< symfony/framework-bundle ### + +###> lexik/jwt-authentication-bundle ### +/config/jwt/*.pem +###< lexik/jwt-authentication-bundle ### +.idea/* +@auto-scripts +*.decrypt.*.php +###> phpunit/phpunit ### +/phpunit.xml +.phpunit.result.cache +###< phpunit/phpunit ### + +deploy.php +.DS_Store +/.docker/certs/*.pem +/.docker/certs/*.srl +###> symfony/phpunit-bridge ### +.phpunit.result.cache +/phpunit.xml +###< symfony/phpunit-bridge ### +*.local diff --git a/bin/console b/app/bin/console similarity index 100% rename from bin/console rename to app/bin/console diff --git a/bin/phpunit b/app/bin/phpunit similarity index 100% rename from bin/phpunit rename to app/bin/phpunit diff --git a/capitals-africa.json b/app/capitals-africa.json similarity index 94% rename from capitals-africa.json rename to app/capitals-africa.json index fcc6a1c..24a0748 100644 --- a/capitals-africa.json +++ b/app/capitals-africa.json @@ -24,6 +24,12 @@ "region": "Africa", "isoCode": "BW" }, + { + "name": "Bouvet Island", + "capital": "Oslo", + "region": "Africa", + "isoCode": "BV" + }, { "name": "Burkina Faso", "capital": "Ouagadougou", @@ -54,6 +60,12 @@ "region": "Africa", "isoCode": "CF" }, + { + "name": "Western Sahara", + "capital": "Laayoune", + "region": "Africa", + "isoCode": "EH" + }, { "name": "Chad", "capital": "N'Djamena", @@ -204,6 +216,12 @@ "region": "Africa", "isoCode": "MU" }, + { + "name": "Mayotte", + "capital": "Mamoudzou", + "region": "Asia", + "isoCode": "YT" + }, { "name": "Morocco", "capital": "Rabat", diff --git a/capitals-americas.json b/app/capitals-americas.json similarity index 94% rename from capitals-americas.json rename to app/capitals-americas.json index 470b681..cd23b4e 100644 --- a/capitals-americas.json +++ b/app/capitals-americas.json @@ -12,6 +12,12 @@ "region": "Americas", "isoCode": "AR" }, + { + "name": "Netherlands Antilles", + "capital": "Willemstad", + "region": "Americas", + "isoCode": "AN" + }, { "name": "Bahamas", "capital": "Nassau", @@ -264,6 +270,12 @@ "region": "Americas", "isoCode": "FK" }, + { + "name": "French Guiana", + "capital": "Cayenne", + "region": "Americas", + "isoCode": "GF" + }, { "name": "Greenland", "capital": "Nuuk", @@ -312,6 +324,13 @@ "region": "Americas", "isoCode": "PM" }, + + { + "name": " South Georgia and the South Sandwich Islands", + "capital": "King Edward Point", + "region": "Americas", + "isoCode": "SG" + }, { "name": "Sint Maarten", "capital": "Philipsburg", diff --git a/capitals-asia.json b/app/capitals-asia.json similarity index 89% rename from capitals-asia.json rename to app/capitals-asia.json index 5ddbc9f..b11f346 100644 --- a/capitals-asia.json +++ b/app/capitals-asia.json @@ -42,6 +42,12 @@ "region": "Asia", "isoCode": "BN" }, + { + "name": "Cocos (Keeling) Islands", + "capital": "West Island", + "region": "Asia", + "isoCode": "CC" + }, { "name": "Cambodia", "capital": "Phnom Penh", @@ -66,6 +72,12 @@ "region": "Asia", "isoCode": "GE" }, + { + "name": "Hong Kong", + "capital": "Hong Kong", + "region": "Asia", + "isoCode": "HK" + }, { "name": "India", "capital": "New Delhi", @@ -78,6 +90,13 @@ "region": "Asia", "isoCode": "ID" }, + + { + "name": "British Indian Ocean Territory", + "capital": "Camp Thunder Cove", + "region": "Asia", + "isoCode": "IO" + }, { "name": "Iran", "capital": "Tehran", @@ -138,6 +157,12 @@ "region": "Asia", "isoCode": "LB" }, + { + "name": "Macau", + "capital": "Macau", + "region": "Asia", + "isoCode": "MO" + }, { "name": "Malaysia", "capital": "Kuala Lumpur", @@ -234,6 +259,12 @@ "region": "Asia", "isoCode": "SY" }, + { + "name": "Taiwan", + "capital": "Taipei", + "region": "Asia", + "isoCode": "TW" + }, { "name": "Tajikistan", "capital": "Dushanbe", diff --git a/capitals-europe.json b/app/capitals-europe.json similarity index 87% rename from capitals-europe.json rename to app/capitals-europe.json index 8b39bb2..84f420a 100644 --- a/capitals-europe.json +++ b/app/capitals-europe.json @@ -18,6 +18,12 @@ "region": "Europe", "isoCode": "AT" }, + { + "name": "Aland", + "capital": "Mariehamn", + "region": "Europe", + "isoCode": "AX" + }, { "name": "Belarus", "capital": "Minsk", @@ -90,12 +96,30 @@ "region": "Europe", "isoCode": "DE" }, + { + "name": "Guernsey", + "capital": "Saint Peter Port", + "region": "Europe", + "isoCode": "GG" + }, + { + "name": "Gibraltar", + "capital": "Gibraltar", + "region": "Europe", + "isoCode": "GI" + }, { "name": "Greece", "capital": "Athens", "region": "Europe", "isoCode": "GR" }, + { + "name": "Faroe Islands", + "capital": "Torshavn", + "region": "Europe", + "isoCode": "FO" + }, { "name": "Hungary", "capital": "Budapest", @@ -114,12 +138,24 @@ "region": "Europe", "isoCode": "IE" }, + { + "name": "Isle of Man", + "capital": "Douglas", + "region": "Europe", + "isoCode": "IM" + }, { "name": "Italy", "capital": "Rome", "region": "Europe", "isoCode": "IT" }, + { + "name": "Jersey", + "capital": "Saint Helier", + "region": "Europe", + "isoCode": "JE" + }, { "name": "Kosovo", "capital": "Pristina", diff --git a/capitals-oceania.json b/app/capitals-oceania.json similarity index 100% rename from capitals-oceania.json rename to app/capitals-oceania.json diff --git a/composer.json b/app/composer.json similarity index 98% rename from composer.json rename to app/composer.json index f929b66..fe4c75f 100644 --- a/composer.json +++ b/app/composer.json @@ -12,6 +12,7 @@ "doctrine/orm": "*", "lexik/jwt-authentication-bundle": "*", "nelmio/cors-bundle": "^2.3", + "phpactor/language-server": "^6.1", "rteeom/isoflags": "^1.1", "sensio/framework-extra-bundle": "*", "symfony/console": "*", diff --git a/composer.lock b/app/composer.lock similarity index 86% rename from composer.lock rename to app/composer.lock index 6ca1dde..3b6ed4b 100644 --- a/composer.lock +++ b/app/composer.lock @@ -4,44 +4,920 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0c6f99db745c439aba06f4970de96864", + "content-hash": "5f7a3875d3d62313d9c8b8b41bd62006", "packages": [ + { + "name": "amphp/amp", + "version": "v2.6.4", + "source": { + "type": "git", + "url": "https://github.com/amphp/amp.git", + "reference": "ded3d9be08f526089eb7ee8d9f16a9768f9dec2d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/amp/zipball/ded3d9be08f526089eb7ee8d9f16a9768f9dec2d", + "reference": "ded3d9be08f526089eb7ee8d9f16a9768f9dec2d", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1", + "ext-json": "*", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^7 | ^8 | ^9", + "react/promise": "^2", + "vimeo/psalm": "^3.12" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "files": [ + "lib/functions.php", + "lib/Internal/functions.php" + ], + "psr-4": { + "Amp\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A non-blocking concurrency framework for PHP applications.", + "homepage": "https://amphp.org/amp", + "keywords": [ + "async", + "asynchronous", + "awaitable", + "concurrency", + "event", + "event-loop", + "future", + "non-blocking", + "promise" + ], + "support": { + "irc": "irc://irc.freenode.org/amphp", + "issues": "https://github.com/amphp/amp/issues", + "source": "https://github.com/amphp/amp/tree/v2.6.4" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-03-21T18:52:26+00:00" + }, + { + "name": "amphp/byte-stream", + "version": "v1.8.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/byte-stream.git", + "reference": "4f0e968ba3798a423730f567b1b50d3441c16ddc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/byte-stream/zipball/4f0e968ba3798a423730f567b1b50d3441c16ddc", + "reference": "4f0e968ba3798a423730f567b1b50d3441c16ddc", + "shasum": "" + }, + "require": { + "amphp/amp": "^2", + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1.4", + "friendsofphp/php-cs-fixer": "^2.3", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^6 || ^7 || ^8", + "psalm/phar": "^3.11.4" + }, + "type": "library", + "autoload": { + "files": [ + "lib/functions.php" + ], + "psr-4": { + "Amp\\ByteStream\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A stream abstraction to make working with non-blocking I/O simple.", + "homepage": "https://amphp.org/byte-stream", + "keywords": [ + "amp", + "amphp", + "async", + "io", + "non-blocking", + "stream" + ], + "support": { + "issues": "https://github.com/amphp/byte-stream/issues", + "source": "https://github.com/amphp/byte-stream/tree/v1.8.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-13T18:00:56+00:00" + }, + { + "name": "amphp/cache", + "version": "v1.5.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/cache.git", + "reference": "fe78cfae2fb8c92735629b8cd1893029c73c9b63" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/cache/zipball/fe78cfae2fb8c92735629b8cd1893029c73c9b63", + "reference": "fe78cfae2fb8c92735629b8cd1893029c73c9b63", + "shasum": "" + }, + "require": { + "amphp/amp": "^2", + "amphp/serialization": "^1", + "amphp/sync": "^1.2", + "php": ">=7.1" + }, + "conflict": { + "amphp/file": "<0.2 || >=3" + }, + "require-dev": { + "amphp/file": "^1 || ^2", + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1.1", + "phpunit/phpunit": "^6 | ^7 | ^8 | ^9", + "vimeo/psalm": "^4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Cache\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + } + ], + "description": "A promise-aware caching API for Amp.", + "homepage": "https://github.com/amphp/cache", + "support": { + "irc": "irc://irc.freenode.org/amphp", + "issues": "https://github.com/amphp/cache/issues", + "source": "https://github.com/amphp/cache/tree/v1.5.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-03-21T19:35:02+00:00" + }, + { + "name": "amphp/dns", + "version": "v1.2.3", + "source": { + "type": "git", + "url": "https://github.com/amphp/dns.git", + "reference": "852292532294d7972c729a96b49756d781f7c59d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/dns/zipball/852292532294d7972c729a96b49756d781f7c59d", + "reference": "852292532294d7972c729a96b49756d781f7c59d", + "shasum": "" + }, + "require": { + "amphp/amp": "^2", + "amphp/byte-stream": "^1.1", + "amphp/cache": "^1.2", + "amphp/parser": "^1", + "amphp/windows-registry": "^0.3", + "daverandom/libdns": "^2.0.1", + "ext-filter": "*", + "ext-json": "*", + "php": ">=7.0" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1", + "phpunit/phpunit": "^6 || ^7 || ^8 || ^9" + }, + "type": "library", + "autoload": { + "files": [ + "lib/functions.php" + ], + "psr-4": { + "Amp\\Dns\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Wright", + "email": "addr@daverandom.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "Async DNS resolution for Amp.", + "homepage": "https://github.com/amphp/dns", + "keywords": [ + "amp", + "amphp", + "async", + "client", + "dns", + "resolve" + ], + "support": { + "issues": "https://github.com/amphp/dns/issues", + "source": "https://github.com/amphp/dns/tree/v1.2.3" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2020-07-21T19:04:57+00:00" + }, + { + "name": "amphp/parser", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/parser.git", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/parser/zipball/3cf1f8b32a0171d4b1bed93d25617637a77cded7", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7", + "shasum": "" + }, + "require": { + "php": ">=7.4" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Parser\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A generator parser to make streaming parsers simple.", + "homepage": "https://github.com/amphp/parser", + "keywords": [ + "async", + "non-blocking", + "parser", + "stream" + ], + "support": { + "issues": "https://github.com/amphp/parser/issues", + "source": "https://github.com/amphp/parser/tree/v1.1.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-03-21T19:16:53+00:00" + }, + { + "name": "amphp/process", + "version": "v1.1.7", + "source": { + "type": "git", + "url": "https://github.com/amphp/process.git", + "reference": "1949d85b6d71af2818ff68144304a98495628f19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/process/zipball/1949d85b6d71af2818ff68144304a98495628f19", + "reference": "1949d85b6d71af2818ff68144304a98495628f19", + "shasum": "" + }, + "require": { + "amphp/amp": "^2", + "amphp/byte-stream": "^1.4", + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1", + "phpunit/phpunit": "^6" + }, + "type": "library", + "autoload": { + "files": [ + "lib/functions.php" + ], + "psr-4": { + "Amp\\Process\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Asynchronous process manager.", + "homepage": "https://github.com/amphp/process", + "support": { + "issues": "https://github.com/amphp/process/issues", + "source": "https://github.com/amphp/process/tree/v1.1.7" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-19T03:00:28+00:00" + }, + { + "name": "amphp/serialization", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/serialization.git", + "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/serialization/zipball/693e77b2fb0b266c3c7d622317f881de44ae94a1", + "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "phpunit/phpunit": "^9 || ^8 || ^7" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Serialization\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Serialization tools for IPC and data storage in PHP.", + "homepage": "https://github.com/amphp/serialization", + "keywords": [ + "async", + "asynchronous", + "serialization", + "serialize" + ], + "support": { + "issues": "https://github.com/amphp/serialization/issues", + "source": "https://github.com/amphp/serialization/tree/master" + }, + "time": "2020-03-25T21:39:07+00:00" + }, + { + "name": "amphp/socket", + "version": "v1.2.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/socket.git", + "reference": "b00528bd75548b7ae06a502358bb3ff8b106f5ab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/socket/zipball/b00528bd75548b7ae06a502358bb3ff8b106f5ab", + "reference": "b00528bd75548b7ae06a502358bb3ff8b106f5ab", + "shasum": "" + }, + "require": { + "amphp/amp": "^2", + "amphp/byte-stream": "^1.6", + "amphp/dns": "^1 || ^0.9", + "ext-openssl": "*", + "kelunik/certificate": "^1.1", + "league/uri-parser": "^1.4", + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1", + "phpunit/phpunit": "^6 || ^7 || ^8", + "vimeo/psalm": "^3.9@dev" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php", + "src/Internal/functions.php" + ], + "psr-4": { + "Amp\\Socket\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@gmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Async socket connection / server tools for Amp.", + "homepage": "https://github.com/amphp/socket", + "keywords": [ + "amp", + "async", + "encryption", + "non-blocking", + "sockets", + "tcp", + "tls" + ], + "support": { + "issues": "https://github.com/amphp/socket/issues", + "source": "https://github.com/amphp/socket/tree/v1.2.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-03-21T18:12:22+00:00" + }, + { + "name": "amphp/sync", + "version": "v1.4.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/sync.git", + "reference": "85ab06764f4f36d63b1356b466df6111cf4b89cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/sync/zipball/85ab06764f4f36d63b1356b466df6111cf4b89cf", + "reference": "85ab06764f4f36d63b1356b466df6111cf4b89cf", + "shasum": "" + }, + "require": { + "amphp/amp": "^2.2", + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1.1", + "phpunit/phpunit": "^9 || ^8 || ^7" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/ConcurrentIterator/functions.php" + ], + "psr-4": { + "Amp\\Sync\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" + } + ], + "description": "Mutex, Semaphore, and other synchronization tools for Amp.", + "homepage": "https://github.com/amphp/sync", + "keywords": [ + "async", + "asynchronous", + "mutex", + "semaphore", + "synchronization" + ], + "support": { + "issues": "https://github.com/amphp/sync/issues", + "source": "https://github.com/amphp/sync/tree/v1.4.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2021-10-25T18:29:10+00:00" + }, + { + "name": "amphp/windows-registry", + "version": "v0.3.3", + "source": { + "type": "git", + "url": "https://github.com/amphp/windows-registry.git", + "reference": "0f56438b9197e224325e88f305346f0221df1f71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/windows-registry/zipball/0f56438b9197e224325e88f305346f0221df1f71", + "reference": "0f56438b9197e224325e88f305346f0221df1f71", + "shasum": "" + }, + "require": { + "amphp/amp": "^2", + "amphp/byte-stream": "^1.4", + "amphp/process": "^1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\WindowsRegistry\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Windows Registry Reader.", + "support": { + "issues": "https://github.com/amphp/windows-registry/issues", + "source": "https://github.com/amphp/windows-registry/tree/master" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2020-07-10T16:13:29+00:00" + }, + { + "name": "brick/math", + "version": "0.12.1", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "f510c0a40911935b77b86859eb5223d58d660df1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/f510c0a40911935b77b86859eb5223d58d660df1", + "reference": "f510c0a40911935b77b86859eb5223d58d660df1", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^10.1", + "vimeo/psalm": "5.16.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "bignumber", + "brick", + "decimal", + "integer", + "math", + "mathematics", + "rational" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.12.1" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2023-11-29T23:19:16+00:00" + }, { "name": "composer/package-versions-deprecated", "version": "1.11.99.5", "source": { "type": "git", - "url": "https://github.com/composer/package-versions-deprecated.git", - "reference": "b4f54f74ef3453349c24a845d22392cd31e65f1d" + "url": "https://github.com/composer/package-versions-deprecated.git", + "reference": "b4f54f74ef3453349c24a845d22392cd31e65f1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/b4f54f74ef3453349c24a845d22392cd31e65f1d", + "reference": "b4f54f74ef3453349c24a845d22392cd31e65f1d", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.1.0 || ^2.0", + "php": "^7 || ^8" + }, + "replace": { + "ocramius/package-versions": "1.11.99" + }, + "require-dev": { + "composer/composer": "^1.9.3 || ^2.0@dev", + "ext-zip": "^1.13", + "phpunit/phpunit": "^6.5 || ^7" + }, + "type": "composer-plugin", + "extra": { + "class": "PackageVersions\\Installer", + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "PackageVersions\\": "src/PackageVersions" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be" + } + ], + "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)", + "support": { + "issues": "https://github.com/composer/package-versions-deprecated/issues", + "source": "https://github.com/composer/package-versions-deprecated/tree/1.11.99.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-01-17T14:14:24+00:00" + }, + { + "name": "dantleech/argument-resolver", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://gitlab.com/dantleech/argument-resolver.git", + "reference": "e34fabf7d6e53e5194f745ad069c4a87cc4b34cc" + }, + "dist": { + "type": "zip", + "url": "https://gitlab.com/api/v4/projects/dantleech%2Fargument-resolver/repository/archive.zip?sha=e34fabf7d6e53e5194f745ad069c4a87cc4b34cc", + "reference": "e34fabf7d6e53e5194f745ad069c4a87cc4b34cc", + "shasum": "" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.16", + "phpstan/phpstan": "^0.10.1", + "phpunit/phpunit": "^7.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "DTL\\ArgumentResolver\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Leech", + "email": "daniel@dantleech.com" + } + ], + "description": "Resolve method arguments from an associative array", + "support": { + "issues": "https://gitlab.com/api/v4/projects/7322320/issues" + }, + "time": "2020-04-09T09:32:31+00:00" + }, + { + "name": "dantleech/invoke", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/dantleech/invoke.git", + "reference": "9b002d746d2c1b86cfa63a47bb5909cee58ef50c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/b4f54f74ef3453349c24a845d22392cd31e65f1d", - "reference": "b4f54f74ef3453349c24a845d22392cd31e65f1d", + "url": "https://api.github.com/repos/dantleech/invoke/zipball/9b002d746d2c1b86cfa63a47bb5909cee58ef50c", + "reference": "9b002d746d2c1b86cfa63a47bb5909cee58ef50c", "shasum": "" }, "require": { - "composer-plugin-api": "^1.1.0 || ^2.0", - "php": "^7 || ^8" - }, - "replace": { - "ocramius/package-versions": "1.11.99" + "php": "^7.2||^8.0" }, "require-dev": { - "composer/composer": "^1.9.3 || ^2.0@dev", - "ext-zip": "^1.13", - "phpunit/phpunit": "^6.5 || ^7" + "friendsofphp/php-cs-fixer": "^2.13", + "phpbench/phpbench": "^1.0", + "phpstan/phpstan": "^0.12.0", + "phpunit/phpunit": "^8.0" }, - "type": "composer-plugin", + "type": "library", "extra": { - "class": "PackageVersions\\Installer", "branch-alias": { - "dev-master": "1.x-dev" + "dev-master": "1.2-dev" } }, "autoload": { "psr-4": { - "PackageVersions\\": "src/PackageVersions" + "DTL\\Invoke\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -50,34 +926,60 @@ ], "authors": [ { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com" - }, - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be" + "name": "daniel leech", + "email": "daniel@dantleech.com" } ], - "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)", + "description": "Emulate named parameters", "support": { - "issues": "https://github.com/composer/package-versions-deprecated/issues", - "source": "https://github.com/composer/package-versions-deprecated/tree/1.11.99.5" + "issues": "https://github.com/dantleech/invoke/issues", + "source": "https://github.com/dantleech/invoke/tree/2.0.0" }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" + "time": "2021-05-01T17:22:58+00:00" + }, + { + "name": "daverandom/libdns", + "version": "v2.1.0", + "source": { + "type": "git", + "url": "https://github.com/DaveRandom/LibDNS.git", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DaveRandom/LibDNS/zipball/b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "Required for IDN support" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "LibDNS\\": "src/" } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" ], - "time": "2022-01-17T14:14:24+00:00" + "description": "DNS protocol implementation written in pure PHP", + "keywords": [ + "dns" + ], + "support": { + "issues": "https://github.com/DaveRandom/LibDNS/issues", + "source": "https://github.com/DaveRandom/LibDNS/tree/v2.1.0" + }, + "time": "2024-04-12T12:12:48+00:00" }, { "name": "doctrine/annotations", @@ -1560,6 +2462,64 @@ ], "time": "2023-05-24T07:17:17+00:00" }, + { + "name": "kelunik/certificate", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/kelunik/certificate.git", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/kelunik/certificate/zipball/7e00d498c264d5eb4f78c69f41c8bd6719c0199e", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "php": ">=7.0" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^6 | 7 | ^8 | ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Kelunik\\Certificate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Access certificate details and transform between different formats.", + "keywords": [ + "DER", + "certificate", + "certificates", + "openssl", + "pem", + "x509" + ], + "support": { + "issues": "https://github.com/kelunik/certificate/issues", + "source": "https://github.com/kelunik/certificate/tree/v1.1.3" + }, + "time": "2023-02-03T21:26:53+00:00" + }, { "name": "laminas/laminas-code", "version": "4.13.0", @@ -1760,6 +2720,76 @@ ], "time": "2023-11-20T21:17:42+00:00" }, + { + "name": "league/uri-parser", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-parser.git", + "reference": "671548427e4c932352d9b9279fdfa345bf63fa00" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-parser/zipball/671548427e4c932352d9b9279fdfa345bf63fa00", + "reference": "671548427e4c932352d9b9279fdfa345bf63fa00", + "shasum": "" + }, + "require": { + "php": ">=7.0.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.0", + "phpstan/phpstan": "^0.9.2", + "phpstan/phpstan-phpunit": "^0.9.4", + "phpstan/phpstan-strict-rules": "^0.9.0", + "phpunit/phpunit": "^6.0" + }, + "suggest": { + "ext-intl": "Allow parsing RFC3987 compliant hosts", + "league/uri-schemes": "Allow validating and normalizing URI parsing results" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "League\\Uri\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "userland URI parser RFC 3986 compliant", + "homepage": "https://github.com/thephpleague/uri-parser", + "keywords": [ + "parse_url", + "parser", + "rfc3986", + "rfc3987", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/thephpleague/uri-parser/issues", + "source": "https://github.com/thephpleague/uri-parser/tree/master" + }, + "abandoned": "league/uri-interfaces", + "time": "2018-11-22T07:55:51+00:00" + }, { "name": "lexik/jwt-authentication-bundle", "version": "v2.20.3", @@ -1971,16 +3001,143 @@ "type": "symfony-bundle", "extra": { "branch-alias": { - "dev-master": "2.x-dev" + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Nelmio\\CorsBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nelmio", + "homepage": "http://nelm.io" + }, + { + "name": "Symfony Community", + "homepage": "https://github.com/nelmio/NelmioCorsBundle/contributors" + } + ], + "description": "Adds CORS (Cross-Origin Resource Sharing) headers support in your Symfony application", + "keywords": [ + "api", + "cors", + "crossdomain" + ], + "support": { + "issues": "https://github.com/nelmio/NelmioCorsBundle/issues", + "source": "https://github.com/nelmio/NelmioCorsBundle/tree/2.4.0" + }, + "time": "2023-11-30T16:41:19+00:00" + }, + { + "name": "phpactor/language-server", + "version": "6.1.4", + "source": { + "type": "git", + "url": "https://github.com/phpactor/language-server.git", + "reference": "18c6336fd3ede98bfe460e051a0a9d6f2456bdc4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpactor/language-server/zipball/18c6336fd3ede98bfe460e051a0a9d6f2456bdc4", + "reference": "18c6336fd3ede98bfe460e051a0a9d6f2456bdc4", + "shasum": "" + }, + "require": { + "amphp/socket": "^1.1", + "dantleech/argument-resolver": "^1.1", + "dantleech/invoke": "^2.0", + "php": "^8.0", + "phpactor/language-server-protocol": "^3.17", + "psr/event-dispatcher": "^1.0", + "psr/log": "^1.0", + "ramsey/uuid": "^4.0", + "thecodingmachine/safe": "^1.1" + }, + "require-dev": { + "amphp/phpunit-util": "^1.3", + "ergebnis/composer-normalize": "^2.0", + "friendsofphp/php-cs-fixer": "^3.0", + "jangregor/phpstan-prophecy": "^1.0", + "phpactor/phly-event-dispatcher": "~2.0.0", + "phpactor/test-utils": "~1.1.3", + "phpspec/prophecy-phpunit": "^2.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.0", + "phpunit/phpunit": "^9.0", + "symfony/var-dumper": "^5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Phpactor\\LanguageServer\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Leech", + "email": "daniel@dantleech.com" + } + ], + "description": "Generic Language Server Platform", + "support": { + "issues": "https://github.com/phpactor/language-server/issues", + "source": "https://github.com/phpactor/language-server/tree/6.1.4" + }, + "time": "2024-03-02T11:34:28+00:00" + }, + { + "name": "phpactor/language-server-protocol", + "version": "3.17.3", + "source": { + "type": "git", + "url": "https://github.com/phpactor/language-server-protocol.git", + "reference": "065fc70b6a26a8d78e034f7db92c8872367356df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpactor/language-server-protocol/zipball/065fc70b6a26a8d78e034f7db92c8872367356df", + "reference": "065fc70b6a26a8d78e034f7db92c8872367356df", + "shasum": "" + }, + "require": { + "dantleech/invoke": "^2.0", + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.0", + "friendsofphp/php-cs-fixer": "^2.17", + "phpstan/phpstan": "~0.12.0", + "phpunit/phpunit": "^9.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.3.x-dev" } }, "autoload": { "psr-4": { - "Nelmio\\CorsBundle\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Phpactor\\LanguageServerProtocol\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1988,25 +3145,16 @@ ], "authors": [ { - "name": "Nelmio", - "homepage": "http://nelm.io" - }, - { - "name": "Symfony Community", - "homepage": "https://github.com/nelmio/NelmioCorsBundle/contributors" + "name": "Daniel Leech", + "email": "daniel@dantleech.com" } ], - "description": "Adds CORS (Cross-Origin Resource Sharing) headers support in your Symfony application", - "keywords": [ - "api", - "cors", - "crossdomain" - ], + "description": "Langauge Server Protocol for PHP (transpiled)", "support": { - "issues": "https://github.com/nelmio/NelmioCorsBundle/issues", - "source": "https://github.com/nelmio/NelmioCorsBundle/tree/2.4.0" + "issues": "https://github.com/phpactor/language-server-protocol/issues", + "source": "https://github.com/phpactor/language-server-protocol/tree/3.17.3" }, - "time": "2023-11-30T16:41:19+00:00" + "time": "2023-03-11T13:19:45+00:00" }, { "name": "psr/cache", @@ -2210,30 +3358,30 @@ }, { "name": "psr/log", - "version": "3.0.0", + "version": "1.1.4", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", "shasum": "" }, "require": { - "php": ">=8.0.0" + "php": ">=5.3.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.x-dev" + "dev-master": "1.1.x-dev" } }, "autoload": { "psr-4": { - "Psr\\Log\\": "src" + "Psr\\Log\\": "Psr/Log/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2254,22 +3402,203 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/3.0.0" + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, + "time": "2021-05-03T11:20:27+00:00" + }, + { + "name": "ramsey/collection", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", + "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.28.3", + "fakerphp/faker": "^1.21", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^1.0", + "mockery/mockery": "^1.5", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpcsstandards/phpcsutils": "^1.0.0-rc1", + "phpspec/prophecy-phpunit": "^2.0", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.9", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5", + "psalm/plugin-mockery": "^1.1", + "psalm/plugin-phpunit": "^0.18.4", + "ramsey/coding-standard": "^2.0.3", + "ramsey/conventional-commits": "^1.3", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/collection", + "type": "tidelift" + } + ], + "time": "2022-12-31T21:50:55+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.7.6", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "91039bc1faa45ba123c4328958e620d382ec7088" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/91039bc1faa45ba123c4328958e620d382ec7088", + "reference": "91039bc1faa45ba123c4328958e620d382ec7088", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12", + "ext-json": "*", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.10", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "doctrine/annotations": "^1.8", + "ergebnis/composer-normalize": "^2.15", + "mockery/mockery": "^1.3", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.2", + "php-mock/php-mock-mockery": "^1.3", + "php-parallel-lint/php-parallel-lint": "^1.1", + "phpbench/phpbench": "^1.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^8.5 || ^9", + "ramsey/composer-repl": "^1.4", + "slevomat/coding-standard": "^8.4", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.9" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.7.6" }, - "time": "2021-07-14T16:46:02+00:00" + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid", + "type": "tidelift" + } + ], + "time": "2024-04-27T21:32:50+00:00" }, { "name": "rteeom/isoflags", - "version": "1.1.6", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/mainstreamer/isoflags.git", - "reference": "fba4ac13a942808a0cbc7c6b19fdf9700ab33492" + "reference": "ec3a4cc808a86cb001cbbc19651ffd514f76684f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mainstreamer/isoflags/zipball/fba4ac13a942808a0cbc7c6b19fdf9700ab33492", - "reference": "fba4ac13a942808a0cbc7c6b19fdf9700ab33492", + "url": "https://api.github.com/repos/mainstreamer/isoflags/zipball/ec3a4cc808a86cb001cbbc19651ffd514f76684f", + "reference": "ec3a4cc808a86cb001cbbc19651ffd514f76684f", "shasum": "" }, "require": { @@ -2299,9 +3628,9 @@ "description": "library for easy emoji flags generation from iso country codes", "support": { "issues": "https://github.com/mainstreamer/isoflags/issues", - "source": "https://github.com/mainstreamer/isoflags/tree/1.1.6" + "source": "https://github.com/mainstreamer/isoflags/tree/1.2.0" }, - "time": "2024-01-03T15:35:58+00:00" + "time": "2024-10-23T19:44:49+00:00" }, { "name": "sensio/framework-extra-bundle", @@ -4324,26 +5653,23 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.28.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5" + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5", - "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -4387,7 +5713,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" }, "funding": [ { @@ -4403,7 +5729,7 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-php83", @@ -5721,175 +7047,148 @@ } ], "time": "2023-11-06T11:00:25+00:00" - } - ], - "packages-dev": [ - { - "name": "amphp/amp", - "version": "v2.6.2", - "source": { - "type": "git", - "url": "https://github.com/amphp/amp.git", - "reference": "9d5100cebffa729aaffecd3ad25dc5aeea4f13bb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/amp/zipball/9d5100cebffa729aaffecd3ad25dc5aeea4f13bb", - "reference": "9d5100cebffa729aaffecd3ad25dc5aeea4f13bb", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "dev-master", - "amphp/phpunit-util": "^1", - "ext-json": "*", - "jetbrains/phpstorm-stubs": "^2019.3", - "phpunit/phpunit": "^7 | ^8 | ^9", - "psalm/phar": "^3.11@dev", - "react/promise": "^2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - } - }, - "autoload": { - "files": [ - "lib/functions.php", - "lib/Internal/functions.php" - ], - "psr-4": { - "Amp\\": "lib" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Daniel Lowrey", - "email": "rdlowrey@php.net" - }, - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Bob Weinand", - "email": "bobwei9@hotmail.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - } - ], - "description": "A non-blocking concurrency framework for PHP applications.", - "homepage": "https://amphp.org/amp", - "keywords": [ - "async", - "asynchronous", - "awaitable", - "concurrency", - "event", - "event-loop", - "future", - "non-blocking", - "promise" - ], - "support": { - "irc": "irc://irc.freenode.org/amphp", - "issues": "https://github.com/amphp/amp/issues", - "source": "https://github.com/amphp/amp/tree/v2.6.2" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2022-02-20T17:52:18+00:00" }, { - "name": "amphp/byte-stream", - "version": "v1.8.1", + "name": "thecodingmachine/safe", + "version": "v1.3.3", "source": { "type": "git", - "url": "https://github.com/amphp/byte-stream.git", - "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd" + "url": "https://github.com/thecodingmachine/safe.git", + "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/byte-stream/zipball/acbd8002b3536485c997c4e019206b3f10ca15bd", - "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/a8ab0876305a4cdaef31b2350fcb9811b5608dbc", + "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc", "shasum": "" }, "require": { - "amphp/amp": "^2", - "php": ">=7.1" + "php": ">=7.2" }, "require-dev": { - "amphp/php-cs-fixer-config": "dev-master", - "amphp/phpunit-util": "^1.4", - "friendsofphp/php-cs-fixer": "^2.3", - "jetbrains/phpstorm-stubs": "^2019.3", - "phpunit/phpunit": "^6 || ^7 || ^8", - "psalm/phar": "^3.11.4" + "phpstan/phpstan": "^0.12", + "squizlabs/php_codesniffer": "^3.2", + "thecodingmachine/phpstan-strict-rules": "^0.12" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-master": "0.1-dev" } }, "autoload": { "files": [ - "lib/functions.php" + "deprecated/apc.php", + "deprecated/libevent.php", + "deprecated/mssql.php", + "deprecated/stats.php", + "lib/special_cases.php", + "generated/apache.php", + "generated/apcu.php", + "generated/array.php", + "generated/bzip2.php", + "generated/calendar.php", + "generated/classobj.php", + "generated/com.php", + "generated/cubrid.php", + "generated/curl.php", + "generated/datetime.php", + "generated/dir.php", + "generated/eio.php", + "generated/errorfunc.php", + "generated/exec.php", + "generated/fileinfo.php", + "generated/filesystem.php", + "generated/filter.php", + "generated/fpm.php", + "generated/ftp.php", + "generated/funchand.php", + "generated/gmp.php", + "generated/gnupg.php", + "generated/hash.php", + "generated/ibase.php", + "generated/ibmDb2.php", + "generated/iconv.php", + "generated/image.php", + "generated/imap.php", + "generated/info.php", + "generated/ingres-ii.php", + "generated/inotify.php", + "generated/json.php", + "generated/ldap.php", + "generated/libxml.php", + "generated/lzf.php", + "generated/mailparse.php", + "generated/mbstring.php", + "generated/misc.php", + "generated/msql.php", + "generated/mysql.php", + "generated/mysqli.php", + "generated/mysqlndMs.php", + "generated/mysqlndQc.php", + "generated/network.php", + "generated/oci8.php", + "generated/opcache.php", + "generated/openssl.php", + "generated/outcontrol.php", + "generated/password.php", + "generated/pcntl.php", + "generated/pcre.php", + "generated/pdf.php", + "generated/pgsql.php", + "generated/posix.php", + "generated/ps.php", + "generated/pspell.php", + "generated/readline.php", + "generated/rpminfo.php", + "generated/rrd.php", + "generated/sem.php", + "generated/session.php", + "generated/shmop.php", + "generated/simplexml.php", + "generated/sockets.php", + "generated/sodium.php", + "generated/solr.php", + "generated/spl.php", + "generated/sqlsrv.php", + "generated/ssdeep.php", + "generated/ssh2.php", + "generated/stream.php", + "generated/strings.php", + "generated/swoole.php", + "generated/uodbc.php", + "generated/uopz.php", + "generated/url.php", + "generated/var.php", + "generated/xdiff.php", + "generated/xml.php", + "generated/xmlrpc.php", + "generated/yaml.php", + "generated/yaz.php", + "generated/zip.php", + "generated/zlib.php" ], "psr-4": { - "Amp\\ByteStream\\": "lib" + "Safe\\": [ + "lib/", + "deprecated/", + "generated/" + ] } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - } - ], - "description": "A stream abstraction to make working with non-blocking I/O simple.", - "homepage": "http://amphp.org/byte-stream", - "keywords": [ - "amp", - "amphp", - "async", - "io", - "non-blocking", - "stream" - ], + "description": "PHP core functions that throw exceptions instead of returning FALSE on error", "support": { - "irc": "irc://irc.freenode.org/amphp", - "issues": "https://github.com/amphp/byte-stream/issues", - "source": "https://github.com/amphp/byte-stream/tree/v1.8.1" + "issues": "https://github.com/thecodingmachine/safe/issues", + "source": "https://github.com/thecodingmachine/safe/tree/v1.3.3" }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2021-03-30T17:13:30+00:00" - }, + "time": "2020-10-28T17:51:34+00:00" + } + ], + "packages-dev": [ { "name": "composer/pcre", "version": "3.1.1", @@ -8708,5 +10007,5 @@ "ext-iconv": "*" }, "platform-dev": [], - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.2.0" } diff --git a/config/bootstrap.php b/app/config/bootstrap.php similarity index 100% rename from config/bootstrap.php rename to app/config/bootstrap.php diff --git a/config/bundles.php b/app/config/bundles.php similarity index 77% rename from config/bundles.php rename to app/config/bundles.php index 49c5b01..8367e24 100644 --- a/config/bundles.php +++ b/app/config/bundles.php @@ -3,10 +3,9 @@ return [ Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true], Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], - Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true], Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true], - Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true], Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], + KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true], ]; diff --git a/config/jwt/.gitkeep b/app/config/jwt/.gitkeep similarity index 100% rename from config/jwt/.gitkeep rename to app/config/jwt/.gitkeep diff --git a/config/packages/cache.yaml b/app/config/packages/cache.yaml similarity index 100% rename from config/packages/cache.yaml rename to app/config/packages/cache.yaml diff --git a/config/packages/doctrine.yaml b/app/config/packages/doctrine.yaml similarity index 93% rename from config/packages/doctrine.yaml rename to app/config/packages/doctrine.yaml index fac0386..895dd7a 100644 --- a/config/packages/doctrine.yaml +++ b/app/config/packages/doctrine.yaml @@ -9,7 +9,7 @@ doctrine: mappings: App: is_bundle: false - type: annotation + type: attribute dir: '%kernel.project_dir%/src/Flags/Entity' prefix: 'App\Flags\Entity' alias: App \ No newline at end of file diff --git a/config/packages/doctrine_migrations.yaml b/app/config/packages/doctrine_migrations.yaml similarity index 100% rename from config/packages/doctrine_migrations.yaml rename to app/config/packages/doctrine_migrations.yaml diff --git a/config/packages/framework.yaml b/app/config/packages/framework.yaml similarity index 66% rename from config/packages/framework.yaml rename to app/config/packages/framework.yaml index 6089f4b..b679507 100644 --- a/config/packages/framework.yaml +++ b/app/config/packages/framework.yaml @@ -10,6 +10,10 @@ framework: cookie_secure: auto cookie_samesite: lax + # Trust proxy headers (k8s, Caddy, ngrok) + trusted_proxies: '127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16' + trusted_headers: ['x-forwarded-for', 'x-forwarded-host', 'x-forwarded-port', 'x-forwarded-proto'] + #esi: true #fragments: true php_errors: diff --git a/app/config/packages/knpu_oauth2_client.yaml b/app/config/packages/knpu_oauth2_client.yaml new file mode 100644 index 0000000..395aa76 --- /dev/null +++ b/app/config/packages/knpu_oauth2_client.yaml @@ -0,0 +1,18 @@ + +# configure your clients as described here: https://github.com/knpuniversity/oauth2-client-bundle#configuration +# config/packages/knpu_oauth2_client.yaml +knpu_oauth2_client: + clients: + flags_app: + type: generic + provider_class: App\Flags\Service\HqAuthProvider + client_id: '%env(OAUTH_CLIENT_ID)%' + client_secret: '%env(OAUTH_CLIENT_SECRET)%' + redirect_route: oauth_check + redirect_params: {} + # Your OAuth2 server's base URL + provider_options: + domain: '%env(OAUTH_SERVER_URL)%' + urlAuthorize: '%env(OAUTH_SERVER_URL)%/oauth2/authorize' + urlAccessToken: '%env(OAUTH_SERVER_URL)%/oauth2/token' + urlResourceOwnerDetails: '%env(OAUTH_SERVER_URL)%/oauth2/me' \ No newline at end of file diff --git a/app/config/packages/lexik_jwt_authentication.yaml b/app/config/packages/lexik_jwt_authentication.yaml new file mode 100644 index 0000000..efdae4a --- /dev/null +++ b/app/config/packages/lexik_jwt_authentication.yaml @@ -0,0 +1,7 @@ +lexik_jwt_authentication: + # secret_key: '%env(resolve:JWT_SECRET_KEY)%' + public_key: '%env(resolve:JWT_PUBLIC_KEY)%' + pass_phrase: '%env(JWT_PASSPHRASE)%' + token_ttl: '%env(JWT_TOKEN_TTL)%' + user_id_claim: sub + # TODO - add public key from oauth server to reuse their accesstokens! diff --git a/config/packages/prod/doctrine.yaml b/app/config/packages/prod/doctrine.yaml similarity index 100% rename from config/packages/prod/doctrine.yaml rename to app/config/packages/prod/doctrine.yaml diff --git a/config/packages/prod/routing.yaml b/app/config/packages/prod/routing.yaml similarity index 100% rename from config/packages/prod/routing.yaml rename to app/config/packages/prod/routing.yaml diff --git a/config/packages/routing.yaml b/app/config/packages/routing.yaml similarity index 100% rename from config/packages/routing.yaml rename to app/config/packages/routing.yaml diff --git a/app/config/packages/secrity.yaml b/app/config/packages/secrity.yaml new file mode 100644 index 0000000..fc335d7 --- /dev/null +++ b/app/config/packages/secrity.yaml @@ -0,0 +1,76 @@ +security: + enable_authenticator_manager: true + + providers: + database_provider: + entity: + class: App\Flags\Entity\User + property: sub + + password_hashers: + App\Flags\Entity\User: + algorithm: bcrypt + + firewalls: + dev: + pattern: ^/(_(profiler|wdt)|css|images|js)/ + security: false + + # NEW: Dedicated firewall to handle API Preflights (OPTIONS) + api_preflight: + pattern: ^/api + methods: [OPTIONS] + security: false # Bypasses all security/authenticators for OPTIONS + + # OAuth login flow (needs sessions for state parameter) + oauth: + pattern: ^/(login|oauth) + stateless: false + provider: database_provider + custom_authenticators: + - App\Flags\Security\HqAuthAuthenticator + + # Public API endpoints (no auth required) + api_public: + pattern: ^/api/flags/scores$ + methods: [GET] + security: false + + # API endpoints protected by JWT + api: + pattern: ^/api + stateless: true + provider: database_provider + jwt: ~ + + # Game endpoints protected by JWT + flags: + pattern: ^/flags + stateless: true + provider: database_provider + jwt: ~ + + capitals: + pattern: ^/capitals + stateless: true + provider: database_provider + jwt: ~ + + # Main firewall for web pages + main: + lazy: true + provider: database_provider + logout: + path: app_logout + + access_control: + # Allow OPTIONS requests for all API paths without a token + - { path: ^/login, roles: PUBLIC_ACCESS } + - { path: ^/oauth/check, roles: PUBLIC_ACCESS } + - { path: ^/api/public, roles: PUBLIC_ACCESS } + - { path: ^/capitals/test$, roles: PUBLIC_ACCESS } + - { path: ^/capitals/high-scores, roles: PUBLIC_ACCESS } + - { path: ^/api, methods: [OPTIONS], roles: PUBLIC_ACCESS } + - { path: ^/capitals, methods: [ OPTIONS ], roles: PUBLIC_ACCESS } + - { path: ^/api, roles: ROLE_USER } + - { path: ^/capitals, roles: ROLE_USER } diff --git a/app/config/packages/security.yaml.bkp b/app/config/packages/security.yaml.bkp new file mode 100644 index 0000000..937e999 --- /dev/null +++ b/app/config/packages/security.yaml.bkp @@ -0,0 +1,96 @@ +security: + enable_authenticator_manager: true + + providers: + database_provider: + entity: + class: App\Flags\Entity\User + property: email + + password_hashers: + App\Flags\Entity\User: + algorithm: bcrypt + + firewalls: + dev: + pattern: ^/(_(profiler|wdt)|css|images|js)/ + security: false + + main: + lazy: true + provider: database_provider + + stateless: true # Important for API! + jwt: ~ # Enable JWT authentication + + custom_authenticators: + - App\Flags\Security\HqAuthAuthenticator + - lexik_jwt_authentication.jwt_token_authenticator # For JWT validation + + logout: + path: app_logout + target: app_login + + access_control: + - { path: ^/login, roles: PUBLIC_ACCESS } + - { path: ^/oauth/check, roles: PUBLIC_ACCESS } + - { path: ^/check, roles: PUBLIC_ACCESS } + - { path: ^/capitals/test, roles: PUBLIC_ACCESS } + - { path: ^/capitals/test2, roles: ROLE_USER } + - { path: ^/capitals/high-scores, roles: PUBLIC_ACCESS } + - { path: ^/api/tg/login, roles: PUBLIC_ACCESS } + - { path: ^/test, roles: ROLE_USER } + - { path: ^/profile, roles: ROLE_USER } + - { path: ^/capitals, roles: ROLE_USER } + + +#security: +# enable_authenticator_manager: true +# # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers +# providers: +# database_provider: +# entity: +# class: App\Flags\Entity\User +## property: telegramId +# # Add a separate provider for OAuth users if needed +## oauth_user_provider: +## entity: +## class: App\Flags\Entity\User +## property: email # or whatever identifier your OAuth returns +# password_hashers: +# App\Entity\User: +# algorithm: bcrypt +# firewalls: +# dev: +# pattern: ^/(_(profiler|wdt)|css|images|js)/ +# security: false +# main: +## jwt: ~ +# lazy: true +# provider: database_provider +# +# custom_authenticators: +# - App\Flags\Security\HqAuthAuthenticator +## - lexic_jwt_authentication.jwt_token_authenticator +# +# logout: +# path: app_logout +# target: app_login +## main: +## jwt: ~ +# # activate different ways to authenticate +# # https://symfony.com/doc/current/security.html#firewalls-authentication +# +# # Easy way to control access for large sections of your site +# # Note: Only the *first* access control that matches will be used +# access_control: +# - { path: ^/login, roles: PUBLIC_ACCESS } +# - { path: ^/oauth/check, roles: PUBLIC_ACCESS } +# - { path: ^/check, roles: PUBLIC_ACCESS } +# - { path: ^/capitals/test, roles: PUBLIC_ACCESS } +# - { path: ^/capitals/test2, roles: ROLE_USER } +# - { path: ^/capitals/high-scores, roles: PUBLIC_ACCESS } +# - { path: ^/api/tg/login, roles: PUBLIC_ACCESS } +# - { path: ^/test, roles: ROLE_USER } +# - { path: ^/profile, roles: ROLE_USER } +# - { path: ^/capitals, roles: ROLE_USER } diff --git a/config/packages/test/framework.yaml b/app/config/packages/test/framework.yaml similarity index 100% rename from config/packages/test/framework.yaml rename to app/config/packages/test/framework.yaml diff --git a/config/packages/test/validator.yaml b/app/config/packages/test/validator.yaml similarity index 100% rename from config/packages/test/validator.yaml rename to app/config/packages/test/validator.yaml diff --git a/config/packages/validator.yaml b/app/config/packages/validator.yaml similarity index 100% rename from config/packages/validator.yaml rename to app/config/packages/validator.yaml diff --git a/config/preload.php b/app/config/preload.php similarity index 100% rename from config/preload.php rename to app/config/preload.php diff --git a/config/routes.yaml b/app/config/routes.yaml similarity index 100% rename from config/routes.yaml rename to app/config/routes.yaml diff --git a/config/routes/attributes.yaml b/app/config/routes/attributes.yaml similarity index 100% rename from config/routes/attributes.yaml rename to app/config/routes/attributes.yaml diff --git a/config/routes/framework.yaml b/app/config/routes/framework.yaml similarity index 100% rename from config/routes/framework.yaml rename to app/config/routes/framework.yaml diff --git a/config/secrets/dev/dev.MYSQL_ROOT_PASSWORD.c07213.php b/app/config/secrets/dev/dev.MYSQL_ROOT_PASSWORD.c07213.php similarity index 100% rename from config/secrets/dev/dev.MYSQL_ROOT_PASSWORD.c07213.php rename to app/config/secrets/dev/dev.MYSQL_ROOT_PASSWORD.c07213.php diff --git a/config/secrets/dev/dev.encrypt.public.php b/app/config/secrets/dev/dev.encrypt.public.php similarity index 100% rename from config/secrets/dev/dev.encrypt.public.php rename to app/config/secrets/dev/dev.encrypt.public.php diff --git a/config/secrets/dev/dev.list.php b/app/config/secrets/dev/dev.list.php similarity index 100% rename from config/secrets/dev/dev.list.php rename to app/config/secrets/dev/dev.list.php diff --git a/config/secrets/prod/prod.BOT_TOKEN.f41697.php b/app/config/secrets/prod/prod.BOT_TOKEN.f41697.php similarity index 100% rename from config/secrets/prod/prod.BOT_TOKEN.f41697.php rename to app/config/secrets/prod/prod.BOT_TOKEN.f41697.php diff --git a/config/secrets/prod/prod.DATABASE_URL.8ea85a.php b/app/config/secrets/prod/prod.DATABASE_URL.8ea85a.php similarity index 100% rename from config/secrets/prod/prod.DATABASE_URL.8ea85a.php rename to app/config/secrets/prod/prod.DATABASE_URL.8ea85a.php diff --git a/config/secrets/prod/prod.MYSQL_DATABASE.bef43b.php b/app/config/secrets/prod/prod.MYSQL_DATABASE.bef43b.php similarity index 100% rename from config/secrets/prod/prod.MYSQL_DATABASE.bef43b.php rename to app/config/secrets/prod/prod.MYSQL_DATABASE.bef43b.php diff --git a/config/secrets/prod/prod.MYSQL_PASSWORD.32327a.php b/app/config/secrets/prod/prod.MYSQL_PASSWORD.32327a.php similarity index 100% rename from config/secrets/prod/prod.MYSQL_PASSWORD.32327a.php rename to app/config/secrets/prod/prod.MYSQL_PASSWORD.32327a.php diff --git a/config/secrets/prod/prod.MYSQL_ROOT_PASSWORD.c07213.php b/app/config/secrets/prod/prod.MYSQL_ROOT_PASSWORD.c07213.php similarity index 100% rename from config/secrets/prod/prod.MYSQL_ROOT_PASSWORD.c07213.php rename to app/config/secrets/prod/prod.MYSQL_ROOT_PASSWORD.c07213.php diff --git a/config/secrets/prod/prod.MYSQL_USER.8e3cb8.php b/app/config/secrets/prod/prod.MYSQL_USER.8e3cb8.php similarity index 100% rename from config/secrets/prod/prod.MYSQL_USER.8e3cb8.php rename to app/config/secrets/prod/prod.MYSQL_USER.8e3cb8.php diff --git a/config/secrets/prod/prod.encrypt.public.php b/app/config/secrets/prod/prod.encrypt.public.php similarity index 100% rename from config/secrets/prod/prod.encrypt.public.php rename to app/config/secrets/prod/prod.encrypt.public.php diff --git a/config/secrets/prod/prod.list.php b/app/config/secrets/prod/prod.list.php similarity index 100% rename from config/secrets/prod/prod.list.php rename to app/config/secrets/prod/prod.list.php diff --git a/config/secrets/staging/staging.BOT_TOKEN.f41697.php b/app/config/secrets/staging/staging.BOT_TOKEN.f41697.php similarity index 100% rename from config/secrets/staging/staging.BOT_TOKEN.f41697.php rename to app/config/secrets/staging/staging.BOT_TOKEN.f41697.php diff --git a/config/secrets/staging/staging.DATABASE_URL.8ea85a.php b/app/config/secrets/staging/staging.DATABASE_URL.8ea85a.php similarity index 100% rename from config/secrets/staging/staging.DATABASE_URL.8ea85a.php rename to app/config/secrets/staging/staging.DATABASE_URL.8ea85a.php diff --git a/config/secrets/staging/staging.MYSQL_DATABASE.bef43b.php b/app/config/secrets/staging/staging.MYSQL_DATABASE.bef43b.php similarity index 100% rename from config/secrets/staging/staging.MYSQL_DATABASE.bef43b.php rename to app/config/secrets/staging/staging.MYSQL_DATABASE.bef43b.php diff --git a/config/secrets/staging/staging.MYSQL_PASSWORD.32327a.php b/app/config/secrets/staging/staging.MYSQL_PASSWORD.32327a.php similarity index 100% rename from config/secrets/staging/staging.MYSQL_PASSWORD.32327a.php rename to app/config/secrets/staging/staging.MYSQL_PASSWORD.32327a.php diff --git a/config/secrets/staging/staging.MYSQL_ROOT.1ea6ef.php b/app/config/secrets/staging/staging.MYSQL_ROOT.1ea6ef.php similarity index 100% rename from config/secrets/staging/staging.MYSQL_ROOT.1ea6ef.php rename to app/config/secrets/staging/staging.MYSQL_ROOT.1ea6ef.php diff --git a/config/secrets/staging/staging.MYSQL_USER.8e3cb8.php b/app/config/secrets/staging/staging.MYSQL_USER.8e3cb8.php similarity index 100% rename from config/secrets/staging/staging.MYSQL_USER.8e3cb8.php rename to app/config/secrets/staging/staging.MYSQL_USER.8e3cb8.php diff --git a/config/secrets/staging/staging.encrypt.public.php b/app/config/secrets/staging/staging.encrypt.public.php similarity index 100% rename from config/secrets/staging/staging.encrypt.public.php rename to app/config/secrets/staging/staging.encrypt.public.php diff --git a/config/secrets/staging/staging.list.php b/app/config/secrets/staging/staging.list.php similarity index 100% rename from config/secrets/staging/staging.list.php rename to app/config/secrets/staging/staging.list.php diff --git a/config/services.yaml b/app/config/services.yaml similarity index 74% rename from config/services.yaml rename to app/config/services.yaml index 3dc86e6..15baa05 100644 --- a/config/services.yaml +++ b/app/config/services.yaml @@ -18,6 +18,8 @@ services: App\Flags\: resource: '../src/Flags/*' exclude: '../src/Flags/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}' + App\Command\: + resource: '../src/Command/*' # controllers are imported separately to make sure services can be injected # as action arguments even if you don't extend any base controller class @@ -29,4 +31,18 @@ services: # please note that last definitions always *replace* previous ones App\DataFixtures\: - resource: '../src/DataFixtures/*' \ No newline at end of file + resource: '../src/DataFixtures/*' + + App\EventListener\NgrokHeaderListener: + tags: + - { name: kernel.event_subscriber } + + App\EventListener\JsonExceptionListener: + arguments: + $environment: '%kernel.environment%' + tags: + - { name: kernel.event_subscriber } + + App\EventListener\JwtAuthenticationFailureListener: + tags: + - { name: kernel.event_subscriber } \ No newline at end of file diff --git a/app/docker-compose.override.yml b/app/docker-compose.override.yml new file mode 100644 index 0000000..5d2ed68 --- /dev/null +++ b/app/docker-compose.override.yml @@ -0,0 +1,23 @@ +services: + php: + build: + args: + USER_ID: 1000 + GROUP_ID: 1000 + target: development + volumes: + - .:/var/www/html:rw,cached + db: + ports: + - "3306:3306" + caddy: + ports: + - "8000:80" + - "4430:443" + volumes: + - ./:/var/www/html:cached + - caddy_data:/data + - caddy_config:/config + - caddy_logs:/var/log/caddy + build: + dockerfile: .docker/caddy/Dockerfile \ No newline at end of file diff --git a/docker-compose.yml b/app/docker-compose.yml similarity index 69% rename from docker-compose.yml rename to app/docker-compose.yml index 8e8b3af..41136c4 100644 --- a/docker-compose.yml +++ b/app/docker-compose.yml @@ -1,32 +1,29 @@ -version: "3.8" - services: php: - container_name: "php-${PROJECT_NAME}" + container_name: "php-flags-api" build: - args: - KEY: ${GITH_KEY} - context: . + context: .. dockerfile: .docker/php-fpm/Dockerfile + target: production environment: SYMFONY_DECRYPTION_SECRET: "${SYMFONY_DECRYPTION_SECRET}" - depends_on: - - nginx networks: - backend-flags - nginx: - container_name: "nginx-${PROJECT_NAME}" + caddy: + container_name: "caddy-flags-api" build: - context: . - dockerfile: .docker/nginx/Dockerfile + context: .. + dockerfile: .docker/caddy/Dockerfile.prod restart: always networks: - backend-flags + depends_on: + - php db: build: - context: . + context: .. dockerfile: .docker/mysql/Dockerfile - container_name: "db-${PROJECT_NAME}" + container_name: "db-flags-api" environment: MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD}" MYSQL_DATABASE: "${MYSQL_DATABASE}" @@ -42,3 +39,6 @@ networks: external: true volumes: db-data-flags: ~ + caddy_logs: ~ + caddy_data: ~ + caddy_config: ~ \ No newline at end of file diff --git a/docker.md b/app/docker.md similarity index 100% rename from docker.md rename to app/docker.md diff --git a/flags.sql b/app/flags.sql similarity index 100% rename from flags.sql rename to app/flags.sql diff --git a/app/http-requests/Correct.http b/app/http-requests/Correct.http new file mode 100644 index 0000000..2080992 --- /dev/null +++ b/app/http-requests/Correct.http @@ -0,0 +1,6 @@ +### GET request to example server +GET https://369c0297b669.ngrok-free.app/api/correct +Authorization: Bearer {{t1}} +ngrok-skip-browser-warning: true + +###http://localhost/test \ No newline at end of file diff --git a/app/http-requests/Incorrect.http b/app/http-requests/Incorrect.http new file mode 100644 index 0000000..19344f6 --- /dev/null +++ b/app/http-requests/Incorrect.http @@ -0,0 +1,6 @@ +### GET request to example server +GET https://369c0297b669.ngrok-free.app/api/incorrect +Authorization: Bearer {{t1}} +ngrok-skip-browser-warning: true + +###http://localhost/test \ No newline at end of file diff --git a/app/http-requests/OAuth2-login.http b/app/http-requests/OAuth2-login.http new file mode 100644 index 0000000..8388776 --- /dev/null +++ b/app/http-requests/OAuth2-login.http @@ -0,0 +1,3 @@ +### GET request to example server +GET https://auth.izeebot.top/oauth2/authorize?client_id=flags_app&redirect_uri={{redirect_url}}&response_type=code&scope=openid +### \ No newline at end of file diff --git a/app/http-requests/Profile.http b/app/http-requests/Profile.http new file mode 100644 index 0000000..931a691 --- /dev/null +++ b/app/http-requests/Profile.http @@ -0,0 +1,6 @@ +### GET request to example server +GET https://localhost:4430/api/profile +Authorization: Bearer {{t1}} +ngrok-skip-browser-warning: true + +###http://localhost/test \ No newline at end of file diff --git a/app/http-requests/ProtectedEndpoint.http b/app/http-requests/ProtectedEndpoint.http new file mode 100644 index 0000000..1f574ff --- /dev/null +++ b/app/http-requests/ProtectedEndpoint.http @@ -0,0 +1,6 @@ +### GET request to example server +GET https://369c0297b669.ngrok-free.app/api/protected +Authorization: Bearer {{t1}} +ngrok-skip-browser-warning: true + +###http://localhost/test \ No newline at end of file diff --git a/app/http-requests/Protected[OPTIONS].http b/app/http-requests/Protected[OPTIONS].http new file mode 100644 index 0000000..574958f --- /dev/null +++ b/app/http-requests/Protected[OPTIONS].http @@ -0,0 +1,5 @@ +### GET request to example server +OPTIONS https://localhost:4430/api/protected +ngrok-skip-browser-warning: true + +###http://localhost/test \ No newline at end of file diff --git a/app/http-requests/Test Auth.http b/app/http-requests/Test Auth.http new file mode 100644 index 0000000..a6b4e6f --- /dev/null +++ b/app/http-requests/Test Auth.http @@ -0,0 +1,5 @@ +### GET request to example server +GET http://localhost/test +Authorization: Bearer {{oauth_token}} + +### \ No newline at end of file diff --git a/app/http-requests/capitals-question.http b/app/http-requests/capitals-question.http new file mode 100644 index 0000000..214ef8e --- /dev/null +++ b/app/http-requests/capitals-question.http @@ -0,0 +1,21 @@ +### Submit game score +GET {{url}}/capitals +Authorization: Bearer {{0xNTK_token}} +Content-Type: application/json + +### Submit game score +GET {{url}}/capitals/test +Authorization: Bearer {{0xNTK_token}} +Content-Type: application/json + + +### Submit game score +GET {{url}}/capitals/game-start/CAPITALS_EUROPE +Authorization: Bearer {{0xNTK_token}} +Content-Type: application/json + + +### Submit game score +GET {{url}}/capitals/question/4 +Authorization: Bearer {{0xNTK_token}} +Content-Type: application/json diff --git a/app/http-requests/capitals/answer.http b/app/http-requests/capitals/answer.http new file mode 100644 index 0000000..88bff8c --- /dev/null +++ b/app/http-requests/capitals/answer.http @@ -0,0 +1,19 @@ +### Submit answer for a game +# URL format: /capitals/answer/{gameId}/{countryCode}/{base64EncodedAnswer} +# Example: answering "Paris" for France (FR) in game 1 +# base64("Paris") = UGFyaXM= +GET {{url}}/capitals/answer/5/FR/UGFyaXM= +Authorization: Bearer {{0xNTK_token}} +Content-Type: application/json + +### Answer example: "Berlin" for Germany (DE) +# base64("Berlin") = QmVybGlu +GET {{url}}/capitals/answer/1/DE/QmVybGlu +Authorization: Bearer {{0xNTK_token}} +Content-Type: application/json + +### Answer example: "Madrid" for Spain (ES) +# base64("Madrid") = TWFkcmlk +GET {{url}}/capitals/answer/1/ES/TWFkcmlk +Authorization: Bearer {{0xNTK_token}} +Content-Type: application/json diff --git a/app/http-requests/capitals/game-over.http b/app/http-requests/capitals/game-over.http new file mode 100644 index 0000000..cffd50e --- /dev/null +++ b/app/http-requests/capitals/game-over.http @@ -0,0 +1,6 @@ +### Submit game results +POST {{url}}/capitals/game-over +Authorization: Bearer {{0xNTK_token}} +Content-Type: application/json + +{"gameId": 5, "score": 10, "sessionTimer": 45} diff --git a/app/http-requests/capitals/game-start.http b/app/http-requests/capitals/game-start.http new file mode 100644 index 0000000..51df1dd --- /dev/null +++ b/app/http-requests/capitals/game-start.http @@ -0,0 +1,24 @@ +### Start Europe game +GET {{url}}/capitals/game-start/CAPITALS_EUROPE +Authorization: Bearer {{0xNTK_token}} +Content-Type: application/json + +### Start Asia game +GET {{url}}/capitals/game-start/CAPITALS_ASIA +Authorization: Bearer {{0xNTK_token}} +Content-Type: application/json + +### Start Africa game +GET {{url}}/capitals/game-start/CAPITALS_AFRICA +Authorization: Bearer {{0xNTK_token}} +Content-Type: application/json + +### Start Americas game +GET {{url}}/capitals/game-start/CAPITALS_AMERICAS +Authorization: Bearer {{0xNTK_token}} +Content-Type: application/json + +### Start Oceania game +GET {{url}}/capitals/game-start/CAPITALS_OCEANIA +Authorization: Bearer {{0xNTK_token}} +Content-Type: application/json diff --git a/app/http-requests/capitals/high-scores.http b/app/http-requests/capitals/high-scores.http new file mode 100644 index 0000000..54c7fa2 --- /dev/null +++ b/app/http-requests/capitals/high-scores.http @@ -0,0 +1,19 @@ +### Get Europe high scores +GET {{url}}/capitals/high-scores/europe +Content-Type: application/json + +### Get Asia high scores +GET {{url}}/capitals/high-scores/asia +Content-Type: application/json + +### Get Africa high scores +GET {{url}}/capitals/high-scores/africa +Content-Type: application/json + +### Get Americas high scores +GET {{url}}/capitals/high-scores/americas +Content-Type: application/json + +### Get Oceania high scores +GET {{url}}/capitals/high-scores/oceania +Content-Type: application/json diff --git a/app/http-requests/capitals/question.http b/app/http-requests/capitals/question.http new file mode 100644 index 0000000..de49270 --- /dev/null +++ b/app/http-requests/capitals/question.http @@ -0,0 +1,9 @@ +### Get random question (no game context) +GET {{url}}/capitals +Authorization: Bearer {{0xNTK_token}} +Content-Type: application/json + +### Get question for specific game (replace {gameId} with actual game ID) +GET {{url}}/capitals/question/5 +Authorization: Bearer {{0xNTK_token}} +Content-Type: application/json diff --git a/app/http-requests/capitals/telegram-login.http b/app/http-requests/capitals/telegram-login.http new file mode 100644 index 0000000..3c4c150 --- /dev/null +++ b/app/http-requests/capitals/telegram-login.http @@ -0,0 +1,5 @@ +### Telegram login +# Query params: id, first_name, last_name, username, photo_url, auth_date, hash +# The hash is calculated from the data using bot token +GET {{url}}/api/tg/login?id=123456789&first_name=John&last_name=Doe&username=johndoe&auth_date=1700000000&hash=abc123 +Content-Type: application/json diff --git a/app/http-requests/capitals/test.http b/app/http-requests/capitals/test.http new file mode 100644 index 0000000..e8c2436 --- /dev/null +++ b/app/http-requests/capitals/test.http @@ -0,0 +1,9 @@ +### Test endpoint 1 +GET {{url}}/capitals/test +Authorization: Bearer {{0xNTK_token}} +Content-Type: application/json + +### Test endpoint 2 +GET {{url}}/capitals/test2 +Authorization: Bearer {{0xNTK_token}} +Content-Type: application/json diff --git a/app/http-requests/flags-correct.http b/app/http-requests/flags-correct.http new file mode 100644 index 0000000..d6dc203 --- /dev/null +++ b/app/http-requests/flags-correct.http @@ -0,0 +1,4 @@ +### Submit correct flag answer +POST {{url}}/api/flags/correct/au +Authorization: Bearer {{0xNTK_token}} +Content-Type: application/json diff --git a/app/http-requests/flags-scores.http b/app/http-requests/flags-scores.http new file mode 100644 index 0000000..684f05a --- /dev/null +++ b/app/http-requests/flags-scores.http @@ -0,0 +1,11 @@ +### Submit game score +POST {{url}}/api/flags/scores +Authorization: Bearer {{0xNTK_token}} +Content-Type: application/json + +{"score":0,"sessionTimer":45,"answers":[]} + + +### Submit game score +GET {{url}}/api/flags/scores +Content-Type: application/json \ No newline at end of file diff --git a/app/http-requests/http-client.env.json b/app/http-requests/http-client.env.json new file mode 100644 index 0000000..126ed65 --- /dev/null +++ b/app/http-requests/http-client.env.json @@ -0,0 +1,10 @@ +{ + "dev": { + "auth_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJmbGFnc19hcHAiLCJqdGkiOiIxM2EwMmU2NDg3NzYzNjcyODI2MzQwZWQ4NDA0NGMxMzMwY2E4MDgxYWYzNGEwYjJiYmE5MjgyNzI2M2RlYjU0MDY2Njk5MDJmZDU2NzQxNyIsImlhdCI6MTc2NTA1ODcwMS4xNjYyODksIm5iZiI6MTc2NTA1ODcwMS4xNjYyOTEsImV4cCI6MTc2NTA2MjMwMS4xNjI2Miwic3ViIjoiMSIsInNjb3BlcyI6WyJvcGVuaWQiLCJwcm9maWxlIiwiZW1haWwiXX0.aBzIJkh_Ri4wjCcQBHg9hucg5JkiZ7T-P22WBN0xcTvQbxbsjyrkyrW2vUR_wgpfvHPNVouA8QB-ndEVqU3LuTI7YNmMwMORLc33rdnrbU_nxgKPY_UEWGHWy85H_SWJyKiMewde48MS-fjXObKKnMMQI1YkpuzP6u6XcxrKYBinsVB-9XAHW5ct_r59gpZPMgKoHlZm2KaLAl3VHSVznov7yfsYBOWFZHepyoxRS1WqbDb5agJANN70tXAi5SPftff9EewzyULe5vxTpblkoAO1aYl7rMqWharWnwqRukGqKdAp1Jc2Vg1qxa0AINCjIFjM1HG8XKTue_MECiou_w", + "redirect_url": "https://3bd5404da6bd.ngrok-free.app/oauth/check", + "oauth_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJmbGFnc19hcHAiLCJqdGkiOiI2N2VkOTk3NmZiMWUxZjFiNDEzOTBlNTEyMTYyOWFhMjEzYmJkNmVhN2ZiOGU2OWJmM2RlYTE3NmI3OWE5YWIwZTA3ZjI5OGQxYjFkYjljYSIsImlhdCI6MTc2NTA3NDc0MS43NzQ0MjIsIm5iZiI6MTc2NTA3NDc0MS43NzQ0MjMsImV4cCI6MTc2NTA3ODM0MS43Njc2MDYsInN1YiI6IjEiLCJzY29wZXMiOlsib3BlbmlkIiwicHJvZmlsZSIsImVtYWlsIl19.R8ZoDkMrFlpW9TtPyEhjpypc_HX-0G1O2ZeXEAYZL_V8bQsyBCcsztZMlTlRh8pGhVAl59n2JIb3OVLu9asBxSfDjHLBKV2f2bMBTGV9PrIxPWcI8rR5D2uK61GbEHenIIijJYeivG3cKpir_tqGtGbrb-O5ytd5v0Bbc97QwZz9DRn75yupVRShEVhWRGQeAtbgYRaru0dZiAvHYq3suLcQF1IFfNFZyenn1FgMblMP0DubWeu2VSX4DZE0ulMlcoeRfQtKzb0ZPXF0VjmXh3fS7b01r39a01dTayo6hGEhZBsa5GuT_blAocQ0KBgtFkYxc-fQeSo6NUKoXT2jmA", + "t1" : "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJmbGFnc19hcHAiLCJqdGkiOiJkNzE1OTc5YmE0YjNiZDJkODIzZjYzYWYyY2Q5N2M2OWU0YzI0OWRlOWMxN2I3Njc0MTQ3NTA0YWZhZGU2YjFlOTA1NmIyMWRkMzA3YWI4OSIsImlhdCI6MTc2NTEyMzEzMi4zNTc0MTMsIm5iZiI6MTc2NTEyMzEzMi4zNTc0MTcsImV4cCI6MTc2NTEyNjczMi4zNTAzMiwic3ViIjoiMSIsInNjb3BlcyI6WyJvcGVuaWQiLCJwcm9maWxlIiwiZW1haWwiXX0.IeX6GCpHqZG5kJ1JoVxFE8gy7v1NOz6vpPPF7GqlJsKL4kaW8-KfsmElfZEk3XVVOvCEeZv01sO7fM5-sr9mhLPKfhHpPf9Sr0Roq80klKJFW3YJ2RSEfPHWnz3PAbSOB6wM00FkXNqixsW4hUVj_q7aIJ4KNy_WA-_A-K17J2n6WodsNvJzIE94G499fKfXfMJJiuxeZntdTzaPT8_p9MR0ORaXGc-8GmpxR-iS_uHftGT4fBSxGY4eiAIPtwZ9SBeluZL7OqKbfyFN5kAJW_iT4H7hRQUxZSrXc_g2il2Kc2zlGRBb5t6yogrSNauzs9VAEZ6zW2i52L_WpVTOqw", + "url" : "http://localhost:8000", + "0xNTK_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJmbGFnc19hcHAiLCJqdGkiOiJhZjA3ZWU0YTJmNTUwOWYzNTYyNGU2NDJjNTVkYjA3MmEwNDBiMzc1YWRiNDU5NzJkODVlODQ4NWQxN2MxMTdjNGM5MjI0NGRlMmY1NjRiMCIsImlhdCI6MTc2NjYxMDM4NS4yNzYzNTcsIm5iZiI6MTc2NjYxMDM4NS4yNzYzNTksImV4cCI6MTc2NjYxMzk4NS4yNzE2MDEsInN1YiI6IjEiLCJzY29wZXMiOlsib3BlbmlkIiwicHJvZmlsZSIsImVtYWlsIl19.l1UVPo1YZqJV6c2TiW0DlWApKKrXEmLixjGYVZzUlSiia1owjYgmfFZwDkaXeYgggCf2IO-EX-kGwZniPpocSHdw0z9-nRNo2vqoNeF9T2ctTxQanDM2R_rQsOkEF8siNh7BrG_AICSu2QcWBD5Mn-JjzQ9qbAj1lDrxOFpJfwPh_CbIz2Z58rDSJAllnWvZo3Dj0YESwX7XyDyqBZEXgYGFN4PqQ9k-H3xTkn4orJrqm0gEHYRzpMzM6LyAXX55QUkMO5AYoksME67C3fUWtKM-n0Ancny6Osm_kIpR0t9hu9YByCLnUO3YXcEnTqJwFxa3rOmrPE26JToWsZri9w" + } +} \ No newline at end of file diff --git a/makefile b/app/makefile similarity index 64% rename from makefile rename to app/makefile index 776466a..cf879b9 100644 --- a/makefile +++ b/app/makefile @@ -1,18 +1,25 @@ include .env +default: welcome init: build-containers run-containers composer import-db run-api run: run-containers run-api +db: + @docker compose exec php bin/console d:d:c --if-not-exists + @docker compose exec php bin/console d:m:m -n build: @docker compose build -run-containers: +up: @docker compose up -d -run-api: - @symfony server:start +down: + @docker compose down +rebuild: down build up import-db: @bin/console d:d:i flags.sql composer: @docker compose exec php composer install -deploy: - @./vendor/bin/dep deploy production +psalm: + @docker compose exec php vendor/bin/psalm --no-cache +cache: + @docker compose exec php bin/console c:c welcome: @echo hi test: @@ -27,5 +34,7 @@ sh: @docker compose exec php sh dumper: @docker compose exec php vendor/bin/var-dump-server +network: + @docker network create backend-flags %: - @ + @ \ No newline at end of file diff --git a/migrations/.gitignore b/app/migrations/.gitignore similarity index 100% rename from migrations/.gitignore rename to app/migrations/.gitignore diff --git a/migrations/Version20200322114725.php b/app/migrations/Version20200322114725.php similarity index 74% rename from migrations/Version20200322114725.php rename to app/migrations/Version20200322114725.php index f500679..45bca18 100644 --- a/migrations/Version20200322114725.php +++ b/app/migrations/Version20200322114725.php @@ -20,7 +20,7 @@ public function getDescription() : string public function up(Schema $schema) : void { // this up() migration is auto-generated, please modify it to your needs - $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + $this->abortIf(!$this->connection->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\AbstractMySQLPlatform, 'Migration can only be executed safely on \'mysql\'.'); $this->addSql('CREATE TABLE user (id INT AUTO_INCREMENT NOT NULL, telegram_id VARCHAR(255) NOT NULL, first_name VARCHAR(255) DEFAULT NULL, last_name VARCHAR(255) DEFAULT NULL, telegram_username VARCHAR(255) DEFAULT NULL, telegram_photo_url VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); } @@ -28,7 +28,7 @@ public function up(Schema $schema) : void public function down(Schema $schema) : void { // this down() migration is auto-generated, please modify it to your needs - $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + $this->abortIf(!$this->connection->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\AbstractMySQLPlatform, 'Migration can only be executed safely on \'mysql\'.'); $this->addSql('DROP TABLE user'); } diff --git a/migrations/Version20210103221647.php b/app/migrations/Version20210103221647.php similarity index 67% rename from migrations/Version20210103221647.php rename to app/migrations/Version20210103221647.php index a1a3a57..2b606ab 100644 --- a/migrations/Version20210103221647.php +++ b/app/migrations/Version20210103221647.php @@ -20,7 +20,7 @@ public function getDescription() : string public function up(Schema $schema) : void { // this up() migration is auto-generated, please modify it to your needs - $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + $this->abortIf(!$this->connection->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\AbstractMySQLPlatform, 'Migration can only be executed safely on \'mysql\'.'); $this->addSql('ALTER TABLE user ADD high_score INT NOT NULL'); } @@ -28,7 +28,7 @@ public function up(Schema $schema) : void public function down(Schema $schema) : void { // this down() migration is auto-generated, please modify it to your needs - $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + $this->abortIf(!$this->connection->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\AbstractMySQLPlatform, 'Migration can only be executed safely on \'mysql\'.'); $this->addSql('ALTER TABLE user DROP high_score'); } diff --git a/migrations/Version20210109193151.php b/app/migrations/Version20210109193151.php similarity index 69% rename from migrations/Version20210109193151.php rename to app/migrations/Version20210109193151.php index 82e1975..579e8f4 100644 --- a/migrations/Version20210109193151.php +++ b/app/migrations/Version20210109193151.php @@ -20,7 +20,7 @@ public function getDescription() : string public function up(Schema $schema) : void { // this up() migration is auto-generated, please modify it to your needs - $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + $this->abortIf(!$this->connection->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\AbstractMySQLPlatform, 'Migration can only be executed safely on \'mysql\'.'); $this->addSql('ALTER TABLE user ADD games_total INT NOT NULL, ADD best_time INT NOT NULL'); } @@ -28,7 +28,7 @@ public function up(Schema $schema) : void public function down(Schema $schema) : void { // this down() migration is auto-generated, please modify it to your needs - $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + $this->abortIf(!$this->connection->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\AbstractMySQLPlatform, 'Migration can only be executed safely on \'mysql\'.'); $this->addSql('ALTER TABLE user DROP games_total, DROP best_time'); } diff --git a/migrations/Version20210109213855.php b/app/migrations/Version20210109213855.php similarity index 74% rename from migrations/Version20210109213855.php rename to app/migrations/Version20210109213855.php index 7d5e321..9c3a8d5 100644 --- a/migrations/Version20210109213855.php +++ b/app/migrations/Version20210109213855.php @@ -20,7 +20,7 @@ public function getDescription() : string public function up(Schema $schema) : void { // this up() migration is auto-generated, please modify it to your needs - $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + $this->abortIf(!$this->connection->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\AbstractMySQLPlatform, 'Migration can only be executed safely on \'mysql\'.'); $this->addSql('CREATE TABLE score (id INT AUTO_INCREMENT NOT NULL, session_timer VARCHAR(255) NOT NULL, score VARCHAR(255) NOT NULL, date DATETIME NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); $this->addSql('ALTER TABLE user ADD time_total INT NOT NULL'); @@ -29,7 +29,7 @@ public function up(Schema $schema) : void public function down(Schema $schema) : void { // this down() migration is auto-generated, please modify it to your needs - $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + $this->abortIf(!$this->connection->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\AbstractMySQLPlatform, 'Migration can only be executed safely on \'mysql\'.'); $this->addSql('DROP TABLE score'); $this->addSql('ALTER TABLE user DROP time_total'); diff --git a/migrations/Version20210109232957.php b/app/migrations/Version20210109232957.php similarity index 73% rename from migrations/Version20210109232957.php rename to app/migrations/Version20210109232957.php index 5a9300b..a7aa024 100644 --- a/migrations/Version20210109232957.php +++ b/app/migrations/Version20210109232957.php @@ -20,7 +20,7 @@ public function getDescription() : string public function up(Schema $schema) : void { // this up() migration is auto-generated, please modify it to your needs - $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + $this->abortIf(!$this->connection->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\AbstractMySQLPlatform, 'Migration can only be executed safely on \'mysql\'.'); $this->addSql('CREATE TABLE answer (id INT AUTO_INCREMENT NOT NULL, timer INT NOT NULL, flag_code VARCHAR(255) NOT NULL, answer_options VARCHAR(255) NOT NULL, correct TINYINT(1) NOT NULL, date DATETIME NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); } @@ -28,7 +28,7 @@ public function up(Schema $schema) : void public function down(Schema $schema) : void { // this down() migration is auto-generated, please modify it to your needs - $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + $this->abortIf(!$this->connection->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\AbstractMySQLPlatform, 'Migration can only be executed safely on \'mysql\'.'); $this->addSql('DROP TABLE answer'); } diff --git a/migrations/Version20210109233702.php b/app/migrations/Version20210109233702.php similarity index 75% rename from migrations/Version20210109233702.php rename to app/migrations/Version20210109233702.php index 29a462c..3ef99d2 100644 --- a/migrations/Version20210109233702.php +++ b/app/migrations/Version20210109233702.php @@ -20,7 +20,7 @@ public function getDescription() : string public function up(Schema $schema) : void { // this up() migration is auto-generated, please modify it to your needs - $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + $this->abortIf(!$this->connection->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\AbstractMySQLPlatform, 'Migration can only be executed safely on \'mysql\'.'); $this->addSql('ALTER TABLE user ADD answer_id INT DEFAULT NULL'); $this->addSql('ALTER TABLE user ADD CONSTRAINT FK_8D93D649AA334807 FOREIGN KEY (answer_id) REFERENCES answer (id)'); @@ -30,7 +30,7 @@ public function up(Schema $schema) : void public function down(Schema $schema) : void { // this down() migration is auto-generated, please modify it to your needs - $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + $this->abortIf(!$this->connection->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\AbstractMySQLPlatform, 'Migration can only be executed safely on \'mysql\'.'); $this->addSql('ALTER TABLE user DROP FOREIGN KEY FK_8D93D649AA334807'); $this->addSql('DROP INDEX IDX_8D93D649AA334807 ON user'); diff --git a/migrations/Version20210110000325.php b/app/migrations/Version20210110000325.php similarity index 82% rename from migrations/Version20210110000325.php rename to app/migrations/Version20210110000325.php index 39e1103..8c41a60 100644 --- a/migrations/Version20210110000325.php +++ b/app/migrations/Version20210110000325.php @@ -20,7 +20,7 @@ public function getDescription() : string public function up(Schema $schema) : void { // this up() migration is auto-generated, please modify it to your needs - $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + $this->abortIf(!$this->connection->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\AbstractMySQLPlatform, 'Migration can only be executed safely on \'mysql\'.'); $this->addSql('ALTER TABLE user DROP FOREIGN KEY FK_8D93D649AA334807'); $this->addSql('DROP INDEX IDX_8D93D649AA334807 ON user'); @@ -33,7 +33,7 @@ public function up(Schema $schema) : void public function down(Schema $schema) : void { // this down() migration is auto-generated, please modify it to your needs - $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + $this->abortIf(!$this->connection->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\AbstractMySQLPlatform, 'Migration can only be executed safely on \'mysql\'.'); $this->addSql('ALTER TABLE answer DROP FOREIGN KEY FK_DADD4A25A76ED395'); $this->addSql('DROP INDEX IDX_DADD4A25A76ED395 ON answer'); diff --git a/migrations/Version20231022221730.php b/app/migrations/Version20231022221730.php similarity index 100% rename from migrations/Version20231022221730.php rename to app/migrations/Version20231022221730.php diff --git a/migrations/Version20231103235658.php b/app/migrations/Version20231103235658.php similarity index 100% rename from migrations/Version20231103235658.php rename to app/migrations/Version20231103235658.php diff --git a/migrations/Version20231112194737.php b/app/migrations/Version20231112194737.php similarity index 100% rename from migrations/Version20231112194737.php rename to app/migrations/Version20231112194737.php diff --git a/migrations/Version20231112201521.php b/app/migrations/Version20231112201521.php similarity index 100% rename from migrations/Version20231112201521.php rename to app/migrations/Version20231112201521.php diff --git a/migrations/Version20231119013323.php b/app/migrations/Version20231119013323.php similarity index 100% rename from migrations/Version20231119013323.php rename to app/migrations/Version20231119013323.php diff --git a/migrations/Version20231119205647.php b/app/migrations/Version20231119205647.php similarity index 100% rename from migrations/Version20231119205647.php rename to app/migrations/Version20231119205647.php diff --git a/migrations/Version20231120235309.php b/app/migrations/Version20231120235309.php similarity index 100% rename from migrations/Version20231120235309.php rename to app/migrations/Version20231120235309.php diff --git a/migrations/Version20240102024927.php b/app/migrations/Version20240102024927.php similarity index 100% rename from migrations/Version20240102024927.php rename to app/migrations/Version20240102024927.php diff --git a/app/migrations/Version20240904211753.php b/app/migrations/Version20240904211753.php new file mode 100644 index 0000000..4923b0e --- /dev/null +++ b/app/migrations/Version20240904211753.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE user ADD email VARCHAR(255) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your neeCHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, executed_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', PRIMARY KEY(version)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB COMMENT = \'\' '); + $this->addSql('ALTER TABLE user DROP email'); + } +} diff --git a/app/migrations/Version20240904212530.php b/app/migrations/Version20240904212530.php new file mode 100644 index 0000000..0f7804e --- /dev/null +++ b/app/migrations/Version20240904212530.php @@ -0,0 +1,30 @@ +addSql('ALTER TABLE user ADD password VARCHAR(255) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE user DROP password'); + } +} diff --git a/app/migrations/Version20240904220255.php b/app/migrations/Version20240904220255.php new file mode 100644 index 0000000..c2a2c68 --- /dev/null +++ b/app/migrations/Version20240904220255.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE user CHANGE telegram_id telegram_id VARCHAR(255) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE user CHANGE telegram_id telegram_id VARCHAR(255) NOT NULL'); + } +} diff --git a/app/migrations/Version20251206170339.php b/app/migrations/Version20251206170339.php new file mode 100644 index 0000000..73f71c1 --- /dev/null +++ b/app/migrations/Version20251206170339.php @@ -0,0 +1,33 @@ +addSql('DROP TABLE migration_versions'); + $this->addSql('ALTER TABLE user ADD sub VARCHAR(255) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE TABLE migration_versions (version VARCHAR(14) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, executed_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', PRIMARY KEY(version)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB COMMENT = \'\' '); + $this->addSql('ALTER TABLE user DROP sub'); + } +} diff --git a/phpunit.xml.dist b/app/phpunit.xml.dist similarity index 100% rename from phpunit.xml.dist rename to app/phpunit.xml.dist diff --git a/psalm.xml b/app/psalm.xml similarity index 100% rename from psalm.xml rename to app/psalm.xml diff --git a/public/index.php b/app/public/index.php similarity index 70% rename from public/index.php rename to app/public/index.php index f094a9b..b5a467b 100644 --- a/public/index.php +++ b/app/public/index.php @@ -1,9 +1,12 @@ setTelegramId('994310081'); + $user->setSub('1'); $manager->persist($user); $manager->flush(); } diff --git a/src/DataFixtures/CapitalsFixtures.php b/app/src/DataFixtures/CapitalsFixtures.php similarity index 96% rename from src/DataFixtures/CapitalsFixtures.php rename to app/src/DataFixtures/CapitalsFixtures.php index d58c57f..a992f66 100644 --- a/src/DataFixtures/CapitalsFixtures.php +++ b/app/src/DataFixtures/CapitalsFixtures.php @@ -8,7 +8,7 @@ class CapitalsFixtures extends Fixture { - private const COUNTRY_FILES = [ + private const array COUNTRY_FILES = [ 'capitals-africa.json', 'capitals-americas.json', 'capitals-asia.json', diff --git a/src/Entity/.gitignore b/app/src/Entity/.gitignore similarity index 100% rename from src/Entity/.gitignore rename to app/src/Entity/.gitignore diff --git a/src/Entity/CapitalsStat.php b/app/src/Entity/CapitalsStat.php similarity index 100% rename from src/Entity/CapitalsStat.php rename to app/src/Entity/CapitalsStat.php diff --git a/app/src/EventListener/JsonExceptionListener.php b/app/src/EventListener/JsonExceptionListener.php new file mode 100644 index 0000000..02dfd3b --- /dev/null +++ b/app/src/EventListener/JsonExceptionListener.php @@ -0,0 +1,45 @@ + ['onKernelException', 0], + ]; + } + + public function onKernelException(ExceptionEvent $event): void + { + $exception = $event->getThrowable(); + + $statusCode = $exception instanceof HttpExceptionInterface + ? $exception->getStatusCode() + : 500; + + $data = [ + 'error' => true, + 'message' => $exception->getMessage(), + 'code' => $statusCode, + ]; + + if ($this->environment === 'dev') { + $data['trace'] = $exception->getTraceAsString(); + } + + $response = new JsonResponse($data, $statusCode); + $event->setResponse($response); + } +} diff --git a/app/src/EventListener/JwtAuthenticationFailureListener.php b/app/src/EventListener/JwtAuthenticationFailureListener.php new file mode 100644 index 0000000..06f3d8a --- /dev/null +++ b/app/src/EventListener/JwtAuthenticationFailureListener.php @@ -0,0 +1,46 @@ + 'onJwtInvalid', + 'lexik_jwt_authentication.on_jwt_not_found' => 'onJwtNotFound', + 'lexik_jwt_authentication.on_jwt_expired' => 'onJwtExpired', + ]; + } + + public function onJwtInvalid(JWTInvalidEvent $event): void + { + $exception = $event->getException(); + $this->logger->error('JWT Invalid: ' . $exception->getMessage(), [ + 'previous' => $exception->getPrevious()?->getMessage(), + ]); + } + + public function onJwtNotFound(JWTNotFoundEvent $event): void + { + $exception = $event->getException(); + $this->logger->warning('JWT Not Found: ' . $exception->getMessage()); + } + + public function onJwtExpired(JWTExpiredEvent $event): void + { + $exception = $event->getException(); + $this->logger->warning('JWT Expired: ' . $exception->getMessage()); + } +} diff --git a/app/src/EventListener/NgrokHeaderListener.php b/app/src/EventListener/NgrokHeaderListener.php new file mode 100644 index 0000000..6060340 --- /dev/null +++ b/app/src/EventListener/NgrokHeaderListener.php @@ -0,0 +1,23 @@ + 'onKernelResponse', + ]; + } + + public function onKernelResponse(ResponseEvent $event): void + { + $response = $event->getResponse(); + $response->headers->set('ngrok-skip-browser-warning', 'true'); + } +} \ No newline at end of file diff --git a/app/src/Flags/ConsoleCommand/GameCapitalsCommand.php b/app/src/Flags/ConsoleCommand/GameCapitalsCommand.php new file mode 100644 index 0000000..3a3f85c --- /dev/null +++ b/app/src/Flags/ConsoleCommand/GameCapitalsCommand.php @@ -0,0 +1,110 @@ +flagsGenerator = new FlagsGenerator(); + $this->isoCodes = FlagsGenerator::getAvailableCodes(); + } + + protected function configure(): void + { + $this + ->addArgument('arg1', InputArgument::OPTIONAL, 'Argument description') + ->addOption('option1', null, InputOption::VALUE_NONE, 'Option description') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $db = $this->load(); + $excluded = ['UM', 'AQ', 'TF', 'HM', 'SH', 'RU', 'CX', 'SJ', 'RE', 'GS']; + $lives = 3; + while ($lives > 0) { + $choices = []; + for ($total = count($this->isoCodes), $max = 10, $i = 0; $i < $max; ++$i) { + if ($randomChoice = $this->isoCodes[rand(0, $total - 1)]) { + if (in_array($randomChoice, $excluded) || in_array($randomChoice, $choices)) { + --$i; + continue; + } + $choices[] = $randomChoice; + } + } + + $correctIndex = rand(0, 9); + $options = array_map(fn (string $code) => $db[$code]->getName(), $choices); + + $io->text(sprintf('Lives: %d', $lives)); + $choice = $io->choice( + 'Select the capital of ' . Countries::getName(strtoupper($choices[$correctIndex])) . " " . $this->flagsGenerator->getEmojiFlagOrNull(strtolower($choices[$correctIndex])) . " ", + $options, + ); + + if ($choice === $db[$choices[$correctIndex]]->getName()) { + $io->success('YES'); + } else { + $io->error('No :['); + --$lives; + } + } + + + return Command::SUCCESS; + } + + private const COUNTRY_FILES = [ + 'capitals-africa.json', + 'capitals-americas.json', + 'capitals-asia.json', + 'capitals-europe.json', + 'capitals-oceania.json', + ]; + + public function load(): array + { + $result = []; + foreach (self::COUNTRY_FILES as $fileName) { + $result = array_merge($result, $this->loadFileContent($fileName)); + } + + return $result; + } + + private function loadFileContent(string $fileName): array + { + if (file_exists($fileName)) { + ['countries' => $countries] = json_decode(file_get_contents($fileName), true); + } + + $capitals = []; + foreach ($countries ?? [] as $country) { + $capitals[$country['isoCode']] = new Capital($country['capital'], $country['name'], $country['isoCode'], $country['region']); + } + + return $capitals; + } +} diff --git a/app/src/Flags/ConsoleCommand/GameFlagsCommand.php b/app/src/Flags/ConsoleCommand/GameFlagsCommand.php new file mode 100644 index 0000000..7b3f072 --- /dev/null +++ b/app/src/Flags/ConsoleCommand/GameFlagsCommand.php @@ -0,0 +1,74 @@ +flagsGenerator = new FlagsGenerator(); + $this->isoCodes = FlagsGenerator::getAvailableCodes(); + } + + protected function configure(): void + { + $this + ->addArgument('arg1', InputArgument::OPTIONAL, 'Argument description') + ->addOption('option1', null, InputOption::VALUE_NONE, 'Option description') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); +// $arg1 = $input->getArgument('arg1'); +// +// if ($arg1) { +// $io->note(sprintf('You passed an argument: %s', $arg1)); +// } +// +// if ($input->getOption('option1')) { +// // ... +// } + + $choices = []; + for ($total = count($this->isoCodes), $max = 4, $i = 0; $i < $max; ++$i) { + $choices[] = strtolower($this->isoCodes[rand(0, $total)]); + } + + $correctIndex = rand(0, 3); + $options = array_map(fn (string $code) => $this->flagsGenerator->getEmojiFlagOrNull($code), $choices); + + $choice = $io->choice( + 'Select the flag of ' . Countries::getName(strtoupper($choices[$correctIndex])), + $options, + ); + + if ($choice === $this->flagsGenerator->getEmojiFlagOrNull($choices[$correctIndex])) { + $io->success('Yes! :]'); + } else { + $io->warning('No :['); + } + + return Command::SUCCESS; + } +} diff --git a/src/Flags/ConsoleCommand/GetTokenCommand.php b/app/src/Flags/ConsoleCommand/GetTokenCommand.php similarity index 100% rename from src/Flags/ConsoleCommand/GetTokenCommand.php rename to app/src/Flags/ConsoleCommand/GetTokenCommand.php diff --git a/app/src/Flags/ConsoleCommand/PopulateCapitalsCommand.php b/app/src/Flags/ConsoleCommand/PopulateCapitalsCommand.php new file mode 100644 index 0000000..127aa78 --- /dev/null +++ b/app/src/Flags/ConsoleCommand/PopulateCapitalsCommand.php @@ -0,0 +1,111 @@ +addOption('purge', null, InputOption::VALUE_NONE, 'Purge existing capitals before populating') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + if ($input->getOption('purge')) { + $this->purgeExistingCapitals($io); + } + + $totalCount = 0; + + foreach (self::COUNTRY_FILES as $fileName) { + $count = $this->loadFileContent($fileName, $io); + $totalCount += $count; + } + + $io->success(sprintf('Successfully populated %d capitals.', $totalCount)); + + return Command::SUCCESS; + } + + private function purgeExistingCapitals(SymfonyStyle $io): void + { + $existingCount = $this->capitalRepository->count([]); + + if ($existingCount > 0) { + $this->entityManager->createQuery('DELETE FROM App\Flags\Entity\Capital')->execute(); + $io->warning(sprintf('Purged %d existing capitals.', $existingCount)); + } + } + + private function loadFileContent(string $fileName, SymfonyStyle $io): int + { + if (!file_exists($fileName)) { + $io->error(sprintf('File not found: %s', $fileName)); + return 0; + } + + $content = file_get_contents($fileName); + if ($content === false) { + $io->error(sprintf('Could not read file: %s', $fileName)); + return 0; + } + + $data = json_decode($content, true); + if (!isset($data['countries']) || !is_array($data['countries'])) { + $io->error(sprintf('Invalid JSON structure in: %s', $fileName)); + return 0; + } + + $count = 0; + foreach ($data['countries'] as $country) { + $capital = new Capital( + $country['capital'], + $country['name'], + $country['isoCode'], + $country['region'] + ); + $this->entityManager->persist($capital); + $count++; + } + + $this->entityManager->flush(); + $io->info(sprintf('Loaded %d capitals from %s', $count, $fileName)); + + return $count; + } +} diff --git a/app/src/Flags/ConsoleCommand/PopulateFlagsCommand.php b/app/src/Flags/ConsoleCommand/PopulateFlagsCommand.php new file mode 100644 index 0000000..5e45a57 --- /dev/null +++ b/app/src/Flags/ConsoleCommand/PopulateFlagsCommand.php @@ -0,0 +1,134 @@ +addOption('purge', null, InputOption::VALUE_NONE, 'Purge existing flags before populating') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + if ($input->getOption('purge')) { + $this->purgeExistingFlags($io); + } + + $codes = $this->collectAllCodes($io); + + if (empty($codes)) { + $io->error('No country codes found in JSON files.'); + return Command::FAILURE; + } + + $createdCount = 0; + $skippedCount = 0; + + foreach ($codes as $code) { + $existing = $this->flagRepository->findOneBy(['code' => $code]); + + if ($existing !== null) { + $skippedCount++; + continue; + } + + $flag = new Flag(); + $flag->setCode($code); + $this->entityManager->persist($flag); + $createdCount++; + } + + $this->entityManager->flush(); + + $io->success(sprintf( + 'Flags populated: %d created, %d skipped (already exist).', + $createdCount, + $skippedCount + )); + + return Command::SUCCESS; + } + + private function purgeExistingFlags(SymfonyStyle $io): void + { + $existingCount = $this->flagRepository->count([]); + + if ($existingCount > 0) { + $this->entityManager->createQuery('DELETE FROM App\Flags\Entity\Flag')->execute(); + $io->warning(sprintf('Purged %d existing flags.', $existingCount)); + } + } + + /** + * @return string[] + */ + private function collectAllCodes(SymfonyStyle $io): array + { + $codes = []; + + foreach (self::COUNTRY_FILES as $fileName) { + if (!file_exists($fileName)) { + $io->warning(sprintf('File not found: %s', $fileName)); + continue; + } + + $content = file_get_contents($fileName); + if ($content === false) { + $io->warning(sprintf('Could not read file: %s', $fileName)); + continue; + } + + $data = json_decode($content, true); + if (!isset($data['countries']) || !is_array($data['countries'])) { + $io->warning(sprintf('Invalid JSON structure in: %s', $fileName)); + continue; + } + + foreach ($data['countries'] as $country) { + if (isset($country['isoCode'])) { + $codes[] = strtolower($country['isoCode']); + } + } + + $io->info(sprintf('Read %d codes from %s', count($data['countries']), $fileName)); + } + + return array_unique($codes); + } +} diff --git a/app/src/Flags/ConsoleCommand/PopulateUsersCommand.php b/app/src/Flags/ConsoleCommand/PopulateUsersCommand.php new file mode 100644 index 0000000..df87b79 --- /dev/null +++ b/app/src/Flags/ConsoleCommand/PopulateUsersCommand.php @@ -0,0 +1,196 @@ +addArgument('telegramId', InputArgument::OPTIONAL, 'Telegram ID for single user creation', '0') + ->addOption('username', 'u', InputOption::VALUE_OPTIONAL, 'Telegram username') + ->addOption('first-name', 'f', InputOption::VALUE_OPTIONAL, 'First name') + ->addOption('last-name', 'l', InputOption::VALUE_OPTIONAL, 'Last name') + ->addOption('json', 'j', InputOption::VALUE_REQUIRED, 'Path to JSON file for batch creation') + ->addOption('skip-existing', null, InputOption::VALUE_NONE, 'Skip users that already exist (by telegramId)') + ->setHelp(<<<'HELP' +Create users individually or in batch from a JSON file. + +Single user: + bin/console app:populate:users 123456 -u johndoe -f John -l Doe + +Batch from JSON: + bin/console app:populate:users --json users.json --skip-existing + +JSON file format: + [ + {"telegramId": "123456", "telegramUsername": "johndoe", "firstName": "John", "lastName": "Doe"}, + {"telegramId": "789012", "firstName": "Jane"} + ] + + Or with wrapper: + {"users": [...]} + +Available fields: + telegramId (required), telegramUsername, firstName, lastName, telegramPhotoUrl, sub +HELP) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $jsonPath = $input->getOption('json'); + + if ($jsonPath !== null) { + return $this->createFromJson($jsonPath, $input->getOption('skip-existing'), $io); + } + + return $this->createSingleUser($input, $io); + } + + private function createSingleUser(InputInterface $input, SymfonyStyle $io): int + { + $telegramId = $input->getArgument('telegramId'); + + $existing = $this->userRepository->findOneBy(['telegramId' => $telegramId]); + if ($existing !== null) { + $io->error(sprintf('User with telegramId "%s" already exists (id: %d).', $telegramId, $existing->getId())); + return Command::FAILURE; + } + + $user = $this->buildUser([ + 'telegramId' => $telegramId, + 'telegramUsername' => $input->getOption('username'), + 'firstName' => $input->getOption('first-name'), + 'lastName' => $input->getOption('last-name'), + ]); + + $this->entityManager->persist($user); + $this->entityManager->flush(); + + $io->success(sprintf('Created user: telegramId=%s, id=%d', $telegramId, $user->getId())); + + return Command::SUCCESS; + } + + private function createFromJson(string $jsonPath, bool $skipExisting, SymfonyStyle $io): int + { + if (!file_exists($jsonPath)) { + $io->error(sprintf('JSON file not found: %s', $jsonPath)); + return Command::FAILURE; + } + + $content = file_get_contents($jsonPath); + if ($content === false) { + $io->error(sprintf('Could not read file: %s', $jsonPath)); + return Command::FAILURE; + } + + $data = json_decode($content, true); + if ($data === null) { + $io->error('Invalid JSON format.'); + return Command::FAILURE; + } + + // Support both {"users": [...]} and plain [...] + $users = $data['users'] ?? $data; + + if (!is_array($users)) { + $io->error('JSON must be an array of users or {"users": [...]}'); + return Command::FAILURE; + } + + $createdCount = 0; + $skippedCount = 0; + $errorCount = 0; + + foreach ($users as $index => $userData) { + if (!isset($userData['telegramId'])) { + $io->warning(sprintf('Entry %d missing telegramId, skipped.', $index)); + $errorCount++; + continue; + } + + $telegramId = (string) $userData['telegramId']; + $existing = $this->userRepository->findOneBy(['telegramId' => $telegramId]); + + if ($existing !== null) { + if ($skipExisting) { + $skippedCount++; + continue; + } + $io->warning(sprintf('User telegramId=%s already exists, skipped.', $telegramId)); + $skippedCount++; + continue; + } + + $user = $this->buildUser($userData); + $this->entityManager->persist($user); + $createdCount++; + } + + $this->entityManager->flush(); + + $io->success(sprintf( + 'Batch complete: %d created, %d skipped, %d errors.', + $createdCount, + $skippedCount, + $errorCount + )); + + return Command::SUCCESS; + } + + /** + * @param array $data + */ + private function buildUser(array $data): User + { + $user = new User(); + $user->setTelegramId((string) $data['telegramId']); + + if (!empty($data['telegramUsername'])) { + $user->setTelegramUsername($data['telegramUsername']); + } + if (!empty($data['firstName'])) { + $user->setFirstName($data['firstName']); + } + if (!empty($data['lastName'])) { + $user->setLastName($data['lastName']); + } + if (!empty($data['telegramPhotoUrl'])) { + $user->setTelegramPhotoUrl($data['telegramPhotoUrl']); + } + if (!empty($data['sub'])) { + $user->setSub($data['sub']); + } + + return $user; + } +} diff --git a/src/Flags/ConsoleCommand/SetWebhookCommand.php b/app/src/Flags/ConsoleCommand/SetWebhookCommand.php similarity index 100% rename from src/Flags/ConsoleCommand/SetWebhookCommand.php rename to app/src/Flags/ConsoleCommand/SetWebhookCommand.php diff --git a/src/Flags/Controller/.gitignore b/app/src/Flags/Controller/.gitignore similarity index 100% rename from src/Flags/Controller/.gitignore rename to app/src/Flags/Controller/.gitignore diff --git a/src/Flags/Controller/CapitalsController.php b/app/src/Flags/Controller/CapitalsController.php similarity index 93% rename from src/Flags/Controller/CapitalsController.php rename to app/src/Flags/Controller/CapitalsController.php index 03ddf81..11f1aff 100644 --- a/src/Flags/Controller/CapitalsController.php +++ b/app/src/Flags/Controller/CapitalsController.php @@ -21,10 +21,9 @@ class CapitalsController extends AbstractController protected FlagsGenerator $flagsGenerator; public function __construct( - protected ValidatorInterface $validator, + protected ValidatorInterface $validator, protected string $botToken, - protected readonly EntityManagerInterface $em - + protected readonly EntityManagerInterface $em, ) { $this->flagsGenerator = new FlagsGenerator(); } @@ -52,17 +51,22 @@ public function gameOver(Request $request, CapitalsGameService $service): JsonRe { try { $entity = $service->handleGameOver($request); - return new JsonResponse($entity); + return new JsonResponse($entity); } catch (\Throwable $e) { return new JsonResponse($e->getMessage()); } } #[Route('/capitals/answer/{game}/{countryCode}/{answer}', name: 'get_question_for_game', methods: ['GET'])] - public function getQuestion(Game $game, string $countryCode, string $answer, CapitalsGameService $service): JsonResponse - { - return $this->json($service->giveAnswer($countryCode, base64_decode($answer), $game)); - } + public function getQuestion( + Game $game, + string $countryCode, + string $answer, + CapitalsGameService $service + ): JsonResponse + { + return $this->json($service->giveAnswer($countryCode, base64_decode($answer), $game)); + } #[Route('/capitals/high-scores/{gameType}', name: 'capitals_high_scores', methods: ['GET'])] public function highScores(Request $request, string $gameType, CapitalsGameService $service): JsonResponse diff --git a/src/Flags/Controller/GameController.php b/app/src/Flags/Controller/GameController.php similarity index 73% rename from src/Flags/Controller/GameController.php rename to app/src/Flags/Controller/GameController.php index 98db5c3..8b2f5b2 100644 --- a/src/Flags/Controller/GameController.php +++ b/app/src/Flags/Controller/GameController.php @@ -11,26 +11,19 @@ use App\Flags\Repository\UserRepository; use Doctrine\ORM\EntityManagerInterface; use Lexik\Bundle\JWTAuthenticationBundle\Encoder\JWTEncoderInterface; -//use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTManager; -use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; use Rteeom\FlagsGenerator\FlagsGenerator; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Entity; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Symfony\Component\DependencyInjection\Attribute\Autowire; -use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Controller\ContainerControllerResolver; use Symfony\Component\Intl\Countries; use Symfony\Component\Routing\Annotation\Route; -use Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface; use Symfony\Component\Security\Http\Attribute\CurrentUser; -use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; -use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Validator\Validator\ValidatorInterface; +use Symfony\Bridge\Doctrine\Attribute\MapEntity; +#[Route('/api/flags')] class GameController extends AbstractController { protected FlagsGenerator $flagsGenerator; @@ -65,7 +58,6 @@ public function getQuestion(): JsonResponse return $this->json([ 'APP_ENV' => getenv('APP_ENV'), - 'xaxa' => 'lalka', 'version' => getenv('VERSION_HASH'), 'flags' => $flags, // questionText @@ -75,11 +67,11 @@ public function getQuestion(): JsonResponse ]); } - /** - * @Entity("flag", expr="repository.findOneByCode(flags)") - */ - #[Route('/flags/correct/{flags}', name: 'submit_correct', methods: ['POST'])] - public function correct(Flag $flag, EntityManagerInterface $entityManager): Response + #[Route('/correct/{flag}', name: 'submit_correct', methods: ['POST'])] + public function correct( + #[MapEntity(mapping: ['flag' => 'code'])] Flag $flag, + EntityManagerInterface $entityManager, + ): Response { $flag->incrementCorrectAnswersCounter(); $entityManager->flush(); @@ -136,36 +128,34 @@ public function authAction(Request $request, JWTEncoderInterface $encoder): Resp return new JsonResponse(['token' => $token]); } - /** @Security("is_granted('ROLE_USER')") */ - #[Route('/protected', name: 'get_profile', methods: ['GET'])] + #[Route('/protected', name: 'get_profile', methods: ['GET', 'OPTIONS'])] public function getProfile(): Response { return $this->json($this->getUser()); } - #[Route('/flags/scores', name: 'get_high_scores', methods: ['GET'])] + #[IsGranted('PUBLIC_ACCESS')] + #[Route('/scores', name: 'get_high_scores', methods: ['GET'])] public function getHighScores(UserRepository $repository): Response { return $this->json($repository->getHighScores()); } - /** @Security("is_granted('ROLE_USER')") */ - #[Route('/flags/scores', name: 'submit_game_results', methods: ['POST'])] - public function postScore(Request $request, EntityManagerInterface $entityManager, #[CurrentUser] $user): Response + #[Route('/scores', name: 'submit_game_results', methods: ['POST'])] + public function postScore(Request $request, EntityManagerInterface $entityManager, #[CurrentUser] User $user): Response { $requestArray = json_decode($request->getContent(), true); $scoreDTO = new ScoreDTO($requestArray); - $score = (new Score())->fromDTO($scoreDTO); + $score = new Score()->fromDTO($scoreDTO); $answers = []; if (isset($requestArray['answers'])) { foreach ($requestArray['answers'] as $answer) { - $item = (new Answer())->fromArray($answer); + $item = new Answer()->fromArray($answer); $answers[] = $item; } } - /** @var User $user */ $user->finalizeGame($score, $answers); $entityManager->flush(); @@ -179,34 +169,8 @@ public function getEmoji(string $flag): Response return new Response($flag); } - - #[Route('/token', name: 'test_token', methods: ['GET'])] - public function getToken( - JWTEncoderInterface $encoder, - UserRepository $repository, - TokenStorageInterface $storage, - JWTTokenManagerInterface $JWTManager, - #[Autowire(service: 'lexik_jwt_authentication.handler.authentication_success')] - AuthenticationSuccessHandlerInterface $handler - ): Response { - $user = $repository->getAnyUser(); -// $token = $encoder -// ->encode([ -// 'username' => $user->getTelegramId(), -// 'exp' => time() + 36000 -// ]); - - $token = $JWTManager->create($user); -// $handler = $this->container->get('lexik_jwt_authentication.handler.authentication_success'); - $handler->handleAuthenticationSuccess($user, $token); - -// $storage->setToken($user->getTelegramId(), $token); - return $this->json(['token' => $token]); - } - - /** @Security("is_granted('ROLE_USER')") */ - #[Route('/incorrect', name: 'incorrect', methods: ['GET'])] + #[Route('/incorrect', name: 'incorrect', methods: ['GET', 'OPTIONS'])] public function getStat(#[CurrentUser] $user, AnswerRepository $repository): Response { $correctResults = $repository->findCorrectGuesses($user->getId()); @@ -238,8 +202,7 @@ public function getStat(#[CurrentUser] $user, AnswerRepository $repository): Res return $this->json($result); } - /** @Security("is_granted('ROLE_USER')") */ - #[Route('/correct', name: 'correct', methods: ['GET'])] + #[Route('/correct', name: 'correct', methods: ['GET', 'OPTIONS'])] public function getRight(#[CurrentUser] $user, AnswerRepository $repository): Response { $correctResults = $repository->findCorrectGuesses($user->getId()); @@ -261,7 +224,7 @@ public function getRight(#[CurrentUser] $user, AnswerRepository $repository): Re if (!isset($result[$key]['rate'])) { $result[$key]['rate'] = 0; - $result[$key]['times'] = $result[$key]['times'].'/'.$result[$key]['times']; + $result[$key]['times'] = "0/".$result[$key]['times']; } } diff --git a/app/src/Flags/Controller/SecurityController.php b/app/src/Flags/Controller/SecurityController.php new file mode 100644 index 0000000..74dfeeb --- /dev/null +++ b/app/src/Flags/Controller/SecurityController.php @@ -0,0 +1,87 @@ +getClient('flags_app') + ->redirect(['openid', 'profile', 'email'], []); + } + + #[Route('/debug/headers', name: 'debug_headers')] + public function debugHeaders(Request $request): JsonResponse + { + return new JsonResponse([ + 'scheme' => $request->getScheme(), + 'isSecure' => $request->isSecure(), + 'host' => $request->getHost(), + 'clientIp' => $request->getClientIp(), + 'X-Forwarded-Proto' => $request->headers->get('X-Forwarded-Proto'), + 'X-Forwarded-For' => $request->headers->get('X-Forwarded-For'), + 'X-Forwarded-Host' => $request->headers->get('X-Forwarded-Host'), + 'trustedProxies' => Request::getTrustedProxies(), + 'server_HTTPS' => $_SERVER['HTTPS'] ?? 'not set', + ]); + } + + #[Route('/oauth/check', name: 'oauth_check')] + public function check() + { + // This route is handled by the authenticator + } + + #[Route('/logout', name: 'app_logout')] + public function logout() + { + throw new \LogicException('This should never be reached'); + } + + #[Route('/api/refresh', name: 'api_refresh', methods: ['POST'])] + public function refresh(Request $request): JsonResponse + { + # TODO Check if it works at all + + $data = json_decode($request->getContent(), true); + $refreshToken = $data['refresh_token'] ?? null; + + if (!$refreshToken) { + return new JsonResponse(['error' => 'No refresh token provided'], 400); + } + + try { + $response = $this->httpClient->request('POST', $_ENV['OAUTH_SERVER_URL'] . '/oauth2/token', [ + 'body' => [ + 'grant_type' => 'refresh_token', + 'refresh_token' => $refreshToken, + 'client_id' => $_ENV['OAUTH_CLIENT_ID'], + 'client_secret' => $_ENV['OAUTH_CLIENT_SECRET'], + ] + ]); + + $tokens = $response->toArray(); + + return new JsonResponse([ + 'access_token' => $tokens['access_token'], + 'refresh_token' => $tokens['refresh_token'] ?? $refreshToken, + 'expires_in' => $tokens['expires_in'], + ]); + } catch (\Exception $e) { + return new JsonResponse(['error' => 'Failed to refresh token'], 401); + } + } +} \ No newline at end of file diff --git a/src/Flags/DTO/CapitalsStatDTO.php b/app/src/Flags/DTO/CapitalsStatDTO.php similarity index 100% rename from src/Flags/DTO/CapitalsStatDTO.php rename to app/src/Flags/DTO/CapitalsStatDTO.php diff --git a/src/Flags/DTO/ScoreDTO.php b/app/src/Flags/DTO/ScoreDTO.php similarity index 100% rename from src/Flags/DTO/ScoreDTO.php rename to app/src/Flags/DTO/ScoreDTO.php diff --git a/src/Flags/Entity/.gitignore b/app/src/Flags/Entity/.gitignore similarity index 100% rename from src/Flags/Entity/.gitignore rename to app/src/Flags/Entity/.gitignore diff --git a/src/Flags/Entity/Answer.php b/app/src/Flags/Entity/Answer.php similarity index 75% rename from src/Flags/Entity/Answer.php rename to app/src/Flags/Entity/Answer.php index 90f05ba..3e8743a 100644 --- a/src/Flags/Entity/Answer.php +++ b/app/src/Flags/Entity/Answer.php @@ -3,52 +3,33 @@ namespace App\Flags\Entity; use Doctrine\ORM\Mapping as ORM; -use Doctrine\ORM\Mapping\ManyToOne; -use phpDocumentor\Reflection\Types\Integer; use Symfony\Component\Serializer\Annotation\Ignore; -/** - * @ORM\Entity(repositoryClass="App\Flags\Repository\AnswerRepository") - */ +#[ORM\Entity(repositoryClass: "App\Flags\Repository\AnswerRepository")] class Answer { - /** - * @ORM\Id() - * @ORM\GeneratedValue() - * @ORM\Column(type="integer") - */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: "integer")] protected int $id; - - /** - * @ORM\Column(type="integer") - */ + + #[ORM\Column(type: "integer")] protected int $timer; - - /** - * @ORM\Column(type="string", length=255) - */ + + #[ORM\Column(type: "string", length: 255)] protected string $flagCode; - - /** - * @ORM\Column(type="string", length=255) - */ + + #[ORM\Column(type: "string", length: 255)] protected string $answerOptions; - - /** - * @ORM\Column(type="boolean") - */ + + #[ORM\Column(type: "boolean")] protected bool $correct; - - /** - * @ORM\Column(type="datetime") - */ + + #[ORM\Column(type: "datetime")] protected \DateTime $date; - - /** - * Many features have one product. This is the owning side. - * @ManyToOne(targetEntity="User", inversedBy="answers") - * @Ignore - */ + + #[ORM\ManyToOne(targetEntity: "User", inversedBy: "answers")] + #[Ignore] protected ?User $user; /** @@ -58,7 +39,7 @@ public function getTimer(): int { return $this->timer; } - + /** * @param int $timer */ @@ -66,7 +47,7 @@ public function setTimer(int $timer): void { $this->timer = $timer; } - + /** * @return string */ @@ -74,7 +55,7 @@ public function getFlagCode(): string { return $this->flagCode; } - + /** * @param string $flagCode */ @@ -82,7 +63,7 @@ public function setFlagCode(string $flagCode): void { $this->flagCode = $flagCode; } - + /** * @return array */ @@ -90,7 +71,7 @@ public function getAnswerOptions(): string { return $this->answerOptions; } - + /** * @param array $answerOptions */ @@ -98,7 +79,7 @@ public function setAnswerOptions(array $answerOptions): void { $this->answerOptions = json_encode($answerOptions); } - + /** * @return bool */ @@ -106,7 +87,7 @@ public function isCorrect(): bool { return $this->correct; } - + /** * @param bool $correct */ @@ -114,7 +95,7 @@ public function setCorrect(bool $correct): void { $this->correct = $correct; } - + /** * @return \DateTime */ @@ -122,7 +103,7 @@ public function getDate(): \DateTime { return $this->date; } - + /** * @param \DateTime $date */ @@ -130,7 +111,7 @@ public function setDate(\DateTime $date): void { $this->date = $date; } - + /** * @return string */ @@ -138,7 +119,7 @@ public function getId(): string { return $this->id; } - + public function fromArray(array $array): self { $item = new static; @@ -147,10 +128,10 @@ public function fromArray(array $array): self $item->setTimer($array['time']); $item->setCorrect($array['correct']); $item->setDate((new \DateTime())->setTimestamp(round($array['answerDateTime']/1000))); - + return $item; } - + /** * @return User|null */ @@ -158,7 +139,7 @@ public function getUser(): ?User { return $this->user; } - + /** * @param User|null $user */ diff --git a/src/Flags/Entity/Capital.php b/app/src/Flags/Entity/Capital.php similarity index 66% rename from src/Flags/Entity/Capital.php rename to app/src/Flags/Entity/Capital.php index 8ba83b7..c5869a0 100644 --- a/src/Flags/Entity/Capital.php +++ b/app/src/Flags/Entity/Capital.php @@ -4,36 +4,24 @@ use Doctrine\ORM\Mapping as ORM; -/** - * @ORM\Entity(repositoryClass="App\Flags\Repository\CapitalRepository") - */ +#[ORM\Entity(repositoryClass: "App\Flags\Repository\CapitalRepository")] class Capital { - /** - * @ORM\Id() - * @ORM\GeneratedValue() - * @ORM\Column(type="integer") - */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: "integer")] private int $id; - /** - * @ORM\Column(type="string", length=255) - */ + #[ORM\Column(type: "string", length: 255)] private string $code; - /** - * @ORM\Column(type="string", length=255) - */ + #[ORM\Column(type: "string", length: 255)] private string $name; - /** - * @ORM\Column(type="string", length=255) - */ + #[ORM\Column(type: "string", length: 255)] private string $region; - /** - * @ORM\Column(type="string", length=255) - */ + #[ORM\Column(type: "string", length: 255)] private string $country; public function __construct(string $name, string $country, string $code, string $region) diff --git a/src/Flags/Entity/CapitalsStat.php b/app/src/Flags/Entity/CapitalsStat.php similarity index 70% rename from src/Flags/Entity/CapitalsStat.php rename to app/src/Flags/Entity/CapitalsStat.php index f97f400..b5ef490 100644 --- a/src/Flags/Entity/CapitalsStat.php +++ b/app/src/Flags/Entity/CapitalsStat.php @@ -6,41 +6,27 @@ use Doctrine\ORM\Mapping as ORM; use DateTime; -/** - * @ORM\Entity(repositoryClass="App\Flags\Repository\CapitalsStatRepository") - */ +#[ORM\Entity(repositoryClass: "App\Flags\Repository\CapitalsStatRepository")] class CapitalsStat { - /** - * @ORM\Id() - * @ORM\GeneratedValue() - * @ORM\Column(type="integer") - */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: "integer")] protected int $id; - /** - * @ORM\Column(type="integer", length=255) - */ + #[ORM\Column(type: "integer", length: 255)] protected int $sessionTimer; - /** - * @ORM\Column(type="integer", length=255) - */ + #[ORM\Column(type: "integer", length: 255)] protected int $score; - /** - * @ORM\ManyToOne(targetEntity="App\Flags\Entity\User") - */ + #[ORM\ManyToOne(targetEntity: "App\Flags\Entity\User")] protected readonly User $user; - /** - * @ORM\Column(type="string", length=255) - */ + #[ORM\Column(type: "string", length: 255)] protected string $gameType; - /** - * @ORM\Column(type="datetime") - */ + #[ORM\Column(type: "datetime")] protected readonly DateTime $created; public function __construct( diff --git a/src/Flags/Entity/Enum/GameType.php b/app/src/Flags/Entity/Enum/GameType.php similarity index 100% rename from src/Flags/Entity/Enum/GameType.php rename to app/src/Flags/Entity/Enum/GameType.php diff --git a/src/Flags/Entity/Enum/WorldRegions.php b/app/src/Flags/Entity/Enum/WorldRegions.php similarity index 100% rename from src/Flags/Entity/Enum/WorldRegions.php rename to app/src/Flags/Entity/Enum/WorldRegions.php diff --git a/src/Flags/Entity/Flag.php b/app/src/Flags/Entity/Flag.php similarity index 73% rename from src/Flags/Entity/Flag.php rename to app/src/Flags/Entity/Flag.php index 820d1d2..03ad4a8 100644 --- a/src/Flags/Entity/Flag.php +++ b/app/src/Flags/Entity/Flag.php @@ -4,31 +4,21 @@ use Doctrine\ORM\Mapping as ORM; -/** - * @ORM\Entity(repositoryClass="App\Flags\Repository\FlagRepository") - */ +#[ORM\Entity(repositoryClass: "App\Flags\Repository\FlagRepository")] class Flag { - /** - * @ORM\Id() - * @ORM\GeneratedValue() - * @ORM\Column(type="integer") - */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: "integer")] private int $id; - /** - * @ORM\Column(type="string", length=255) - */ + #[ORM\Column(type: "string", length: 255)] private string $code; - /** - * @ORM\Column(type="integer") - */ + #[ORM\Column(type: "integer")] private int $shows = 0; - /** - * @ORM\Column(type="integer") - */ + #[ORM\Column(type: "integer")] private int $correctGuesses = 0; @@ -72,7 +62,7 @@ public function getCorrectGuesses(): int { return $this->correctGuesses; } - + public function incrementCorrectAnswersCounter(): void { diff --git a/src/Flags/Entity/Game.php b/app/src/Flags/Entity/Game.php similarity index 73% rename from src/Flags/Entity/Game.php rename to app/src/Flags/Entity/Game.php index 0cb656c..b33473d 100644 --- a/src/Flags/Entity/Game.php +++ b/app/src/Flags/Entity/Game.php @@ -5,31 +5,21 @@ use App\Flags\Entity\Enum\GameType; use Doctrine\ORM\Mapping as ORM; -/** - * @ORM\Entity(repositoryClass="App\Flags\Repository\GameRepository") - */ +#[ORM\Entity(repositoryClass: "App\Flags\Repository\GameRepository")] class Game { - /** - * @ORM\Id() - * @ORM\GeneratedValue() - * @ORM\Column(type="integer") - */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: "integer")] private int $id; - /** - * @ORM\Column(type="string", length=255) - */ + #[ORM\Column(type: "string", length: 255)] private string $type; - /** - * @ORM\ManyToOne(targetEntity="App\Flags\Entity\User") - */ + #[ORM\ManyToOne(targetEntity: "App\Flags\Entity\User")] private readonly User $user; - /** - * @ORM\Column(type="json") - */ + #[ORM\Column(type: "json")] private array $questions = []; public function __construct( diff --git a/src/Flags/Entity/Score.php b/app/src/Flags/Entity/Score.php similarity index 71% rename from src/Flags/Entity/Score.php rename to app/src/Flags/Entity/Score.php index af0def5..0a70d46 100644 --- a/src/Flags/Entity/Score.php +++ b/app/src/Flags/Entity/Score.php @@ -6,39 +6,29 @@ use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; -/** - * @ORM\Entity(repositoryClass="App\Flags\Repository\ScoreRepository") - */ +#[ORM\Entity(repositoryClass: "App\Flags\Repository\ScoreRepository")] class Score { - /** - * @ORM\Id() - * @ORM\GeneratedValue() - * @ORM\Column(type="integer") - */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: "integer")] protected int $id; - - /** - * @ORM\Column(type="integer", length=255) - */ + + #[ORM\Column(type: "integer", length: 255)] protected int $sessionTimer; - - /** - * @ORM\Column(type="integer", length=255) - * @Assert\Type("integer") - */ + + #[ORM\Column(type: "integer", length: 255)] + #[Assert\Type("integer")] protected int $score; - - /** - * @ORM\Column(type="datetime") - */ + + #[ORM\Column(type: "datetime")] protected \DateTime $date; - + public function __construct() { $this->date = new \DateTime(); } - + /** * @return int|mixed */ @@ -46,7 +36,7 @@ public function getSessionTimer(): int { return $this->sessionTimer; } - + /** * @param int|mixed $sessionTimer */ @@ -54,7 +44,7 @@ public function setSessionTimer($sessionTimer): void { $this->sessionTimer = $sessionTimer; } - + /** * @return int|mixed */ @@ -62,7 +52,7 @@ public function getScore() { return $this->score; } - + /** * @param int|mixed $score */ @@ -70,13 +60,13 @@ public function setScore($score): void { $this->score = $score; } - + public function fromDTO(ScoreDTO $dto): self { $score = new static(); $score->setSessionTimer($dto->sessionTimer); $score->setScore($dto->score); - + return $score; } } diff --git a/src/Flags/Entity/User.php b/app/src/Flags/Entity/User.php similarity index 80% rename from src/Flags/Entity/User.php rename to app/src/Flags/Entity/User.php index 9ac1e11..3f621f8 100644 --- a/src/Flags/Entity/User.php +++ b/app/src/Flags/Entity/User.php @@ -6,77 +6,55 @@ use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Security\Core\User\UserInterface; -use Doctrine\ORM\Mapping\OneToMany; use Symfony\Component\Serializer\Annotation\Ignore; -/** - * @ORM\Entity(repositoryClass="App\Flags\Repository\UserRepository") - */ +#[ORM\Entity(repositoryClass: "App\Flags\Repository\UserRepository")] class User implements UserInterface { - /** - * @ORM\Id() - * @ORM\GeneratedValue() - * @ORM\Column(type="integer") - */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: "integer")] private int $id; - /** - * @ORM\Column(type="string", length=255) - */ + #[ORM\Column(type: "string", length: 255)] private string $telegramId; - /** - * @ORM\Column(type="string", length=255, nullable=true) - */ + #[ORM\Column(type: "string", length: 255, nullable: true)] private ?string $firstName; - /** - * @ORM\Column(type="string", length=255, nullable=true) - */ + #[ORM\Column(type: "string", length: 255, nullable: true)] private ?string $lastName; - /** - * @ORM\Column(type="string", length=255, nullable=true) - */ + #[ORM\Column(type: "string", length: 255, nullable: true)] private ?string $telegramUsername; - /** - * @ORM\Column(type="string", length=255, nullable=true) - */ + #[ORM\Column(type: "string", length: 255, nullable: true)] private ?string $telegramPhotoUrl; - - /** - * @ORM\Column(type="integer") - */ + + #[ORM\Column(type: "integer")] private int $highScore = 0; - - /** - * @ORM\Column(type="integer") - */ + + #[ORM\Column(type: "integer")] private int $gamesTotal = 0; - - /** - * @ORM\Column(type="integer") - */ + + #[ORM\Column(type: "integer")] private int $bestTime = 0; - - /** - * @ORM\Column(type="integer") - */ + + #[ORM\Column(type: "integer")] private int $timeTotal = 0; - - /** - * @OneToMany(targetEntity="Answer", mappedBy="user", cascade={"persist"}) - * @Ignore - */ + + #[ORM\Column(type: "string", length: 255, nullable: true)] + private ?string $sub = null; // OAuth2 subject identifier + + #[ORM\OneToMany(targetEntity: "Answer", mappedBy: "user", cascade: ["persist"])] + #[Ignore] private ?Collection $answers; - + public function __construct() { $this->answers = new ArrayCollection(); } - + public function getId(): ?int { return $this->id; @@ -166,17 +144,17 @@ public function eraseCredentials() { // TODO: Implement eraseCredentials() method. } - + public function getHighScore(): int { return $this->highScore; } - + public function setHighScore(int $score): void { - $this->highScore = $score; + $this->highScore = $score; } - + /** * @return int */ @@ -184,7 +162,7 @@ public function getGamesTotal(): int { return $this->gamesTotal; } - + /** * @param int $gamesTotal */ @@ -192,7 +170,7 @@ public function setGamesTotal(int $gamesTotal): void { $this->gamesTotal = $gamesTotal; } - + /** * @return int */ @@ -200,7 +178,7 @@ public function getBestTime(): int { return $this->bestTime; } - + /** * @param int $bestTime */ @@ -208,7 +186,7 @@ public function setBestTime(int $bestTime): void { $this->bestTime = $bestTime; } - + /** * @return int */ @@ -216,7 +194,7 @@ public function getTimeTotal(): int { return $this->timeTotal; } - + /** * @param Score $score * @param array[App\Entity\Answer] $answers @@ -236,19 +214,19 @@ public function finalizeGame(Score $score, array $answers): void $this->addAnswer($item); } } - + public function addAnswer(Answer $answer): void { $answer->setUser($this); $this->answers[] = $answer; } - + public function removeAnswer(Answer $answer): void { $answer->setUser(null); $this->answers->removeElement($answer); } - + /** * @return ArrayCollection|Collection */ @@ -257,8 +235,19 @@ public function getAnswers(): ?Collection return $this->answers; } + public function getUserIdentifier(): string { - return $this->id; + return $this->sub ?? $this->telegramId ?? $this->id; + } + + public function getSub(): ?string + { + return $this->sub; + } + + public function setSub(string $sub): void + { + $this->sub = $sub; } } diff --git a/src/Flags/Repository/.gitignore b/app/src/Flags/Repository/.gitignore similarity index 100% rename from src/Flags/Repository/.gitignore rename to app/src/Flags/Repository/.gitignore diff --git a/src/Flags/Repository/AnswerRepository.php b/app/src/Flags/Repository/AnswerRepository.php similarity index 100% rename from src/Flags/Repository/AnswerRepository.php rename to app/src/Flags/Repository/AnswerRepository.php diff --git a/src/Flags/Repository/CapitalRepository.php b/app/src/Flags/Repository/CapitalRepository.php similarity index 100% rename from src/Flags/Repository/CapitalRepository.php rename to app/src/Flags/Repository/CapitalRepository.php diff --git a/src/Flags/Repository/CapitalsStatRepository.php b/app/src/Flags/Repository/CapitalsStatRepository.php similarity index 100% rename from src/Flags/Repository/CapitalsStatRepository.php rename to app/src/Flags/Repository/CapitalsStatRepository.php diff --git a/src/Flags/Repository/FlagRepository.php b/app/src/Flags/Repository/FlagRepository.php similarity index 100% rename from src/Flags/Repository/FlagRepository.php rename to app/src/Flags/Repository/FlagRepository.php diff --git a/src/Flags/Repository/GameRepository.php b/app/src/Flags/Repository/GameRepository.php similarity index 100% rename from src/Flags/Repository/GameRepository.php rename to app/src/Flags/Repository/GameRepository.php diff --git a/src/Flags/Repository/ScoreRepository.php b/app/src/Flags/Repository/ScoreRepository.php similarity index 100% rename from src/Flags/Repository/ScoreRepository.php rename to app/src/Flags/Repository/ScoreRepository.php diff --git a/src/Flags/Repository/UserRepository.php b/app/src/Flags/Repository/UserRepository.php similarity index 63% rename from src/Flags/Repository/UserRepository.php rename to app/src/Flags/Repository/UserRepository.php index 81ea2d8..6092188 100644 --- a/src/Flags/Repository/UserRepository.php +++ b/app/src/Flags/Repository/UserRepository.php @@ -6,6 +6,9 @@ use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Common\Collections\Criteria; use Doctrine\Persistence\ManagerRegistry; +use League\OAuth2\Client\Provider\GenericResourceOwner; +use Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface; +use Symfony\Component\Security\Core\User\UserInterface; /** * @method User|null find($id, $lockMode = null, $lockVersion = null) @@ -13,7 +16,7 @@ * @method User[] findAll() * @method User[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ -class UserRepository extends ServiceEntityRepository +class UserRepository extends ServiceEntityRepository implements UserLoaderInterface { public function __construct(ManagerRegistry $registry) { @@ -76,4 +79,36 @@ public function findOneBySomeField($value): ?User ; } */ + + public function loadUserByIdentifier(string $identifier): ?UserInterface + { + return $this->findOneBySub($identifier) ?? $this->findOneByTelegramId($identifier); + } + + public function loadOrCreateFromOAuth(GenericResourceOwner $userInfo): User + { + // Assuming your OAuth server returns "sub" as unique identifier + $sub = $userInfo->getId(); // or ->getUid(), ->getEmail(), depending on your provider +// $email = $userInfo->getEmail(); + + // Try to find existing user + $user = $this->findOneBy(['sub' => $sub]); + + if ($user) { + return $user; + } + +// $userInfoArray = $userInfo->toArray() + // Create new user + $user = new User(); + $user->setSub($sub); +// $user->setEmail($userInfoArray['email'] ?? null); +// $user->setRoles(['ROLE_USER']); + + $em = $this->getEntityManager(); + $em->persist($user); + $em->flush(); + + return $user; + } } diff --git a/app/src/Flags/Security/HqAuthAuthenticator.php b/app/src/Flags/Security/HqAuthAuthenticator.php new file mode 100644 index 0000000..7f39c69 --- /dev/null +++ b/app/src/Flags/Security/HqAuthAuthenticator.php @@ -0,0 +1,202 @@ +attributes->get('_route') === 'oauth_check'; +// } +// +// public function authenticate(Request $request): Passport +// { +// $client = $this->clientRegistry->getClient('flags_app'); +// $accessToken = $this->fetchAccessToken($client); +// +// // Store the access token in the request for later use +// $request->attributes->set('oauth_access_token', $accessToken->getToken()); +// $request->attributes->set('oauth_refresh_token', $accessToken->getRefreshToken()); +// $request->attributes->set('oauth_expires_in', $accessToken->getExpires()); +// +// return new SelfValidatingPassport( +// new UserBadge($accessToken->getToken(), function () use ($accessToken, $client) { +// $userInfo = $client->fetchUserFromToken($accessToken); +// return $this->userRepository->loadOrCreateFromOAuth($userInfo); +// }) +// ); +// } +// +// public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response +// { +// // Get the JWT access token +// $accessToken = $request->attributes->get('oauth_access_token'); +// $refreshToken = $request->attributes->get('oauth_refresh_token'); +// $expiresIn = $request->attributes->get('oauth_expires_in'); +// +// // Return JSON with the tokens for the frontend +// return new JsonResponse([ +// 'success' => true, +// 'access_token' => $accessToken, +// 'refresh_token' => $refreshToken, +// 'expires_in' => $expiresIn, +// 'token_type' => 'Bearer', +// 'user' => [ +//// 'email' => $token->getUser()->getEmail(), +// 'roles' => $token->getUser()->getRoles(), +// ] +// ]); +// } +// +// public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response +// { +// return new JsonResponse([ +// 'success' => false, +// 'error' => $exception->getMessage() +// ], Response::HTTP_UNAUTHORIZED); +// } +//} + + + +namespace App\Flags\Security; + +use App\Flags\Repository\UserRepository; +use KnpU\OAuth2ClientBundle\Client\ClientRegistry; +use KnpU\OAuth2ClientBundle\Security\Authenticator\OAuth2Authenticator; +use Lcobucci\JWT\Parser; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\RouterInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Passport; +use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; + +class HqAuthAuthenticator extends OAuth2Authenticator +{ + public function __construct( + private ClientRegistry $clientRegistry, + private RouterInterface $router, + private UserRepository $userRepository, +// private Parser $jwtParser, + ) {} + + public function supports(Request $request): ?bool + { + return $request->attributes->get('_route') === 'oauth_check'; + } + + public function authenticate(Request $request): Passport + { + $client = $this->clientRegistry->getClient('flags_app'); + $accessToken = $this->fetchAccessToken($client); + + +// // Optional: parse & verify JWT locally +// $jwt = $accessToken->getToken(); +// $token = $this->jwtParser->parse($jwt); // e.g., lcobucci/jwt +// if (!$token->verify($signer, $publicKey) || $token->isExpired(new \DateTimeImmutable())) { +// throw new AuthenticationException('Invalid or expired token'); +// } + +// Store the access token in the request for later use + $request->attributes->set('oauth_access_token', $accessToken->getToken()); + $request->attributes->set('oauth_refresh_token', $accessToken->getRefreshToken()); + $request->attributes->set('oauth_expires_in', $accessToken->getExpires()); + + return new SelfValidatingPassport( + new UserBadge($accessToken->getToken(), function() use ($accessToken, $client) { + $userInfo = $client->fetchUserFromToken($accessToken); + + return $this->userRepository->loadOrCreateFromOAuth($userInfo); + // Here you'd load or create your user based on the OAuth data + // For example, using the 'sub' claim as the user identifier +// return $this->loadOrCreateUser($userInfo); + }) + ); + } + +// public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response +// { +// return new JsonResponse([$token->getUser()->getUserIdentifier(), implode($token->getUser()->getRoles())]); +//// return new RedirectResponse($this->router->generate('app_dashboard')); +// } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response + { +// dd($request->toArray()); + // Get the JWT access token + $accessToken = $request->attributes->get('oauth_access_token'); + $refreshToken = $request->attributes->get('oauth_refresh_token'); + $expiresIn = $request->attributes->get('oauth_expires_in'); + + + return new Response(""); + + // Return JSON with the tokens for the frontend +// return new JsonResponse([ +// 'success' => true, +// 'access_token' => $accessToken, +// 'refresh_token' => $refreshToken, +// 'expires_in' => $expiresIn, +// 'token_type' => 'Bearer', +// 'user' => [ +//// 'email' => $token->getUser()->getEmail(), +// 'roles' => $token->getUser()->getRoles(), +// ] +// ]); + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response + { + // DEBUG: Log the actual error + error_log('OAuth authentication failed: ' . $exception->getMessage()); + error_log('Previous exception: ' . ($exception->getPrevious() ? $exception->getPrevious()->getMessage() : 'none')); + + // Temporarily return error instead of redirect loop + return new JsonResponse([ + 'error' => 'authentication_failed', + 'message' => $exception->getMessage(), + 'previous' => $exception->getPrevious() ? $exception->getPrevious()->getMessage() : null, + ], 401); + } + +// private function loadOrCreateUser($userInfo) +// { +// // Implement your user loading/creation logic +// // You might want to inject UserRepository here +// } +} \ No newline at end of file diff --git a/src/Flags/Serializer/UserNormalizer.php b/app/src/Flags/Serializer/UserNormalizer.php similarity index 100% rename from src/Flags/Serializer/UserNormalizer.php rename to app/src/Flags/Serializer/UserNormalizer.php diff --git a/src/Flags/Service/CapitalsGameService.php b/app/src/Flags/Service/CapitalsGameService.php similarity index 100% rename from src/Flags/Service/CapitalsGameService.php rename to app/src/Flags/Service/CapitalsGameService.php diff --git a/app/src/Flags/Service/HqAuthProvider.php b/app/src/Flags/Service/HqAuthProvider.php new file mode 100644 index 0000000..174262d --- /dev/null +++ b/app/src/Flags/Service/HqAuthProvider.php @@ -0,0 +1,29 @@ + $countries] = json_decode(file_get_contents($fileName), true); foreach ($countries ?? [] as $country) { - $this->assertNotNull($flagsGenerator->getEmojiFlagOrNull($country['isoCode']), 'Error with '.$country['isoCode']); + $this->assertNotNull( + $flagsGenerator->getEmojiFlagOrNull($country['isoCode']), + 'Error with ' . $country['isoCode'], + ); } } } @@ -47,10 +50,10 @@ public function testCountriesCount() if (file_exists($fileName)) { ['countries' => $countries] = json_decode(file_get_contents($fileName), true); match ($fileName) { - 'capitals-africa.json' => self::assertCount(54, $countries), - 'capitals-americas.json' => self::assertCount(55, $countries), - 'capitals-asia.json' => self::assertCount(48, $countries), - 'capitals-europe.json' => self::assertCount(45, $countries), + 'capitals-africa.json' => self::assertCount(57, $countries), + 'capitals-americas.json' => self::assertCount(58, $countries), + 'capitals-asia.json' => self::assertCount(53, $countries), + 'capitals-europe.json' => self::assertCount(51, $countries), 'capitals-oceania.json' => self::assertCount(25, $countries), }; } diff --git a/tests/bootstrap.php b/app/tests/bootstrap.php similarity index 100% rename from tests/bootstrap.php rename to app/tests/bootstrap.php diff --git a/config/.DS_Store b/config/.DS_Store deleted file mode 100644 index 2bd003a..0000000 Binary files a/config/.DS_Store and /dev/null differ diff --git a/config/packages/lexik_jwt_authentication.yaml b/config/packages/lexik_jwt_authentication.yaml deleted file mode 100644 index e24f344..0000000 --- a/config/packages/lexik_jwt_authentication.yaml +++ /dev/null @@ -1,6 +0,0 @@ -lexik_jwt_authentication: - secret_key: '%env(resolve:JWT_SECRET_KEY)%' - public_key: '%env(resolve:JWT_PUBLIC_KEY)%' - pass_phrase: '%env(JWT_PASSPHRASE)%' - token_ttl: '%env(JWT_TOKEN_TTL)%' - \ No newline at end of file diff --git a/config/packages/nelmio_cors.yaml b/config/packages/nelmio_cors.yaml deleted file mode 100644 index 5450d51..0000000 --- a/config/packages/nelmio_cors.yaml +++ /dev/null @@ -1,22 +0,0 @@ -nelmio_cors: - defaults: - origin_regex: true - allow_origin: ['*'] -# allow_origin: ['%env(CORS_ALLOW_ORIGIN)%'] - allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'] -# allow_headers: ['Content-Type', 'Authorization'] - allow_headers: ['*'] - expose_headers: ['Link'] - max_age: 3600 - forced_allow_origin_value: '*' - paths: - '^/': - origin_regex: true - allow_origin: ['*'] -# allow_origin: ['%env(CORS_ALLOW_ORIGIN)%'] - allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'] - # allow_headers: ['Content-Type', 'Authorization'] - allow_headers: ['*'] - expose_headers: ['Link'] - max_age: 3600 - forced_allow_origin_value: '*' diff --git a/config/packages/security.yaml b/config/packages/security.yaml deleted file mode 100644 index d0e1cbb..0000000 --- a/config/packages/security.yaml +++ /dev/null @@ -1,31 +0,0 @@ -security: - enable_authenticator_manager: true - # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers - providers: - database_provider: - entity: - class: App\Flags\Entity\User - property: telegramId - password_hashers: - App\Entity\User: - algorithm: bcrypt - firewalls: - dev: - pattern: ^/(_(profiler|wdt)|css|images|js)/ - security: false - main: - jwt: ~ - # activate different ways to authenticate - # https://symfony.com/doc/current/security.html#firewalls-authentication - - # Easy way to control access for large sections of your site - # Note: Only the *first* access control that matches will be used - access_control: - - { path: ^/check, roles: PUBLIC_ACCESS } - - { path: ^/capitals/test, roles: PUBLIC_ACCESS } - - { path: ^/capitals/test2, roles: ROLE_USER } - - { path: ^/capitals/high-scores, roles: PUBLIC_ACCESS } - - { path: ^/api/tg/login, roles: PUBLIC_ACCESS } - - { path: ^/test, roles: ROLE_USER } - - { path: ^/profile, roles: ROLE_USER } - - { path: ^/capitals, roles: ROLE_USER } diff --git a/deploy.php b/deploy.php deleted file mode 100644 index 26509d4..0000000 --- a/deploy.php +++ /dev/null @@ -1,40 +0,0 @@ -setHostname('api.izeebot.top') - ->setRemoteUser('root') -// ->setIdentityFile('') - ->set('username', 'root') -// ->set('branch', 'php8') - ->set('branch', 'fixes') -// ->set('stage','production') - ->set('deploy_path', '/var/www/flags-api') -; - -// Tasks -desc('Restart PHP-FPM service'); -task('php-fpm:restart', function () { - // The user must have rights for restart service - // /etc/sudoers: username ALL=NOPASSWD:/bin/systemctl restart php-fpm.service - run('sudo service php8.0-fpm restart'); -}); -after('deploy:symlink', 'php-fpm:restart'); -// [Optional] if deploy fails automatically unlock. -after('deploy:failed', 'deploy:unlock'); -// Migrate database before symlink new release. -//before('deploy:symlink', 'database:migrate'); diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml deleted file mode 100644 index 7caad79..0000000 --- a/docker-compose-prod.yml +++ /dev/null @@ -1,40 +0,0 @@ -version: "3.6" -services: - php: - environment: - VERSION_HASH: "${GITHUB_SHA:-none}" - SYMFONY_DECRYPTION_SECRET: "${SYMFONY_DECRYPTION_SECRET}" - APP_ENV : "prod" - extends: - file: docker-compose.yml - service: php - image: ${IMAGE}:php-latest - build: - dockerfile: .docker/php-fpm/Dockerfile - networks: - - backend-flags - nginx: - extends: - file: docker-compose.yml - service: nginx - image: ${IMAGE}:nginx-latest - container_name: "nginx-${PROJECT_NAME}" - ports: - - "8080:80" - - "4443:443" - networks: - - backend-flags - db: - extends: - file: docker-compose.yml - service: db - image: ${IMAGE}:db-latest - ports: - - "33060:3306" - networks: - - backend-flags -networks: - backend-flags: - external: true -volumes: - db-data-flags: ~ diff --git a/docker-compose-staging.yml b/docker-compose-staging.yml deleted file mode 100644 index e90025d..0000000 --- a/docker-compose-staging.yml +++ /dev/null @@ -1,49 +0,0 @@ -version: "3.6" -services: - php: - container_name: "php-staging-${PROJECT_NAME}" - build: - args: - KEY: ${GITH_KEY} - context: . - dockerfile: .docker/php-fpm/Dockerfile - depends_on: - - nginx - environment: - VERSION_HASH: "${GITHUB_SHA:-none}" - SYMFONY_DECRYPTION_SECRET: "${SYMFONY_DECRYPTION_SECRET}" - networks: - - backend-flags-staging - nginx: - container_name: "nginx-staging-${PROJECT_NAME}" - build: - context: . - dockerfile: .docker/nginx/Dockerfile - restart: always - ports: - - "8080:80" - - "4443:443" - networks: - - backend-flags-staging - db: - build: - context: . - dockerfile: .docker/mysql/Dockerfile - container_name: "db-staging-${PROJECT_NAME}" - environment: - MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD}" - MYSQL_DATABASE: "flags_staging" - MYSQL_USER: "${MYSQL_USER}" - MYSQL_PASSWORD: "${MYSQL_PASSWORD}" - volumes: - - db-data-flags-staging:/var/lib/mysql - restart: always - ports: - - "33061:3306" - networks: - - backend-flags-staging -networks: - backend-flags-staging: - external: true -volumes: - db-data-flags-staging: ~ diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..e180491 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,52 @@ +version: "3.8" +services: + php: + container_name: "php-dev-${PROJECT_NAME:-flags}" + build: + context: . + dockerfile: .docker/php-fpm/Dockerfile.dev + volumes: + - .:/var/www/webapp:cached + - composer-cache:/home/appuser/.composer + environment: + - SYMFONY_ENV=dev + depends_on: + - db + networks: + - flags-dev + + nginx: + container_name: "nginx-dev-${PROJECT_NAME:-flags}" + image: nginx:alpine + volumes: + - .:/var/www/webapp:cached + - ./.docker/nginx/dev.conf:/etc/nginx/conf.d/default.conf + ports: + - "8080:80" + depends_on: + - php + networks: + - flags-dev + + db: + container_name: "db-dev-${PROJECT_NAME:-flags}" + image: mysql:8.0 + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: flags_dev + MYSQL_USER: flags + MYSQL_PASSWORD: flags + ports: + - "3306:3306" + volumes: + - db-dev-data:/var/lib/mysql + networks: + - flags-dev + +networks: + flags-dev: + driver: bridge + +volumes: + db-dev-data: + composer-cache: diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 42bcca4..1b8e7e7 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -3,11 +3,11 @@ services: php: volumes: - .:/var/www/webapp:rw,cached - + user: "1000:1000" db: ports: - "3306:3306" nginx: ports: - "80:80" - - "443:443" \ No newline at end of file + - "443:443" diff --git a/env/._.env b/env/._.env deleted file mode 100755 index 5fa2cd8..0000000 Binary files a/env/._.env and /dev/null differ diff --git a/env/._.env.staging.local b/env/._.env.staging.local deleted file mode 100755 index 75985ce..0000000 Binary files a/env/._.env.staging.local and /dev/null differ diff --git a/k8s/README.md b/k8s/README.md new file mode 100644 index 0000000..e767c07 --- /dev/null +++ b/k8s/README.md @@ -0,0 +1,37 @@ +Done! I've created a complete Kubernetes deployment setup in k8s/. Here's the structure: + +k8s/ +├── namespace.yaml # flags-api namespace +├── configmap.yaml # Environment variables +├── secrets.yaml # Secrets template (edit with your values) +├── mysql-pvc.yaml # Persistent storage for MySQL +├── mysql-deployment.yaml # MySQL 9.0 database +├── php-deployment.yaml # PHP-FPM (ghcr.io/mainstreamer/flags-api-php:latest) +├── caddy-deployment.yaml # Caddy webserver (ghcr.io/mainstreamer/flags-api-caddy:latest) +├── services.yaml # ClusterIP services for mysql, php, caddy +├── ingress.yaml # Ingress for API (configure your domain) +├── frontend-deployment.yaml # Template for flagsapp:latest (same namespace) +├── kustomization.yaml # Kustomize for easy deployment +└── deploy.sh # Deployment helper script + +To deploy: + +1. Create GHCR credentials secret: + kubectl create secret docker-registry ghcr-credentials \ + --docker-server=ghcr.io \ + --docker-username=YOUR_GITHUB_USERNAME \ + --docker-password=YOUR_GITHUB_PAT \ + -n flags-api + +2. Edit secrets.yaml with your actual values (passwords, APP_SECRET, BOT_TOKEN, etc.) +3. Deploy with kustomize: + cd k8s + kubectl apply -k . + +Or use the helper script: +./deploy.sh + +4. Access the API via port-forward: + kubectl port-forward svc/caddy 8080:80 -n flags-api + +Frontend compatibility: The frontend-deployment.yaml template uses the same flags-api namespace and labels for easy service discovery. The frontend can reach the API at http://caddy internally. diff --git a/k8s/caddy-deployment.yaml b/k8s/caddy-deployment.yaml new file mode 100644 index 0000000..4e4cad7 --- /dev/null +++ b/k8s/caddy-deployment.yaml @@ -0,0 +1,55 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: caddy + namespace: flags-api + labels: + app.kubernetes.io/name: caddy + app.kubernetes.io/component: webserver + app.kubernetes.io/part-of: flags-quiz +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: caddy + template: + metadata: + labels: + app.kubernetes.io/name: caddy + app.kubernetes.io/component: webserver + app.kubernetes.io/part-of: flags-quiz + spec: + imagePullSecrets: + - name: ghcr-credentials + containers: + - name: caddy + image: ghcr.io/mainstreamer/flags-api-caddy:latest + imagePullPolicy: Always + ports: + - containerPort: 80 + name: http + - containerPort: 443 + name: https + envFrom: + - configMapRef: + name: flags-api-config + resources: + requests: + memory: "64Mi" + cpu: "50m" + limits: + memory: "256Mi" + cpu: "200m" +# livenessProbe: +# httpGet: +# path: /health +# port: 80 +# initialDelaySeconds: 10 +# periodSeconds: 10 +# failureThreshold: 3 +# readinessProbe: +# httpGet: +# path: /health +# port: 80 +# initialDelaySeconds: 5 +# periodSeconds: 5 diff --git a/k8s/configmap.yaml b/k8s/configmap.yaml new file mode 100644 index 0000000..d00984e --- /dev/null +++ b/k8s/configmap.yaml @@ -0,0 +1,25 @@ +# ============================================================================= +# SECURITY NOTE: +# This simplified setup loads all env vars from a single secret created from .env.prod +# For production, consider using: +# - Kubernetes Secrets with proper RBAC +# - External secret managers (HashiCorp Vault, AWS Secrets Manager, etc.) +# - Sealed Secrets for GitOps workflows +# - Separate secrets from non-sensitive config +# ============================================================================= + +# Non-sensitive configuration only +apiVersion: v1 +kind: ConfigMap +metadata: + name: flags-api-config + namespace: flags-api + labels: + app.kubernetes.io/name: flags-api + app.kubernetes.io/part-of: flags-quiz +data: + # PHP-FPM service discovery + PHP_FPM_HOST: "php" + PHP_FPM_PORT: "59000" + # Trust all private network ranges (Traefik -> Caddy -> PHP) + TRUSTED_PROXIES: "127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16" diff --git a/k8s/cors-middleware.yaml b/k8s/cors-middleware.yaml new file mode 100644 index 0000000..bf1a4bd --- /dev/null +++ b/k8s/cors-middleware.yaml @@ -0,0 +1,33 @@ +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: cors-headers + namespace: flags-api +spec: + headers: + accessControlAllowMethods: + - "GET" + - "POST" + - "PUT" + - "DELETE" + - "PATCH" + - "OPTIONS" + accessControlAllowHeaders: + - "Authorization" + - "Content-Type" + - "Accept" + - "Origin" + - "User-Agent" + - "DNT" + - "Cache-Control" + - "X-Mx-ReqToken" + - "Keep-Alive" + - "X-Requested-With" + - "If-Modified-Since" + accessControlAllowOriginList: + - "https://flags.izeebot.top" + - "https://capitals.izeebot.top" + accessControlAllowCredentials: true + accessControlExposeHeaders: + - "Authorization" + accessControlMaxAge: 1728000 diff --git a/k8s/deploy.sh b/k8s/deploy.sh new file mode 100755 index 0000000..84944f2 --- /dev/null +++ b/k8s/deploy.sh @@ -0,0 +1,100 @@ +#!/bin/bash +set -e + +NAMESPACE="flags-api" +ENV_FILE="${1:-./.env.prod}" + +echo "=== Flags API Kubernetes Deployment ===" + +# Check if kubectl is available +if ! command -v kubectl &> /dev/null; then + echo "Error: kubectl is not installed" + exit 1 +fi + +# Function to wait for deployment +wait_for_deployment() { + local deployment=$1 + echo "Waiting for $deployment to be ready..." + kubectl rollout status deployment/$deployment -n $NAMESPACE --timeout=120s +} + +# Create namespace first +echo "Creating namespace..." +kubectl apply -f namespace.yaml + +# Create secret from .env.prod file +if [ -f "$ENV_FILE" ]; then + echo "Creating secret from $ENV_FILE..." + kubectl create secret generic flags-api-secrets \ + --from-env-file="$ENV_FILE" \ + -n $NAMESPACE \ + --dry-run=client -o yaml | kubectl apply -f - + + # Create ghcr.io credentials from GITH_KEY + GITH_KEY=$(grep -E "^GITH_KEY=" "$ENV_FILE" | cut -d '=' -f2-) + if [ -n "$GITH_KEY" ]; then + echo "Creating ghcr.io registry credentials..." + kubectl create secret docker-registry ghcr-credentials \ + --docker-server=ghcr.io \ + --docker-username=mainstreamer \ + --docker-password="$GITH_KEY" \ + -n $NAMESPACE \ + --dry-run=client -o yaml | kubectl apply -f - + else + echo "Warning: GITH_KEY not found in $ENV_FILE, skipping ghcr credentials" + fi +else + echo "Warning: $ENV_FILE not found" + echo "Create the secret manually:" + echo " kubectl create secret generic flags-api-secrets --from-env-file=.env.prod -n $NAMESPACE" + read -p "Press Enter to continue without secret, or Ctrl+C to exit..." +fi + +# Apply configmap +echo "Applying configmap..." +kubectl apply -f configmap.yaml + +# Apply storage +echo "Applying persistent volume claim..." +kubectl apply -f mysql-pvc.yaml + +# Apply deployments +echo "Applying deployments..." +kubectl apply -f mysql-deployment.yaml +kubectl apply -f php-deployment.yaml +kubectl apply -f caddy-deployment.yaml + +# Apply services +echo "Applying services..." +kubectl apply -f services.yaml + +# Apply CORS middleware (required for Traefik) +echo "Applying CORS middleware..." +kubectl apply -f cors-middleware.yaml + +# Apply ingress (optional) +echo "Applying ingress..." +kubectl apply -f ingress.yaml + +# Wait for deployments +echo "" +echo "Waiting for deployments to be ready..." +wait_for_deployment mysql +wait_for_deployment php +wait_for_deployment caddy + +echo "" +echo "=== Deployment Complete ===" +echo "" +echo "Services:" +kubectl get svc -n $NAMESPACE +echo "" +echo "Pods:" +kubectl get pods -n $NAMESPACE +echo "" +echo "Access the API:" +echo " kubectl port-forward svc/caddy 58080:58080 -n $NAMESPACE" +echo "" +echo "Run database migrations:" +echo " kubectl exec -it deploy/php -n $NAMESPACE -- php bin/console doctrine:migrations:migrate" diff --git a/k8s/frontend-deployment.yaml b/k8s/frontend-deployment.yaml new file mode 100644 index 0000000..35c380d --- /dev/null +++ b/k8s/frontend-deployment.yaml @@ -0,0 +1,97 @@ +# Frontend deployment template - use same namespace for compatibility +# Image: flagsapp:latest (local registry or configure your registry) +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend + namespace: flags-api + labels: + app.kubernetes.io/name: frontend + app.kubernetes.io/component: frontend + app.kubernetes.io/part-of: flags-quiz +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: frontend + template: + metadata: + labels: + app.kubernetes.io/name: frontend + app.kubernetes.io/component: frontend + app.kubernetes.io/part-of: flags-quiz + spec: + # Uncomment if using private registry + # imagePullSecrets: + # - name: ghcr-credentials + containers: + - name: frontend + image: flagsapp:latest + imagePullPolicy: Always + ports: + - containerPort: 80 + name: http + env: + # API URL for frontend to connect to backend + - name: API_URL + value: "http://caddy" + resources: + requests: + memory: "64Mi" + cpu: "50m" + limits: + memory: "256Mi" + cpu: "200m" + livenessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 10 + periodSeconds: 10 + readinessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 5 + periodSeconds: 5 +--- +apiVersion: v1 +kind: Service +metadata: + name: frontend + namespace: flags-api + labels: + app.kubernetes.io/name: frontend + app.kubernetes.io/component: frontend + app.kubernetes.io/part-of: flags-quiz +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: http + selector: + app.kubernetes.io/name: frontend +--- +# Ingress for frontend - add to main ingress or use separately +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: frontend-ingress + namespace: flags-api + labels: + app.kubernetes.io/name: frontend + app.kubernetes.io/part-of: flags-quiz +spec: + rules: + - host: flags.example.com # Change to your domain + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: frontend + port: + number: 80 diff --git a/k8s/ingress.yaml b/k8s/ingress.yaml new file mode 100644 index 0000000..0040659 --- /dev/null +++ b/k8s/ingress.yaml @@ -0,0 +1,29 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: flags-api-ingress + namespace: flags-api + labels: + app.kubernetes.io/name: flags-api + app.kubernetes.io/part-of: flags-quiz + annotations: + traefik.ingress.kubernetes.io/router.middlewares: flags-api-cors-headers@kubernetescrd + # Uncomment for cert-manager TLS + # cert-manager.io/cluster-issuer: "letsencrypt-prod" +spec: + # Uncomment and configure TLS + # tls: + # - hosts: + # - api.flags.example.com + # secretName: flags-api-tls + rules: + - host: api.flags.izeebot.top # Change to your domain + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: caddy + port: + number: 80 diff --git a/k8s/iptables-rule-add.sh b/k8s/iptables-rule-add.sh new file mode 100644 index 0000000..80e0916 --- /dev/null +++ b/k8s/iptables-rule-add.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# Advanced Port Forwarding Script with Custom Port Mapping +# Usage: ./iptables-port.sh [public_port:internal_port] ... +# Example: ./iptables-port.sh flags-app 4129:3000 4130:8000 + +if [ "$#" -lt 2 ]; then + echo "Usage: $0 [public_port:internal_port] ..." + echo "" + echo "Examples:" + echo " $0 flags-app 4129:3000 4130:8000" + echo " $0 myapp 8080:80" + echo " $0 matrix 3478:3478 5349:5349" + exit 1 +fi + +SERVICE_NAME=$1 +LOCAL_SERVER="10.0.0.3" +AUDIT_DIR="/etc/caddy/audit-logs" +DOCS_FILE="$AUDIT_DIR/port-mappings.log" +IPTABLES_DIR="/etc/iptables" +shift # Remove first argument, leaving only port mappings + +echo "Setting up port forwarding for $SERVICE_NAME..." + +# Enable IP forwarding (idempotent) +if ! grep -q "net.ipv4.ip_forward=1" /etc/sysctl.conf; then + echo 'net.ipv4.ip_forward=1' >> /etc/sysctl.conf + sysctl -p +fi + +# Create directories if they don't exist +mkdir -p $AUDIT_DIR +mkdir -p $IPTABLES_DIR +if [ ! -f $DOCS_FILE ]; then + echo "# Port Forwarding Mappings" > $DOCS_FILE + echo "# Format: [timestamp] service_name: public_port -> internal_port" >> $DOCS_FILE + echo "" >> $DOCS_FILE +fi + +# Log this operation +echo "# =====================================" >> $DOCS_FILE +echo "# Service: $SERVICE_NAME" >> $DOCS_FILE +echo "# Date: $(date '+%Y-%m-%d %H:%M:%S')" >> $DOCS_FILE +echo "# =====================================" >> $DOCS_FILE + +# Forward each port mapping +for MAPPING in "$@"; do + # Parse public:internal format + if [[ $MAPPING =~ ^([0-9]+):([0-9]+)$ ]]; then + PUBLIC_PORT="${BASH_REMATCH[1]}" + INTERNAL_PORT="${BASH_REMATCH[2]}" + else + echo "⚠ Invalid format: $MAPPING (expected format: public:internal)" + continue + fi + + echo "Forwarding $PUBLIC_PORT -> $LOCAL_SERVER:$INTERNAL_PORT..." + + # Check if rule already exists to avoid duplicates + if ! iptables -t nat -C PREROUTING -p tcp --dport $PUBLIC_PORT -j DNAT --to-destination $LOCAL_SERVER:$INTERNAL_PORT 2>/dev/null; then + iptables -t nat -A PREROUTING -p tcp --dport $PUBLIC_PORT -j DNAT --to-destination $LOCAL_SERVER:$INTERNAL_PORT + iptables -A FORWARD -p tcp --dport $INTERNAL_PORT -d $LOCAL_SERVER -j ACCEPT + echo "✓ Port $PUBLIC_PORT -> $INTERNAL_PORT forwarded" + + # Log successful forward + echo "$PUBLIC_PORT -> $INTERNAL_PORT (added)" >> $DOCS_FILE + else + echo "⚠ Port $PUBLIC_PORT already forwarded" + + # Log that it already existed + echo "$PUBLIC_PORT -> $INTERNAL_PORT (already exists)" >> $DOCS_FILE + fi +done + +echo "" >> $DOCS_FILE + +# Save iptables rules to standard location for persistence +echo "Saving iptables rules..." +iptables-save > $IPTABLES_DIR/rules.v4 + +echo "" +echo "✅ Port forwarding complete for $SERVICE_NAME!" +echo "Mappings configured:" +for MAPPING in "$@"; do + if [[ $MAPPING =~ ^([0-9]+):([0-9]+)$ ]]; then + echo " - Public port ${BASH_REMATCH[1]} -> Internal port ${BASH_REMATCH[2]}" + fi +done +echo "Destination server: $LOCAL_SERVER" +echo "" +echo "📝 Audit log: $DOCS_FILE" +echo "💾 Rules saved: $IPTABLES_DIR/rules.v4" +echo "" +echo "Next steps:" +echo "1. Expose port(s) in Kubernetes (NodePort or hostPort)" +echo "2. Update Caddyfile if needed" diff --git a/k8s/kustomization.yaml b/k8s/kustomization.yaml new file mode 100644 index 0000000..f4e7389 --- /dev/null +++ b/k8s/kustomization.yaml @@ -0,0 +1,27 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: flags-api + +resources: + - namespace.yaml + - configmap.yaml + # secrets.yaml is a placeholder - secret created from .env.prod via deploy.sh + - mysql-pvc.yaml + - mysql-deployment.yaml + - php-deployment.yaml + - caddy-deployment.yaml + - services.yaml + - ingress.yaml + +commonLabels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/part-of: flags-quiz + +images: + - name: ghcr.io/mainstreamer/flags-api-php + newTag: latest + - name: ghcr.io/mainstreamer/flags-api-caddy + newTag: latest + - name: mysql + newTag: "9.0" diff --git a/k8s/mysql-deployment.yaml b/k8s/mysql-deployment.yaml new file mode 100644 index 0000000..77d556d --- /dev/null +++ b/k8s/mysql-deployment.yaml @@ -0,0 +1,67 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mysql + namespace: flags-api + labels: + app.kubernetes.io/name: mysql + app.kubernetes.io/component: database + app.kubernetes.io/part-of: flags-quiz +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: mysql + strategy: + type: Recreate + template: + metadata: + labels: + app.kubernetes.io/name: mysql + app.kubernetes.io/component: database + app.kubernetes.io/part-of: flags-quiz + spec: + containers: + - name: mysql + image: mysql:9.0 + ports: + - containerPort: 3306 + name: mysql + # Load MYSQL_* variables from the env secret + envFrom: + - secretRef: + name: flags-api-secrets + volumeMounts: + - name: mysql-data + mountPath: /var/lib/mysql + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "1Gi" + cpu: "500m" + livenessProbe: + exec: + command: + - mysqladmin + - ping + - -h + - localhost + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + readinessProbe: + exec: + command: + - mysqladmin + - ping + - -h + - localhost + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 2 + volumes: + - name: mysql-data + persistentVolumeClaim: + claimName: mysql-pvc diff --git a/k8s/mysql-pvc.yaml b/k8s/mysql-pvc.yaml new file mode 100644 index 0000000..46b30ac --- /dev/null +++ b/k8s/mysql-pvc.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: mysql-pvc + namespace: flags-api + labels: + app.kubernetes.io/name: mysql + app.kubernetes.io/part-of: flags-quiz +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi + # Uncomment and set storageClassName if needed + # storageClassName: standard diff --git a/k8s/namespace.yaml b/k8s/namespace.yaml new file mode 100644 index 0000000..e1a8198 --- /dev/null +++ b/k8s/namespace.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: flags-api + labels: + app.kubernetes.io/name: flags-api + app.kubernetes.io/part-of: flags-quiz diff --git a/k8s/php-deployment.yaml b/k8s/php-deployment.yaml new file mode 100644 index 0000000..bd61060 --- /dev/null +++ b/k8s/php-deployment.yaml @@ -0,0 +1,53 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: php + namespace: flags-api + labels: + app.kubernetes.io/name: php + app.kubernetes.io/component: backend + app.kubernetes.io/part-of: flags-quiz +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: php + template: + metadata: + labels: + app.kubernetes.io/name: php + app.kubernetes.io/component: backend + app.kubernetes.io/part-of: flags-quiz + spec: + imagePullSecrets: + - name: ghcr-credentials + containers: + - name: php + image: ghcr.io/mainstreamer/flags-api-php:latest + imagePullPolicy: Always + ports: + - containerPort: 9000 + name: php-fpm + # Load all environment variables from secret and configmap + envFrom: + - secretRef: + name: flags-api-secrets + - configMapRef: + name: flags-api-config + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" + livenessProbe: + tcpSocket: + port: 9000 + initialDelaySeconds: 10 + periodSeconds: 10 + readinessProbe: + tcpSocket: + port: 9000 + initialDelaySeconds: 5 + periodSeconds: 5 diff --git a/k8s/redeploy.sh b/k8s/redeploy.sh new file mode 100755 index 0000000..eaebfc4 --- /dev/null +++ b/k8s/redeploy.sh @@ -0,0 +1,21 @@ +#!/bin/bash +NAMESPACE="flags-api" + +echo "=== Redeploying flags-api ===" + +# Restart deployments to pull latest images +echo "Restarting deployments..." +kubectl rollout restart deployment/php deployment/caddy -n $NAMESPACE + +# Wait for rollouts to complete +echo "" +echo "Waiting for php..." +kubectl rollout status deployment/php -n $NAMESPACE --timeout=120s + +echo "" +echo "Waiting for caddy..." +kubectl rollout status deployment/caddy -n $NAMESPACE --timeout=120s + +echo "" +echo "=== Redeploy Complete ===" +kubectl get pods -n $NAMESPACE diff --git a/k8s/reset-secrets.sh b/k8s/reset-secrets.sh new file mode 100644 index 0000000..4432d23 --- /dev/null +++ b/k8s/reset-secrets.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +ENV_FILE="${1:-./.env.prod}" +NAMESPACE="flags-api" + +# Reset env secrets +kubectl delete secret flags-api-secrets -n $NAMESPACE --ignore-not-found +kubectl create secret generic flags-api-secrets --from-env-file="$ENV_FILE" -n $NAMESPACE + +# Reset ghcr credentials +GITH_KEY=$(grep -E "^GITH_KEY=" "$ENV_FILE" | cut -d '=' -f2-) +if [ -n "$GITH_KEY" ]; then + echo "Recreating ghcr.io registry credentials..." + kubectl delete secret ghcr-credentials -n $NAMESPACE --ignore-not-found + kubectl create secret docker-registry ghcr-credentials \ + --docker-server=ghcr.io \ + --docker-username=mainstreamer \ + --docker-password="$GITH_KEY" \ + -n $NAMESPACE +fi + +# Restart deployments to pick up new secrets +kubectl rollout restart deployment/php -n $NAMESPACE +kubectl rollout restart deployment/caddy -n $NAMESPACE + +echo "Secrets reset - done" diff --git a/k8s/secrets.yaml b/k8s/secrets.yaml new file mode 100644 index 0000000..53825f9 --- /dev/null +++ b/k8s/secrets.yaml @@ -0,0 +1,23 @@ +# ============================================================================= +# SECURITY NOTE: +# This file is a placeholder. The actual secret is created from .env.prod file: +# +# kubectl create secret generic flags-api-secrets \ +# --from-env-file=../.env.prod \ +# -n flags-api +# +# For production, consider: +# - HashiCorp Vault with vault-injector +# - External Secrets Operator +# - Sealed Secrets (Bitnami) for GitOps +# - Cloud provider secret managers (AWS/GCP/Azure) +# - NEVER commit actual secrets to git +# ============================================================================= + +# Placeholder - created via kubectl from .env.prod +# apiVersion: v1 +# kind: Secret +# metadata: +# name: flags-api-secrets +# namespace: flags-api +# type: Opaque diff --git a/k8s/services.yaml b/k8s/services.yaml new file mode 100644 index 0000000..c6aa107 --- /dev/null +++ b/k8s/services.yaml @@ -0,0 +1,57 @@ +apiVersion: v1 +kind: Service +metadata: + name: mysql + namespace: flags-api + labels: + app.kubernetes.io/name: mysql + app.kubernetes.io/component: database + app.kubernetes.io/part-of: flags-quiz +spec: + type: ClusterIP + ports: + - port: 53306 + targetPort: 3306 + protocol: TCP + name: mysql + selector: + app.kubernetes.io/name: mysql +--- +apiVersion: v1 +kind: Service +metadata: + name: php + namespace: flags-api + labels: + app.kubernetes.io/name: php + app.kubernetes.io/component: backend + app.kubernetes.io/part-of: flags-quiz +spec: + type: ClusterIP + ports: + - port: 59000 + targetPort: 9000 + protocol: TCP + name: php-fpm + selector: + app.kubernetes.io/name: php +--- +apiVersion: v1 +kind: Service +metadata: + name: caddy + namespace: flags-api + labels: + app.kubernetes.io/name: caddy + app.kubernetes.io/component: webserver + app.kubernetes.io/part-of: flags-quiz +spec: + type: NodePort + ports: + - port: 80 + targetPort: 80 + nodePort: 31180 + protocol: TCP + name: http + selector: + app.kubernetes.io/name: caddy diff --git a/my_key.bin b/my_key.bin new file mode 100644 index 0000000..28ac2c7 Binary files /dev/null and b/my_key.bin differ diff --git a/src/Flags/Application/PostParamConverter.php b/src/Flags/Application/PostParamConverter.php deleted file mode 100644 index f6fec0a..0000000 --- a/src/Flags/Application/PostParamConverter.php +++ /dev/null @@ -1,386 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -//namespace Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter; - - - - /** - * DoctrineParamConverter. - * - * @author Fabien Potencier - */ -//class DoctrineParamConverter implements ParamConverterInterface -//{ - /** - * @var ManagerRegistry - */ - private $registry; - - /** - * @var ExpressionLanguage - */ - private $language; - - /** - * @var array - */ - private $defaultOptions; - private $s; - public function __construct( - ManagerRegistry $registry = null, - ExpressionLanguage $expressionLanguage = null, - SerializerInterface $s, - array $options = [] - ) { - $this->registry = $registry; - $this->language = $expressionLanguage; - - $defaultValues = [ - 'entity_manager' => null, - 'exclude' => [], - 'mapping' => [], - 'strip_null' => false, - 'expr' => null, - 'id' => null, - 'repository_method' => null, - 'map_method_signature' => false, - 'evict_cache' => false, - ]; - - $this->defaultOptions = array_merge($defaultValues, $options); - - $this->s = $s; - } - - /** - * {@inheritdoc} - * - * @throws \LogicException When unable to guess how to get a Doctrine instance from the request information - * @throws NotFoundHttpException When object not found - */ - public function apply(Request $request, ParamConverter $configuration) - { -// dd($configuration); - -// return true; -// dd($configuration); - $name = $configuration->getName(); - $class = $configuration->getClass(); - $options = $this->getOptions($configuration); - - if (null === $request->attributes->get($name, false)) { - $configuration->setIsOptional(true); - } -// - $errorMessage = null; -// if ($expr = $options['expr']) { -// $object = $this->findViaExpression($class, $request, $expr, $options, $configuration); -// -// if (null === $object) { -// $errorMessage = sprintf('The expression "%s" returned null', $expr); -// } -// -// // find by identifier? -// } elseif (false === $object = $this->find($class, $request, $options, $name)) { -// // find by criteria -// if (false === $object = $this->findOneBy($class, $request, $options)) { -// if ($configuration->isOptional()) { -// $object = null; -// } else { -// throw new \LogicException(sprintf('Unable to guess how to get a Doctrine instance from the request information for parameter "%s".', -// $name)); -// } -// } -// } - -// if (null === $object && false === $configuration->isOptional()) { -// $message = sprintf('%s object not found by the @%s annotation.', $class, -// $this->getAnnotationName($configuration)); -// if ($errorMessage) { -// $message .= ' ' . $errorMessage; -// } -// throw new NotFoundHttpException($message); -// } - - $object = $request->getContent(); - $o = $this->s->deserialize($object, $class, 'json'); -// dump($o); - -// $name = $configuration->getName(); - - $request->attributes->set($name, $o); - -// $request->attributes->set($name, $object); - - return true; - } - - private function find($class, Request $request, $options, $name) - { - if ($options['mapping'] || $options['exclude']) { - return false; - } - - $id = $this->getIdentifier($request, $options, $name); - - if (false === $id || null === $id) { - return false; - } - - if ($options['repository_method']) { - $method = $options['repository_method']; - } else { - $method = 'find'; - } - - $om = $this->getManager($options['entity_manager'], $class); - if ($options['evict_cache'] && $om instanceof EntityManagerInterface) { - $cacheProvider = $om->getCache(); - if ($cacheProvider && $cacheProvider->containsEntity($class, $id)) { - $cacheProvider->evictEntity($class, $id); - } - } - - try { - return $om->getRepository($class)->$method($id); - } catch (NoResultException $e) { - return; - } catch (ConversionException $e) { - return; - } - } - - private function getIdentifier(Request $request, $options, $name) - { - if (null !== $options['id']) { - if (!\is_array($options['id'])) { - $name = $options['id']; - } elseif (\is_array($options['id'])) { - $id = []; - foreach ($options['id'] as $field) { - if (false !== strstr($field, '%s')) { - // Convert "%s_uuid" to "foobar_uuid" - $field = sprintf($field, $name); - } - $id[$field] = $request->attributes->get($field); - } - - return $id; - } - } - - if ($request->attributes->has($name)) { - return $request->attributes->get($name); - } - - if ($request->attributes->has('id') && !$options['id']) { - return $request->attributes->get('id'); - } - - return false; - } - - private function findOneBy($class, Request $request, $options) - { - if (!$options['mapping']) { - $keys = $request->attributes->keys(); - $options['mapping'] = $keys ? array_combine($keys, $keys) : []; - } - - foreach ($options['exclude'] as $exclude) { - unset($options['mapping'][$exclude]); - } - - if (!$options['mapping']) { - return false; - } - - // if a specific id has been defined in the options and there is no corresponding attribute - // return false in order to avoid a fallback to the id which might be of another object - if ($options['id'] && null === $request->attributes->get($options['id'])) { - return false; - } - - $criteria = []; - $em = $this->getManager($options['entity_manager'], $class); - $metadata = $em->getClassMetadata($class); - - $mapMethodSignature = $options['repository_method'] - && $options['map_method_signature'] - && true === $options['map_method_signature']; - - foreach ($options['mapping'] as $attribute => $field) { - if ($metadata->hasField($field) - || ($metadata->hasAssociation($field) && $metadata->isSingleValuedAssociation($field)) - || $mapMethodSignature) { - $criteria[$field] = $request->attributes->get($attribute); - } - } - - if ($options['strip_null']) { - $criteria = array_filter($criteria, function ($value) { - return null !== $value; - }); - } - - if (!$criteria) { - return false; - } - - if ($options['repository_method']) { - $repositoryMethod = $options['repository_method']; - } else { - $repositoryMethod = 'findOneBy'; - } - - try { - if ($mapMethodSignature) { - return $this->findDataByMapMethodSignature($em, $class, $repositoryMethod, $criteria); - } - - return $em->getRepository($class)->$repositoryMethod($criteria); - } catch (NoResultException $e) { - return; - } catch (ConversionException $e) { - return; - } - } - - private function findDataByMapMethodSignature($em, $class, $repositoryMethod, $criteria) - { - $arguments = []; - $repository = $em->getRepository($class); - $ref = new \ReflectionMethod($repository, $repositoryMethod); - foreach ($ref->getParameters() as $parameter) { - if (\array_key_exists($parameter->name, $criteria)) { - $arguments[] = $criteria[$parameter->name]; - } elseif ($parameter->isDefaultValueAvailable()) { - $arguments[] = $parameter->getDefaultValue(); - } else { - throw new \InvalidArgumentException(sprintf('Repository method "%s::%s" requires that you provide a value for the "$%s" argument.', - \get_class($repository), $repositoryMethod, $parameter->name)); - } - } - - return $ref->invokeArgs($repository, $arguments); - } - - private function findViaExpression($class, Request $request, $expression, $options, ParamConverter $configuration) - { - if (null === $this->language) { - throw new \LogicException(sprintf('To use the @%s tag with the "expr" option, you need to install the ExpressionLanguage component.', - $this->getAnnotationName($configuration))); - } - - $repository = $this->getManager($options['entity_manager'], $class)->getRepository($class); - $variables = array_merge($request->attributes->all(), ['repository' => $repository]); - - try { - return $this->language->evaluate($expression, $variables); - } catch (NoResultException $e) { - return; - } catch (ConversionException $e) { - return; - } catch (SyntaxError $e) { - throw new \LogicException(sprintf('Error parsing expression -- %s -- (%s)', $expression, $e->getMessage()), - 0, $e); - } - } - - /** - * {@inheritdoc} - */ - public function supports(ParamConverter $configuration) - { -// dd($configuration); -// exit; - return $configuration->getClass() === Score::class; - // if there is no manager, this means that only Doctrine DBAL is configured - if (null === $this->registry || !\count($this->registry->getManagerNames())) { - return false; - } - - if (null === $configuration->getClass()) { - return false; - } - - $options = $this->getOptions($configuration, false); - - // Doctrine Entity? - $em = $this->getManager($options['entity_manager'], $configuration->getClass()); - if (null === $em) { - return false; - } - - return !$em->getMetadataFactory()->isTransient($configuration->getClass()); - } - - private function getOptions(ParamConverter $configuration, $strict = true) - { - $passedOptions = $configuration->getOptions(); - - if (isset($passedOptions['repository_method'])) { - @trigger_error('The repository_method option of @ParamConverter is deprecated and will be removed in 6.0. Use the expr option or @Entity.', - E_USER_DEPRECATED); - } - - if (isset($passedOptions['map_method_signature'])) { - @trigger_error('The map_method_signature option of @ParamConverter is deprecated and will be removed in 6.0. Use the expr option or @Entity.', - E_USER_DEPRECATED); - } - - $extraKeys = array_diff(array_keys($passedOptions), array_keys($this->defaultOptions)); - if ($extraKeys && $strict) { - throw new \InvalidArgumentException(sprintf('Invalid option(s) passed to @%s: %s', - $this->getAnnotationName($configuration), implode(', ', $extraKeys))); - } - - return array_replace($this->defaultOptions, $passedOptions); - } - - private function getManager($name, $class) - { - if (null === $name) { - return $this->registry->getManagerForClass($class); - } - - return $this->registry->getManager($name); - } - - private function getAnnotationName(ParamConverter $configuration) - { - $r = new \ReflectionClass($configuration); - - return $r->getShortName(); - } -//} - -} \ No newline at end of file diff --git a/test b/test deleted file mode 100644 index 8b13789..0000000 --- a/test +++ /dev/null @@ -1 +0,0 @@ -