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 @@
-