Compare commits

..
Author SHA1 Message Date
lang 8b428260d7 chg: pkg: new version release !skipChangelog
Deploy to Production / deploy (push) Successful in 27s
2026-07-27 19:30:53 +02:00
lang 306add033a chg: pkg: new version release !skipChangelog 2026-07-27 19:02:31 +02:00
lang f451df283c chg: usr: add current version of the app in the footer #14
Deploy to Production / deploy (push) Successful in 3m42s
2026-07-27 19:01:42 +02:00
lang 8d542e683e chg: pkg: upgrade FE to the latest possible version #14 2026-07-27 18:52:14 +02:00
lang dff46cdc6f chg: test: all tests passed and migrated to Symfony 8 #14 2026-07-27 18:39:17 +02:00
lang 82fff0cdf0 chg: pkg: upgrade the BE deps to the latest possible version #14 2026-07-27 18:35:42 +02:00
lang 4bf7ac358d chg: dev: remove registration notification #14 2026-07-27 18:16:08 +02:00
lang 1a97c2c4b4 chg: pkg: add a process that removes the unnecessary minio-init when it does not needed anymore #14 2026-07-27 18:09:33 +02:00
lang 9ea83d7e6e new: dev: hardening the registration process with removing the not activated registrations #14 2026-07-27 18:08:52 +02:00
lang f3e4ff211d chg: dev: removed the obsolete contact form entry - legacy code from Google ReCapthca #14 2026-07-27 14:57:33 +02:00
lang 7a86767db1 chg: pkg: new version release !skipChangelog
Deploy to Production / deploy (push) Successful in 2m18s
2026-06-08 12:59:29 +02:00
lang 2b1689b321 chg: usr: add clickable badges for Cap app-wide #13
Deploy to Production / deploy (push) Successful in 2m43s
2026-06-02 09:48:23 +02:00
lang cf56b10aba chg: pkg: upgrade both backend and frontend to the latest available version #13 2026-06-01 22:36:19 +02:00
lang 7063704539 chg: usr: replace Google ReCaptcha with Cap instance #13 2026-06-01 22:24:34 +02:00
lang 199bb7e525 chg: pkg: fix all eslint issues - & add example of the testing env #10 2026-04-28 08:10:18 +02:00
lang 5daaf71ae7 chg: pkg: new version release !skipChangelog 2026-04-23 21:42:21 +02:00
lang 0aeec47996 new: pkg: add tracking code for the app #10
Deploy to Production / deploy (push) Successful in 30s
2026-04-23 21:41:47 +02:00
lang 3d67b8f2d9 chg: pkg: new version release !skipChangelog 2026-04-22 12:15:29 +02:00
lang dd9a190fd9 fix: usr: the error message cannot be seen during avatar changing #10
Deploy to Production / deploy (push) Successful in 3m7s
2026-04-22 12:15:06 +02:00
lang f5e5019ea8 chg: pkg: new version release !skipChangelog 2026-04-21 22:47:04 +02:00
lang 3f51eb5db6 chg: usr: increase the 2 MB avatar maximum file size to 10 MB #10
Deploy to Production / deploy (push) Successful in 29s
2026-04-21 22:46:44 +02:00
lang 55ef7c9301 chg: pkg: new version release !skipChangelog 2026-04-21 21:05:54 +02:00
78 changed files with 3203 additions and 2409 deletions
+18 -6
View File
@@ -6,6 +6,9 @@
APP_ENV=dev APP_ENV=dev
APP_SECRET=changethis APP_SECRET=changethis
APP_NAME=mineseeker APP_NAME=mineseeker
# Public application release identifier, e.g. 2026.3.0. In Gitea production
# deployments this is supplied by the APP_VERSION repository variable.
APP_VERSION=1.0.0
# APP_PUBLIC_HOSTNAME: The public hostname for your application (used for generating absolute URLs in emails) # APP_PUBLIC_HOSTNAME: The public hostname for your application (used for generating absolute URLs in emails)
# For production, set this to your domain (e.g., mineseeker.com) # For production, set this to your domain (e.g., mineseeker.com)
APP_PUBLIC_HOSTNAME=localhost APP_PUBLIC_HOSTNAME=localhost
@@ -17,15 +20,17 @@ APP_PUBLIC_HOSTNAME=localhost
###< symfony/framework-bundle ### ###< symfony/framework-bundle ###
###> doctrine/doctrine-bundle ### ###> doctrine/doctrine-bundle ###
# Docker PostgreSQL is exposed on port 15432 — use that for bare-metal dev. # Docker PostgreSQL is exposed on port 5452 — use that for bare-metal dev.
# Replace POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB with the values from your .env. # Replace POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB with the values from your .env.
DATABASE_URL=postgresql://POSTGRES_USER:POSTGRES_PASSWORD@127.0.0.1:15432/POSTGRES_DB?serverVersion=18&charset=utf8 DATABASE_URL=postgresql://POSTGRES_USER:POSTGRES_PASSWORD@127.0.0.1:5452/POSTGRES_DB?serverVersion=18&charset=utf8
###< doctrine/doctrine-bundle ### ###< doctrine/doctrine-bundle ###
###> google/recaptcha ### ###> trycap.dev/cap ###
RECAPTCHA_SITE_KEY=changethis # CAP_API_ENDPOINT: Full URL including site-key path, e.g. https://cap.example.com/my-site-key/
RECAPTCHA_SECRET_KEY=changethis # After deploying the Cap service, create a site in the dashboard and paste the endpoint here.
###< google/recaptcha ### CAP_API_ENDPOINT=http://localhost:3000/changethis/
CAP_SECRET_KEY=changethis
###< trycap.dev/cap ###
###> minio/minio ### ###> minio/minio ###
MINIO_ROOT_USER=changethis MINIO_ROOT_USER=changethis
@@ -57,3 +62,10 @@ MERCURE_SUBSCRIBER_JWT=changethis
WEBAUTHN_RP_ID=mine.local WEBAUTHN_RP_ID=mine.local
WEBAUTHN_ORIGIN=https://mine.local WEBAUTHN_ORIGIN=https://mine.local
###< web-auth/webauthn-framework ### ###< web-auth/webauthn-framework ###
###> symfony/messenger ###
# Choose one of the transports below
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
###< symfony/messenger ###
+1
View File
@@ -1,3 +1,4 @@
# define your env variables for the test env here # define your env variables for the test env here
KERNEL_CLASS='App\Kernel' KERNEL_CLASS='App\Kernel'
APP_SECRET='$ecretf0rt3st' APP_SECRET='$ecretf0rt3st'
DATABASE_URL="postgresql://system7:bazmeg@127.0.0.1:5452/mineseeker_test?serverVersion=18&charset=utf8"
+5
View File
@@ -34,6 +34,11 @@ jobs:
docker compose build docker compose build
- name: Start services - name: Start services
env:
APP_VERSION: ${{ vars.APP_VERSION }}
run: | run: |
cd "${{ vars.PROD_APP_DIR }}" cd "${{ vars.PROD_APP_DIR }}"
docker compose up -d minio
docker compose rm -sf minio_init
docker compose run --rm minio_init
docker compose up -d docker compose up -d
-1
View File
@@ -94,7 +94,6 @@ assets/
│ ├── utils/ # Shared utilities │ ├── utils/ # Shared utilities
│ ├── profile.jsx # Profile page entry │ ├── profile.jsx # Profile page entry
│ ├── passkey.jsx # Passkey management entry │ ├── passkey.jsx # Passkey management entry
│ └── contact.jsx # Contact form entry
├── css/ ├── css/
│ └── homepage/ # SCSS partials (imported by style.homepage.scss) │ └── homepage/ # SCSS partials (imported by style.homepage.scss)
└── fonts/ └── fonts/
+64
View File
@@ -1,6 +1,70 @@
# Changelog # Changelog
## v2026.3.0-0 (2026-07-27)
### New
* Hardening the registration process with removing the not activated registrations #14. [Lang]
### Changes
* Add current version of the app in the footer #14. [Lang]
* Upgrade FE to the latest possible version #14. [Lang]
* All tests passed and migrated to Symfony 8 #14. [Lang]
* Upgrade the BE deps to the latest possible version #14. [Lang]
* Remove registration notification #14. [Lang]
* Add a process that removes the unnecessary minio-init when it does not needed anymore #14. [Lang]
* Removed the obsolete contact form entry - legacy code from Google ReCapthca #14. [Lang]
## v2026.2.10-0 (2026-06-02)
### Changes
* Add clickable badges for Cap app-wide #13. [Lang]
* Upgrade both backend and frontend to the latest available version #13. [Lang]
* Replace Google ReCaptcha with Cap instance #13. [Lang]
* Fix all eslint issues - & add example of the testing env #10. [Lang]
## v2026.2.9-0 (2026-04-23)
### New
* Add tracking code for the app #10. [Lang]
## v2026.2.8-3 (2026-04-22)
### Fix
* The error message cannot be seen during avatar changing #10. [Lang]
## v2026.2.8-2 (2026-04-21)
### Changes
* Increase the 2 MB avatar maximum file size to 10 MB #10. [Lang]
## v2026.2.8-1 (2026-04-21)
### Changes
* Upgrade front-end & back-end deps to the latest available version #10. [Lang]
## v2026.2.8-0 (2026-04-21) ## v2026.2.8-0 (2026-04-21)
### New ### New
+1
View File
@@ -23,6 +23,7 @@ RUN install-php-extensions \
sodium sodium
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
RUN printf '[PHP]\nupload_max_filesize=10M\npost_max_size=11M\n' > "$PHP_INI_DIR/conf.d/uploads.ini"
RUN printf '[opcache]\nopcache.enable=1\nopcache.memory_consumption=256\nopcache.max_accelerated_files=20000\nopcache.validate_timestamps=0\n' \ RUN printf '[opcache]\nopcache.enable=1\nopcache.memory_consumption=256\nopcache.max_accelerated_files=20000\nopcache.validate_timestamps=0\n' \
> "$PHP_INI_DIR/conf.d/opcache.ini" > "$PHP_INI_DIR/conf.d/opcache.ini"
+8 -2
View File
@@ -4,8 +4,8 @@
help: help:
@echo "Available commands:" @echo "Available commands:"
@echo " make start - Start services without building (uses existing images)" @echo " make start - Start services and initialize MinIO (uses existing images)"
@echo " make start-build - Start services and build images if needed" @echo " make start-build - Build, start services, and initialize MinIO"
@echo " make stop - Stop running services" @echo " make stop - Stop running services"
@echo " make build - Build Docker images only" @echo " make build - Build Docker images only"
@echo " make down - Stop and remove containers/networks" @echo " make down - Stop and remove containers/networks"
@@ -19,11 +19,17 @@ help:
@echo " make test - Run PHPUnit tests" @echo " make test - Run PHPUnit tests"
start: start:
docker compose up -d minio
docker compose rm -sf minio_init
docker compose run --rm minio_init
docker compose up -d docker compose up -d
start-build: start-build:
composer i composer i
bun run build bun run build
docker compose up -d --build minio
docker compose rm -sf minio_init
docker compose run --rm minio_init
docker compose up -d --build docker compose up -d --build
stop: stop:
+4 -3
View File
@@ -78,7 +78,8 @@ Edit `.env` and fill in every value. Key ones:
| `MINIO_ROOT_USER/PASSWORD` | MinIO admin credentials | | `MINIO_ROOT_USER/PASSWORD` | MinIO admin credentials |
| `MINIO_ENDPOINT` | `http://localhost:9000` (bare-metal) | | `MINIO_ENDPOINT` | `http://localhost:9000` (bare-metal) |
| `MINIO_PUBLIC_URL` | Public URL browsers use to reach MinIO | | `MINIO_PUBLIC_URL` | Public URL browsers use to reach MinIO |
| `RECAPTCHA_SITE_KEY/SECRET_KEY` | Google reCAPTCHA v3 keys for your domain | | `CAP_API_ENDPOINT` | Full URL of your Cap instance including the site-key path (e.g. `https://cap.example.com/my-site-key/`) |
| `CAP_SECRET_KEY` | Secret key from your Cap site dashboard (stored in `PROD_ENV_FILE` Gitea secret on prod) |
| `MERCURE_JWT_SECRET` | Random secret (generated in step 3) | | `MERCURE_JWT_SECRET` | Random secret (generated in step 3) |
| `MERCURE_JWT_TOKEN` | Signed publisher JWT (generated in step 3) | | `MERCURE_JWT_TOKEN` | Signed publisher JWT (generated in step 3) |
| `MERCURE_SUBSCRIBER_JWT` | Signed subscriber JWT (generated in step 3) | | `MERCURE_SUBSCRIBER_JWT` | Signed subscriber JWT (generated in step 3) |
@@ -222,8 +223,8 @@ MINIO_PUBLIC_URL=https://aws.mineseeker.hu
MAILER_DSN=smtp://mail:25?verify_peer=0 MAILER_DSN=smtp://mail:25?verify_peer=0
MAIL_DOMAIN=mineseeker.hu MAIL_DOMAIN=mineseeker.hu
RECAPTCHA_SITE_KEY="<your reCAPTCHA v3 site key>" CAP_API_ENDPOINT="https://cap.example.com/your-site-key/"
RECAPTCHA_SECRET_KEY="<your reCAPTCHA v3 secret key>" CAP_SECRET_KEY="<secret key from your Cap site dashboard>"
MERCURE_URL=https://mineseeker.hu/.well-known/mercure MERCURE_URL=https://mineseeker.hu/.well-known/mercure
MERCURE_PUBLIC_URL=https://mineseeker.hu/.well-known/mercure MERCURE_PUBLIC_URL=https://mineseeker.hu/.well-known/mercure
+60 -1
View File
@@ -75,7 +75,7 @@
border-radius: 10px; border-radius: 10px;
padding: 44px 48px 40px; padding: 44px 48px 40px;
width: 100%; width: 100%;
max-width: 420px; max-width: 520px;
backdrop-filter: blur(4px); backdrop-filter: blur(4px);
box-shadow: 0 8px 48px rgba(0, 0, 0, 0.5); box-shadow: 0 8px 48px rgba(0, 0, 0, 0.5);
} }
@@ -87,6 +87,65 @@
margin-bottom: 6px; margin-bottom: 6px;
} }
.auth-title-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 6px;
.auth-title {
margin-bottom: 0;
}
}
.auth-cap-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 999px;
border: 1px solid rgba(149, 207, 245, 0.35);
background:
linear-gradient(115deg, transparent 0 38%, rgba(255, 255, 255, 0.18) 48%, transparent 58% 100%),
rgba(35, 111, 135, 0.2);
background-position: 120% 50%, 0 0;
background-size: 260% 100%, auto;
color: #95cff5;
font: 600 11px 'Rajdhani', sans-serif;
letter-spacing: 0.8px;
text-decoration: none;
text-transform: uppercase;
transition:
background-position 650ms ease,
border-color 200ms ease,
box-shadow 200ms ease,
color 200ms ease,
transform 200ms ease;
white-space: nowrap;
&:hover,
&:focus-visible {
background-position: -60% 50%, 0 0;
border-color: rgba(149, 207, 245, 0.65);
box-shadow: 0 0 18px rgba(35, 111, 135, 0.28);
color: #d5f1ff;
transform: translateY(-1px);
}
&:hover,
&:focus,
&:active,
&:visited {
text-decoration: none;
}
&:focus-visible {
outline: 2px solid rgba(149, 207, 245, 0.75);
outline-offset: 2px;
}
}
.auth-sub { .auth-sub {
font: 400 14px 'Rajdhani', sans-serif; font: 400 14px 'Rajdhani', sans-serif;
color: rgba(149, 207, 245, 0.6); color: rgba(149, 207, 245, 0.6);
+23
View File
@@ -119,3 +119,26 @@ footer {
&:hover { color: #95cff5; } &:hover { color: #95cff5; }
} }
} }
.footer-version {
display: inline-flex;
align-items: center;
gap: 5px;
margin: 0 7px;
padding: 3px 8px 2px;
border: 1px solid rgba(149, 207, 245, 0.24);
border-radius: 999px;
background: linear-gradient(135deg, rgba(35, 111, 135, 0.22), rgba(149, 207, 245, 0.08));
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08), 0 0 12px rgba(35, 111, 135, 0.12);
color: rgba(180, 224, 248, 0.9);
font: 700 10px 'Rajdhani', sans-serif;
letter-spacing: 1px;
line-height: 1;
text-transform: uppercase;
vertical-align: middle;
i {
color: #95cff5;
font-size: 9px;
}
}
+15
View File
@@ -28,6 +28,21 @@
box-shadow: 0 8px 48px rgba(0, 0, 0, 0.4); box-shadow: 0 8px 48px rgba(0, 0, 0, 0.4);
} }
#profile-avatar-root {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.profile-avatar__error {
font-size: 11px;
color: #e57373;
text-align: center;
max-width: 120px;
line-height: 1.3;
}
.profile-avatar { .profile-avatar {
position: relative; position: relative;
width: 80px; width: 80px;
+75 -3
View File
@@ -7,12 +7,17 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
.back-from-game { .game-brand-bar {
display: inline-block; display: flex;
align-items: center;
position: fixed; position: fixed;
top: 20px; top: 20px;
left: 20px; left: 20px;
z-index: 20;
}
.back-from-game {
display: inline-block;
-ms-transform: scale(1); -ms-transform: scale(1);
-webkit-transform: scale(1); -webkit-transform: scale(1);
transform: scale(1); transform: scale(1);
@@ -30,4 +35,71 @@
transform: scale(1.2); transform: scale(1.2);
-webkit-transition: all 250ms cubic-bezier(.17, .67, .83, .67); -webkit-transition: all 250ms cubic-bezier(.17, .67, .83, .67);
transition: all 250ms cubic-bezier(.17, .67, .83, .67); transition: all 250ms cubic-bezier(.17, .67, .83, .67);
} }
.game-cap-badge {
display: inline-flex;
align-items: center;
gap: 6px;
position: fixed;
bottom: 20px;
left: 50%;
z-index: 20;
padding: 5px 10px;
border: 1px solid rgba(255, 216, 92, 0.42);
-webkit-border-radius: 999px;
border-radius: 999px;
background:
linear-gradient(115deg, transparent 0 38%, rgba(255, 255, 255, 0.22) 48%, transparent 58% 100%),
rgba(14, 16, 18, 0.72);
background-position: 120% 50%, 0 0;
background-size: 260% 100%, auto;
-webkit-box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.32), 0 8px 22px rgba(0, 0, 0, 0.28);
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.32), 0 8px 22px rgba(0, 0, 0, 0.28);
color: #ffe58a;
font: 700 11px 'Rajdhani', sans-serif;
letter-spacing: 0.9px;
line-height: 1;
text-decoration: none;
text-transform: uppercase;
-ms-transform: translateX(-50%);
-webkit-transform: translateX(-50%);
transform: translateX(-50%);
white-space: nowrap;
-webkit-transition:
background-position 650ms ease,
border-color 200ms ease,
box-shadow 200ms ease,
color 200ms ease,
transform 200ms ease;
transition:
background-position 650ms ease,
border-color 200ms ease,
box-shadow 200ms ease,
color 200ms ease,
transform 200ms ease;
}
.game-cap-badge:hover,
.game-cap-badge:focus-visible {
background-position: -60% 50%, 0 0;
border-color: rgba(255, 229, 138, 0.78);
-webkit-box-shadow: 0 0 0 1px rgba(255, 229, 138, 0.18), 0 0 24px rgba(255, 195, 50, 0.28);
box-shadow: 0 0 0 1px rgba(255, 229, 138, 0.18), 0 0 24px rgba(255, 195, 50, 0.28);
color: #fff4bd;
text-decoration: none;
-ms-transform: translateX(-50%) translateY(-1px);
-webkit-transform: translateX(-50%) translateY(-1px);
transform: translateX(-50%) translateY(-1px);
}
.game-cap-badge:focus,
.game-cap-badge:active,
.game-cap-badge:visited {
text-decoration: none;
}
.game-cap-badge:focus-visible {
outline: 2px solid rgba(255, 229, 138, 0.75);
outline-offset: 2px;
}
+87
View File
@@ -0,0 +1,87 @@
/**
* This file is part of the SplendidBear Websites' projects.
*
* Copyright (c) 2026 @ www.splendidbear.org
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'cap-widget';
const FORMS_SELECTOR = '.auth-form';
const SUBMIT_SELECTOR = 'button[type="submit"], input[type="submit"]';
const createInvisibleCap = apiEndpoint => {
const host = document.createElement('cap-widget');
host.style.display = 'none';
const cap = new window.Cap({ apiEndpoint }, host);
return { cap, host };
};
const lockSubmit = (form, locked) => {
const submit = form.querySelector(SUBMIT_SELECTOR);
if (!submit) return;
submit.disabled = locked;
};
const bindInvisibleCap = (form, apiEndpoint) => {
const { cap, host } = createInvisibleCap(apiEndpoint);
form.appendChild(host);
let solving = null;
const solveToken = async () => {
if (!solving) {
solving = cap.solve()
.finally(() => {
solving = null;
});
}
const result = await solving;
return result?.token ?? cap.token ?? '';
};
lockSubmit(form, true);
solveToken()
.catch(() => '')
.finally(() => lockSubmit(form, false));
form.addEventListener('submit', async e => {
if (cap.token) {
return;
}
e.preventDefault();
lockSubmit(form, true);
try {
const token = await solveToken();
if (!token) {
lockSubmit(form, false);
return;
}
form.requestSubmit();
} catch (_) {
lockSubmit(form, false);
}
});
};
const initInvisibleCap = () => {
const endpointSource = document.querySelector('[data-cap-api-endpoint]')
|| document.getElementById('mine-wrapper');
const apiEndpoint = endpointSource?.dataset.capApiEndpoint;
if ('function' !== typeof window.Cap || !apiEndpoint) {
return;
}
document.querySelectorAll(FORMS_SELECTOR).forEach(form => bindInvisibleCap(form, apiEndpoint));
};
initInvisibleCap();
+25 -23
View File
@@ -7,7 +7,7 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
import React, { useMemo, useRef } from 'react'; import React, { Fragment, useMemo, useRef } from 'react';
import { string } from 'prop-types'; import { string } from 'prop-types';
import { useProfileDataProvider } from '@mine-hooks/useGameDataProvider'; import { useProfileDataProvider } from '@mine-hooks/useGameDataProvider';
@@ -16,6 +16,8 @@ export const AvatarUpload = ({ uploadUrl, initialThumbUrl, initials }) => {
const [thumbUrl, setThumbUrl] = React.useState(initialThumbUrl || null); const [thumbUrl, setThumbUrl] = React.useState(initialThumbUrl || null);
const { uploadAvatarMutation: { isPending, error, mutate } } = useProfileDataProvider(); const { uploadAvatarMutation: { isPending, error, mutate } } = useProfileDataProvider();
const errorMessage = useMemo(() => error?.message ?? null, [error]);
const handleChange = e => { const handleChange = e => {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
if (!file) return; if (!file) return;
@@ -40,32 +42,32 @@ export const AvatarUpload = ({ uploadUrl, initialThumbUrl, initials }) => {
}); });
}; };
const errorMessage = useMemo(() => error?.message ?? null, [error]);
return ( return (
<div <Fragment>
className={`profile-avatar${isPending ? ' profile-avatar--loading' : ''}`} <div
title="Click to change profile picture" className={`profile-avatar${isPending ? ' profile-avatar--loading' : ''}`}
onClick={() => inputRef.current?.click()} title="Click to change profile picture"
> onClick={() => inputRef.current?.click()}
{thumbUrl >
? <img src={thumbUrl} alt={initials} className="profile-avatar__img" /> {thumbUrl
: <span className="profile-avatar__initials">{initials}</span> ? <img src={thumbUrl} alt={initials} className="profile-avatar__img" />
} : <span className="profile-avatar__initials">{initials}</span>
<div className="profile-avatar__overlay"> }
<i className="fa fa-camera" /> <div className="profile-avatar__overlay">
<i className="fa fa-camera" />
</div>
<input
ref={inputRef}
type="file"
accept="image/jpeg,image/png,image/gif,image/webp"
style={{ display: 'none' }}
onChange={handleChange}
/>
</div> </div>
<input
ref={inputRef}
type="file"
accept="image/jpeg,image/png,image/gif,image/webp"
style={{ display: 'none' }}
onChange={handleChange}
/>
{errorMessage && <div className="profile-avatar__error">{errorMessage}</div>} {errorMessage && <div className="profile-avatar__error">{errorMessage}</div>}
</div> </Fragment>
); );
} };
AvatarUpload.propTypes = { AvatarUpload.propTypes = {
uploadUrl: string.isRequired, uploadUrl: string.isRequired,
-89
View File
@@ -1,89 +0,0 @@
/**
* This file is part of the SplendidBear Websites' projects.
*
* Copyright (c) 2026 @ www.splendidbear.org
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import { useEffect, useRef } from 'react';
import { string } from 'prop-types';
/**
* ContactForm Component
*
* Handles reCAPTCHA v3 integration for the contact form.
* Intercepts form submission, executes reCAPTCHA, and submits the form with the token.
*
* @param {string} siteKey - Google reCAPTCHA site key
* @param {string} recaptchaFieldId - ID of the hidden recaptcha input field
*/
const ContactForm = ({ siteKey, recaptchaFieldId }) => {
const formRef = useRef(null);
const isSubmittingRef = useRef(false);
useEffect(() => {
const form = document.querySelector('.auth-form');
if (!form) {
console.warn('ContactForm: No .auth-form found');
return;
}
formRef.current = form;
const handleSubmit = e => {
e.preventDefault();
if (isSubmittingRef.current) {
return;
}
isSubmittingRef.current = true;
if ('undefined' !== typeof grecaptcha) {
grecaptcha.ready(() => {
grecaptcha
.execute(siteKey, { action: 'contact' })
.then(token => {
const recaptchaField = document.getElementById(recaptchaFieldId);
if (recaptchaField) {
recaptchaField.value = token;
} else {
console.error(`ContactForm: Recaptcha field with ID "${recaptchaFieldId}" not found`);
}
isSubmittingRef.current = false;
form.submit();
})
.catch(error => {
console.error('ContactForm: reCAPTCHA execution failed', error);
isSubmittingRef.current = false;
});
});
} else {
console.error('ContactForm: grecaptcha is not loaded');
isSubmittingRef.current = false;
}
};
form.addEventListener('submit', handleSubmit);
return () => {
if (formRef.current) {
formRef.current.removeEventListener('submit', handleSubmit);
}
};
}, [siteKey, recaptchaFieldId]);
return null;
};
ContactForm.propTypes = {
siteKey: string.isRequired,
recaptchaFieldId: string.isRequired,
};
export default ContactForm;
-1
View File
@@ -9,7 +9,6 @@
export { AvatarUpload } from './AvatarUpload'; export { AvatarUpload } from './AvatarUpload';
export { BattleDialog } from './BattleDialog'; export { BattleDialog } from './BattleDialog';
export { default as ContactForm } from './ContactForm';
export { default as PasskeyLogin } from './PasskeyLogin'; export { default as PasskeyLogin } from './PasskeyLogin';
export { default as PasskeyManager } from './PasskeyManager'; export { default as PasskeyManager } from './PasskeyManager';
export { default as ProfileCharts } from './ProfileCharts'; export { default as ProfileCharts } from './ProfileCharts';
-30
View File
@@ -1,30 +0,0 @@
/**
* This file is part of the SplendidBear Websites' projects.
*
* Copyright (c) 2026 @ www.splendidbear.org
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import React from 'react';
import { createRoot } from 'react-dom/client';
import { ContactForm } from '@global-components';
const wrapper = document.getElementById('contact-form-wrapper');
if (wrapper) {
const siteKey = wrapper.dataset.siteKey;
const recaptchaFieldId = wrapper.dataset.recaptchaFieldId;
if (siteKey && recaptchaFieldId) {
createRoot(wrapper).render(
<ContactForm
siteKey={siteKey}
recaptchaFieldId={recaptchaFieldId}
/>
);
} else {
console.error('ContactForm: Missing siteKey or recaptchaFieldId in data attributes');
}
}
@@ -7,17 +7,16 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react'; import 'cap-widget';
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react';
import { func, node, string } from 'prop-types'; import { func, node, string } from 'prop-types';
const CAPTCHA_STORAGE_KEY = 'mineseeker_captcha_verified'; const CAPTCHA_STORAGE_KEY = 'mineseeker_captcha_verified';
const CAPTCHA_TOKEN_KEY = 'mineseeker_captcha_token'; const CAPTCHA_TOKEN_KEY = 'mineseeker_captcha_token';
const RECAPTCHA_ACTION = 'mineseeker_play';
const CaptchaOverlay = ({ siteKey, onVerified, children }) => { const CaptchaOverlay = ({ apiEndpoint, onVerified, children }) => {
const [verified, setVerified] = useState(false); const [verified, setVerified] = useState(false);
const [loading, setLoading] = useState(false); const capRef = useRef(null);
const [error, setError] = useState(false);
const handleToken = useCallback(token => { const handleToken = useCallback(token => {
const wrapper = document.getElementById('mine-wrapper'); const wrapper = document.getElementById('mine-wrapper');
@@ -30,12 +29,6 @@ const CaptchaOverlay = ({ siteKey, onVerified, children }) => {
onVerified?.(); onVerified?.();
}, [onVerified]); }, [onVerified]);
const buttonClasses = useMemo(() => [
'captcha-button',
error && 'captcha-button--error',
loading && 'captcha-button--loading',
].filter(Boolean).join(' '), [error, loading]);
useEffect(() => { useEffect(() => {
const storedToken = sessionStorage.getItem(CAPTCHA_TOKEN_KEY); const storedToken = sessionStorage.getItem(CAPTCHA_TOKEN_KEY);
const storedTime = sessionStorage.getItem(CAPTCHA_STORAGE_KEY); const storedTime = sessionStorage.getItem(CAPTCHA_STORAGE_KEY);
@@ -52,40 +45,42 @@ const CaptchaOverlay = ({ siteKey, onVerified, children }) => {
return; return;
} }
} }
}, [onVerified]);
if (window.grecaptcha) { useEffect(() => {
window.grecaptcha.ready(() => { const widget = document.createElement('cap-widget');
window.grecaptcha widget.style.display = 'none';
.execute(siteKey, { action: RECAPTCHA_ACTION }) capRef.current = widget;
.then(token => { document.body.appendChild(widget);
handleToken(token);
})
.catch(() => {
setError(true);
});
});
}
}, [siteKey, onVerified, handleToken]);
const cap = new window.Cap({ apiEndpoint }, widget);
let cancelled = false;
const handleClick = () => { const run = async () => {
setLoading(true); try {
setError(false); const result = await cap.solve();
if (!cancelled && result?.token) {
handleToken(result.token);
}
} catch (_) {
if (!cancelled) {
setTimeout(() => {
if (!cancelled) {
run();
}
}, 1200);
}
}
};
window.grecaptcha.ready(() => { run();
window.grecaptcha
.execute(siteKey, { action: RECAPTCHA_ACTION }) return () => {
.then(token => { cancelled = true;
handleToken(token); widget.remove();
setLoading(false); capRef.current = null;
}) };
.catch(() => { }, [apiEndpoint, handleToken]);
setLoading(false);
setError(true);
setTimeout(() => setError(false), 2000);
});
});
};
if (verified) { if (verified) {
return <Fragment>{children}</Fragment>; return <Fragment>{children}</Fragment>;
@@ -99,16 +94,8 @@ const CaptchaOverlay = ({ siteKey, onVerified, children }) => {
</div> </div>
<h1 className="captcha-title">Ready to Play?</h1> <h1 className="captcha-title">Ready to Play?</h1>
<p className="captcha-description"> <p className="captcha-description">
Click below to verify you&apos;re human and start playing. Verifying your session...
</p> </p>
<button
className={buttonClasses}
onClick={handleClick}
disabled={loading}
>
<i className={`fa ${loading ? 'fa-spinner fa-spin' : error ? 'fa-exclamation-circle' : 'fa-play'}`} />
{loading ? 'Verifying...' : error ? 'Try Again' : 'Start Playing'}
</button>
</div> </div>
</div> </div>
); );
@@ -117,7 +104,7 @@ const CaptchaOverlay = ({ siteKey, onVerified, children }) => {
export default CaptchaOverlay; export default CaptchaOverlay;
CaptchaOverlay.propTypes = { CaptchaOverlay.propTypes = {
siteKey: string.isRequired, apiEndpoint: string.isRequired,
onVerified: func, onVerified: func,
children: node, children: node,
}; };
@@ -19,7 +19,7 @@ export const GameBoard = ({ gameAssoc, gameInherited, opponentName = '', isEnvDe
const { onClick, resign } = useServerCommunication(gameAssoc, gameInherited, opponentName, isEnvDev); const { onClick, resign } = useServerCommunication(gameAssoc, gameInherited, opponentName, isEnvDev);
const [captchaVerified, setCaptchaVerified] = useState(false); const [captchaVerified, setCaptchaVerified] = useState(false);
const siteKey = document.getElementById('mine-wrapper')?.dataset.recaptchaSiteKey; const apiEndpoint = document.getElementById('mine-wrapper')?.dataset.capApiEndpoint;
if (!gridReady) { if (!gridReady) {
return ( return (
@@ -29,9 +29,9 @@ export const GameBoard = ({ gameAssoc, gameInherited, opponentName = '', isEnvDe
); );
} }
if (!captchaVerified && siteKey) { if (!captchaVerified && apiEndpoint) {
return ( return (
<CaptchaOverlay siteKey={siteKey} onVerified={() => setCaptchaVerified(true)} /> <CaptchaOverlay apiEndpoint={apiEndpoint} onVerified={() => setCaptchaVerified(true)} />
); );
} }
@@ -212,7 +212,7 @@ const useServerCommunication = (gameAssoc, gameInherited, opponentName, isEnvDev
*/ */
if (!endRef.current && (!isTrueRestoredRef.current || 0 !== opponentLastSeenRef.current)) { if (!endRef.current && (!isTrueRestoredRef.current || 0 !== opponentLastSeenRef.current)) {
hideOverlay(); hideOverlay();
sounds.current.starting.play(); sounds.current.starting.play();
} }
}; };
+105 -70
View File
@@ -11,26 +11,27 @@
"@fontsource/open-sans": "^5.2.7", "@fontsource/open-sans": "^5.2.7",
"@fontsource/rajdhani": "^5.2.7", "@fontsource/rajdhani": "^5.2.7",
"@fortawesome/fontawesome-free": "^7.2.0", "@fortawesome/fontawesome-free": "^7.2.0",
"@mui/material": "^9.0.0", "@mui/material": "^9.0.1",
"@mui/x-charts": "^9.0.2", "@mui/x-charts": "^9.3.0",
"@tanstack/react-query": "^5.99.2", "@tanstack/react-query": "^5.100.14",
"cap-widget": "^0.1.53",
"howler": "^2.2.4", "howler": "^2.2.4",
"lodash": "^4.18.1", "lodash": "^4.18.1",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react": "^19.2.5", "react": "^19.2.7",
"react-dom": "^19.2.5", "react-dom": "^19.2.7",
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.3.5", "@eslint/eslintrc": "^3.3.5",
"@eslint/js": "10.0.1", "@eslint/js": "10.0.1",
"@stylistic/eslint-plugin": "5.10.0", "@stylistic/eslint-plugin": "5.10.0",
"@vitejs/plugin-react": "^6.0.1", "@vitejs/plugin-react": "^6.0.2",
"eslint": "10.2.1", "eslint": "^10.4.1",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "7.1.1", "eslint-plugin-react-hooks": "7.1.1",
"globals": "17.5.0", "globals": "^17.6.0",
"sass": "^1.99.0", "sass": "^1.100.0",
"vite": "^8.0.8", "vite": "^8.0.16",
"vite-plugin-symfony": "^8.2.4", "vite-plugin-symfony": "^8.2.4",
}, },
}, },
@@ -70,11 +71,13 @@
"@babel/types": ["@babel/types@7.29.0", "http://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], "@babel/types": ["@babel/types@7.29.0", "http://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
"@emnapi/core": ["@emnapi/core@1.9.2", "http://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], "@base-ui/utils": ["@base-ui/utils@0.3.1", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@floating-ui/utils": "^0.2.11", "reselect": "^5.2.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-gFFiltORVmW/N6IILTGxizP3PBpVpysqML1ALY5Vk0mH+7faVkCknOU31goYHN5Aoek2dkjxva1XOD2Ce9WuIg=="],
"@emnapi/runtime": ["@emnapi/runtime@1.9.2", "http://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], "@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "http://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], "@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
"@emotion/babel-plugin": ["@emotion/babel-plugin@11.13.5", "http://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", { "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/serialize": "^1.3.3", "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", "find-root": "^1.1.0", "source-map": "^0.5.7", "stylis": "4.2.0" } }, "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ=="], "@emotion/babel-plugin": ["@emotion/babel-plugin@11.13.5", "http://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", { "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/serialize": "^1.3.3", "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", "find-root": "^1.1.0", "source-map": "^0.5.7", "stylis": "4.2.0" } }, "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ=="],
@@ -108,25 +111,27 @@
"@eslint/config-array": ["@eslint/config-array@0.23.5", "http://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="], "@eslint/config-array": ["@eslint/config-array@0.23.5", "http://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="],
"@eslint/config-helpers": ["@eslint/config-helpers@0.5.5", "http://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w=="], "@eslint/config-helpers": ["@eslint/config-helpers@0.7.0", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw=="],
"@eslint/core": ["@eslint/core@1.2.1", "http://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="], "@eslint/core": ["@eslint/core@1.2.1", "http://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="],
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "http://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="], "@eslint/eslintrc": ["@eslint/eslintrc@3.3.6", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.3.0", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA=="],
"@eslint/js": ["@eslint/js@10.0.1", "http://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", { "peerDependencies": { "eslint": "^10.0.0" }, "optionalPeers": ["eslint"] }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="], "@eslint/js": ["@eslint/js@10.0.1", "http://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", { "peerDependencies": { "eslint": "^10.0.0" }, "optionalPeers": ["eslint"] }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="],
"@eslint/object-schema": ["@eslint/object-schema@3.0.5", "http://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="], "@eslint/object-schema": ["@eslint/object-schema@3.0.5", "http://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="],
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.1", "http://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ=="], "@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.2", "http://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A=="],
"@fontsource/changa-one": ["@fontsource/changa-one@5.2.8", "http://registry.npmjs.org/@fontsource/changa-one/-/changa-one-5.2.8.tgz", {}, "sha512-gagiU8sMWLs9ejh41NmrYlGdgasSYWFegz5/+22WqYdJVS9HZbaUEXj/6jj3ZKgi+dQZT0kYI+Nha2UQGbO/mA=="], "@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="],
"@fontsource/open-sans": ["@fontsource/open-sans@5.2.7", "http://registry.npmjs.org/@fontsource/open-sans/-/open-sans-5.2.7.tgz", {}, "sha512-8yfgDYjE5O0vmTPdrcjV35y4yMnctsokmi9gN49Gcsr0sjzkMkR97AnKDe6OqZh2SFkYlR28fxOvi21bYEgMSw=="], "@fontsource/changa-one": ["@fontsource/changa-one@5.3.0", "", {}, "sha512-ACriykYOe12aupip0QfAaNo9/XNEt0WD1U1CiL/ZUGnrzIXFizJEkUzjnXWg+M4GXkY4n0o/W6TUtcNqMn0GrA=="],
"@fontsource/rajdhani": ["@fontsource/rajdhani@5.2.7", "http://registry.npmjs.org/@fontsource/rajdhani/-/rajdhani-5.2.7.tgz", {}, "sha512-7Gy10U688fCdeFfYKebUF2TZotdgH/ghKyMsseXPmB60lpaUHC8aoCSJl5/OpZ+KHKSU2TqBfKfteVkcIXxTAQ=="], "@fontsource/open-sans": ["@fontsource/open-sans@5.3.0", "", {}, "sha512-V1bdMlGNyZrJwzrdusshMOw2YR0aqYDL7kOo9dkxUEKl8dFv66A9HLckOOXl572sG/7ohhDQrCrTFGSn9LZcSw=="],
"@fortawesome/fontawesome-free": ["@fortawesome/fontawesome-free@7.2.0", "http://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.2.0.tgz", {}, "sha512-3DguDv/oUE+7vjMeTSOjCSG+KeawgVQOHrKRnvUuqYh1mfArrh7s+s8hXW3e4RerBA1+Wh+hBqf8sJNpqNrBWg=="], "@fontsource/rajdhani": ["@fontsource/rajdhani@5.3.0", "", {}, "sha512-oBJgSIibWJvVBKseme1JczqPLoDHN97LHti4uh0LjfcJqQoVmnQQlRKIpEjTJV559rmVQ6qbVZZwKwk2wkBTtw=="],
"@fortawesome/fontawesome-free": ["@fortawesome/fontawesome-free@7.3.1", "", {}, "sha512-wmglKKPDIkgV3aWlZzWECCPoGIkYCulzBwxG9+w7rc5BGapZ6cPMpoPOT8k36J0Ni7PPX6c/rsoMWfS4d1MUMg=="],
"@humanfs/core": ["@humanfs/core@0.19.1", "http://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], "@humanfs/core": ["@humanfs/core@0.19.1", "http://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
@@ -146,29 +151,29 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "http://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "http://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@mui/core-downloads-tracker": ["@mui/core-downloads-tracker@9.0.0", "http://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-9.0.0.tgz", {}, "sha512-uwQNGkhv0lf7ufxw6QXev77BW6pWbW+7uxYjU5+rfp4lBkFtMEgJCsarTM3Tn+i0lGx6+Ol2u88JdGXr0GDskA=="], "@mui/core-downloads-tracker": ["@mui/core-downloads-tracker@9.2.0", "", {}, "sha512-+XMav+ZaXkZKUFUgzjrfMEedfyJKxxviAske2q8N8CWDMeqZdDU2lWMkiUPiB388hGaDqhwvOAwkrsc/pUyp8g=="],
"@mui/material": ["@mui/material@9.0.0", "http://registry.npmjs.org/@mui/material/-/material-9.0.0.tgz", { "dependencies": { "@babel/runtime": "^7.29.2", "@mui/core-downloads-tracker": "^9.0.0", "@mui/system": "^9.0.0", "@mui/types": "^9.0.0", "@mui/utils": "^9.0.0", "@popperjs/core": "^2.11.8", "@types/react-transition-group": "^4.4.12", "clsx": "^2.1.1", "csstype": "^3.2.3", "prop-types": "^15.8.1", "react-is": "^19.2.4", "react-transition-group": "^4.4.5" }, "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", "@mui/material-pigment-css": "^9.0.0", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/react", "@emotion/styled", "@mui/material-pigment-css", "@types/react"] }, "sha512-+VP/oQCDhDR87NQQgXnNBG8dwy6GNuQLnenS1pZvkbn2dKFSxRSRMybTpH9xUxXP+316mlYDy5CSbYtusnCWtw=="], "@mui/material": ["@mui/material@9.2.0", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@mui/core-downloads-tracker": "^9.2.0", "@mui/system": "^9.2.0", "@mui/types": "^9.1.1", "@mui/utils": "^9.2.0", "@popperjs/core": "^2.11.8", "@types/react-transition-group": "^4.4.12", "clsx": "^2.1.1", "csstype": "^3.2.3", "prop-types": "^15.8.1", "react-is": "^19.2.6", "react-transition-group": "^4.4.5" }, "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", "@mui/material-pigment-css": "^9.2.0", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/react", "@emotion/styled", "@mui/material-pigment-css", "@types/react"] }, "sha512-+YTRSgGKGrrRo2XJZXs7JRA6qHoHWvNtxyqxnrRJTBmIuLOUpxxh7m4G9lF4tWberxGFY+EqkkRPgJCl+fSMJg=="],
"@mui/private-theming": ["@mui/private-theming@9.0.0", "http://registry.npmjs.org/@mui/private-theming/-/private-theming-9.0.0.tgz", { "dependencies": { "@babel/runtime": "^7.29.2", "@mui/utils": "^9.0.0", "prop-types": "^15.8.1" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-JtuZoaiCqwD6vjgYu6Xp3T7DZkrxJlgtDz5yESzhI34fEX5hHMh2VJUbuL9UOg8xrfIFMrq6dcYoH/7Zi4G0RA=="], "@mui/private-theming": ["@mui/private-theming@9.2.0", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@mui/utils": "^9.2.0", "prop-types": "^15.8.1" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-w9wpyDxGPGnAACPB2hKhCDmILJIAvQxrfjUbIAEa0AznX1rOjaz5N+yB1uuw8ixnJcpEh/tPbD9oEe19wcWPHw=="],
"@mui/styled-engine": ["@mui/styled-engine@9.0.0", "http://registry.npmjs.org/@mui/styled-engine/-/styled-engine-9.0.0.tgz", { "dependencies": { "@babel/runtime": "^7.29.2", "@emotion/cache": "^11.14.0", "@emotion/serialize": "^1.3.3", "@emotion/sheet": "^1.4.0", "csstype": "^3.2.3", "prop-types": "^15.8.1" }, "peerDependencies": { "@emotion/react": "^11.4.1", "@emotion/styled": "^11.3.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/react", "@emotion/styled"] }, "sha512-9RLGdX4Jg0aQPRuvqh/OLzYSPlgd5zyEw5/1HIRfdavSiOd03WtUaGZH9/w1RoTYuRKwpgy0hpIFaMHIqPVIWg=="], "@mui/styled-engine": ["@mui/styled-engine@9.1.1", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@emotion/cache": "^11.14.0", "@emotion/serialize": "^1.3.3", "@emotion/sheet": "^1.4.0", "csstype": "^3.2.3", "prop-types": "^15.8.1" }, "peerDependencies": { "@emotion/react": "^11.4.1", "@emotion/styled": "^11.3.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/react", "@emotion/styled"] }, "sha512-neaYKdJfvEG54q8efHLJR7swpHG/gfSv9xGqW5iTSMsubD7yPCPFrhVBt284j1DOF3uZaaDJSHQL7gz6jGF21Q=="],
"@mui/system": ["@mui/system@9.0.0", "http://registry.npmjs.org/@mui/system/-/system-9.0.0.tgz", { "dependencies": { "@babel/runtime": "^7.29.2", "@mui/private-theming": "^9.0.0", "@mui/styled-engine": "^9.0.0", "@mui/types": "^9.0.0", "@mui/utils": "^9.0.0", "clsx": "^2.1.1", "csstype": "^3.2.3", "prop-types": "^15.8.1" }, "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/react", "@emotion/styled", "@types/react"] }, "sha512-YnC5Zg6j04IxiLc/boAKs0464jfZlLFVa7mf5E8lF0XOtZVUvG6R6gJK50lgUYdaaLdyLfxF6xR7LaPuEpeT/g=="], "@mui/system": ["@mui/system@9.2.0", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@mui/private-theming": "^9.2.0", "@mui/styled-engine": "^9.1.1", "@mui/types": "^9.1.1", "@mui/utils": "^9.2.0", "clsx": "^2.1.1", "csstype": "^3.2.3", "prop-types": "^15.8.1" }, "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/react", "@emotion/styled", "@types/react"] }, "sha512-YvUJwKoGVtbnOm2PyPi5TvX2d1rOA6sqSpEWVs4WmXNIaFTuYmNUaVdU2o1NKUEe31URnD3E8ZVUMcsLQXwcYg=="],
"@mui/types": ["@mui/types@9.0.0", "http://registry.npmjs.org/@mui/types/-/types-9.0.0.tgz", { "dependencies": { "@babel/runtime": "^7.29.2" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-i1cuFCAWN44b3AJWO7mh7tuh1sqbQSeVr/94oG0TX5uXivac8XalgE4/6fQZcmGZigzbQ35IXxj/4jLpRIBYZg=="], "@mui/types": ["@mui/types@9.1.1", "", { "dependencies": { "@babel/runtime": "^7.29.2" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-Zjt7u8wNvDg40rPTGoL+TnfkpuSKjwubsNSFRH1KAVZLcaV4I3AFNHIFbvH7p4F3alEibSbdd90xAgn5Rnfndg=="],
"@mui/utils": ["@mui/utils@9.0.0", "http://registry.npmjs.org/@mui/utils/-/utils-9.0.0.tgz", { "dependencies": { "@babel/runtime": "^7.29.2", "@mui/types": "^9.0.0", "@types/prop-types": "^15.7.15", "clsx": "^2.1.1", "prop-types": "^15.8.1", "react-is": "^19.2.4" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-bQcqyg/gjULUqTuyUjSAFr6LQGLvtkNtDbJerAtoUn9kGZ0hg5QJiN1PLHMLbeFpe3te1831uq7GFl2ITokGdg=="], "@mui/utils": ["@mui/utils@9.2.0", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@mui/types": "^9.1.1", "@types/prop-types": "^15.7.15", "clsx": "^2.1.1", "prop-types": "^15.8.1", "react-is": "^19.2.6" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-OsUH5zhlSOM4xmLl53+agug1M1UyWb4zxFxWQCqwKTKUeQPvTENtg3JhrroBD2qpCLKsX5W/DYGERJ4mBUbc8g=="],
"@mui/x-charts": ["@mui/x-charts@9.0.2", "http://registry.npmjs.org/@mui/x-charts/-/x-charts-9.0.2.tgz", { "dependencies": { "@babel/runtime": "^7.28.6", "@mui/utils": "9.0.0", "@mui/x-charts-vendor": "^9.0.0", "@mui/x-internal-gestures": "^9.0.2", "@mui/x-internals": "^9.0.0", "bezier-easing": "^2.1.0", "clsx": "^2.1.1", "prop-types": "^15.8.1", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@emotion/react": "^11.9.0", "@emotion/styled": "^11.8.1", "@mui/material": "^7.3.0 || ^9.0.0", "@mui/system": "^7.3.0 || ^9.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/react", "@emotion/styled"] }, "sha512-bKgjGD+uJbDN/g7tMjVmlNdm+iM4UkCJoYruQmgpQ0l+cip8Kn4kmn1iD//rZ35an+LdWaUZ4MHvMzV76D6EJw=="], "@mui/x-charts": ["@mui/x-charts@9.10.1", "", { "dependencies": { "@babel/runtime": "^7.29.7", "@mui/utils": "^9.2.0", "@mui/x-charts-vendor": "^9.4.0", "@mui/x-internal-gestures": "^9.10.0", "@mui/x-internals": "^9.10.1", "bezier-easing": "^3.0.1", "clsx": "^2.1.1", "prop-types": "^15.8.1", "reselect": "^5.2.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@emotion/react": "^11.9.0", "@emotion/styled": "^11.8.1", "@mui/material": "^7.3.0 || ^9.0.0", "@mui/system": "^7.3.0 || ^9.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/react", "@emotion/styled"] }, "sha512-GVVyuCllFkega4yJEIxDLcJCoFuvU9rkjt4PMcWrAWEQxKsaN2TNTY5CmcJ3NJBRSoe0oN3/dBeWBS2ul4t7FA=="],
"@mui/x-charts-vendor": ["@mui/x-charts-vendor@9.0.0", "http://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-9.0.0.tgz", { "dependencies": { "@babel/runtime": "^7.28.6", "@types/d3-array": "^3.2.2", "@types/d3-color": "^3.1.3", "@types/d3-format": "^3.0.4", "@types/d3-interpolate": "^3.0.4", "@types/d3-path": "^3.1.1", "@types/d3-scale": "^4.0.9", "@types/d3-shape": "^3.1.8", "@types/d3-time": "^3.0.4", "@types/d3-time-format": "^4.0.3", "@types/d3-timer": "^3.0.2", "d3-array": "^3.2.4", "d3-color": "^3.1.0", "d3-format": "^3.1.2", "d3-interpolate": "^3.0.1", "d3-path": "^3.1.0", "d3-scale": "^4.0.2", "d3-shape": "^3.2.0", "d3-time": "^3.1.0", "d3-time-format": "^4.1.0", "d3-timer": "^3.0.1", "flatqueue": "^3.0.0", "internmap": "^2.0.3" } }, "sha512-Do91i+fZiNj/4LN5oaGpJoutolzDBDwdfw6tHrx2LKXDMCRlaImCfreLbdbkk7dFsi9fuIP7hWiMV4vDJKPJTA=="], "@mui/x-charts-vendor": ["@mui/x-charts-vendor@9.4.0", "", { "dependencies": { "@babel/runtime": "^7.29.7", "@types/d3-array": "^3.2.2", "@types/d3-color": "^3.1.3", "@types/d3-format": "^3.0.4", "@types/d3-geo": "^3.1.0", "@types/d3-interpolate": "^3.0.4", "@types/d3-path": "^3.1.1", "@types/d3-scale": "^4.0.9", "@types/d3-shape": "^3.1.8", "@types/d3-time": "^3.0.4", "@types/d3-time-format": "^4.0.3", "@types/d3-timer": "^3.0.2", "d3-array": "^3.2.4", "d3-color": "^3.1.0", "d3-format": "^3.1.2", "d3-geo": "^3.1.1", "d3-interpolate": "^3.0.1", "d3-path": "^3.1.0", "d3-scale": "^4.0.2", "d3-shape": "^3.2.0", "d3-time": "^3.1.0", "d3-time-format": "^4.1.0", "d3-timer": "^3.0.1", "flatqueue": "^3.0.0", "internmap": "^2.0.3" } }, "sha512-1/p3RmRPGlzCtvpWwjwBCRoxJW1eDoQdml8MWq71Q1Oq0n36mTkFjzPTbcaJ52jigIIIQrMloqTUkhmEXwwBMg=="],
"@mui/x-internal-gestures": ["@mui/x-internal-gestures@9.0.2", "http://registry.npmjs.org/@mui/x-internal-gestures/-/x-internal-gestures-9.0.2.tgz", { "dependencies": { "@babel/runtime": "^7.28.6" } }, "sha512-xCp99a7cSb7iH1bj4G524ooMOFe92H8m/rONCUiKyj7LvV1YUGzTfHgJysQgDCZJqHYaW7YAGLvwMUyEMZVzqQ=="], "@mui/x-internal-gestures": ["@mui/x-internal-gestures@9.10.0", "", { "dependencies": { "@babel/runtime": "^7.29.7", "@base-ui/utils": "^0.3.1" } }, "sha512-Fgmh+vJlOjLst238/J5SlEOZLdnSe0yWjrYctYVWJr3pKbtmYSbDkjYvWjG97PeDvq5Wa1+W1qagiBoJgkUIng=="],
"@mui/x-internals": ["@mui/x-internals@9.0.0", "http://registry.npmjs.org/@mui/x-internals/-/x-internals-9.0.0.tgz", { "dependencies": { "@babel/runtime": "^7.28.6", "@mui/utils": "9.0.0", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-E/4rdg69JjhyybpPGypCjAKSKLLnSdCFM+O6P/nkUg47+qt3uftxQEhjQO53rcn6ahHl6du/uNZ9BLgeY6kYxQ=="], "@mui/x-internals": ["@mui/x-internals@9.10.1", "", { "dependencies": { "@babel/runtime": "^7.29.7", "@base-ui/utils": "^0.3.1", "@mui/utils": "^9.2.0", "reselect": "^5.2.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-pKFVzFQHB34TuMG3gOVx+LfIR8VBsi/ZhX/erRX2WOXu+ewEYGvdeK1mW9hLe7e2v3uyUbgT8RNeN0cy10cDjg=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "http://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "http://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
@@ -176,7 +181,7 @@
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "http://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "http://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
"@oxc-project/types": ["@oxc-project/types@0.126.0", "", {}, "sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ=="], "@oxc-project/types": ["@oxc-project/types@0.139.0", "", {}, "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw=="],
"@parcel/watcher": ["@parcel/watcher@2.5.6", "http://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.3" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.6", "@parcel/watcher-darwin-arm64": "2.5.6", "@parcel/watcher-darwin-x64": "2.5.6", "@parcel/watcher-freebsd-x64": "2.5.6", "@parcel/watcher-linux-arm-glibc": "2.5.6", "@parcel/watcher-linux-arm-musl": "2.5.6", "@parcel/watcher-linux-arm64-glibc": "2.5.6", "@parcel/watcher-linux-arm64-musl": "2.5.6", "@parcel/watcher-linux-x64-glibc": "2.5.6", "@parcel/watcher-linux-x64-musl": "2.5.6", "@parcel/watcher-win32-arm64": "2.5.6", "@parcel/watcher-win32-ia32": "2.5.6", "@parcel/watcher-win32-x64": "2.5.6" } }, "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ=="], "@parcel/watcher": ["@parcel/watcher@2.5.6", "http://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.3" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.6", "@parcel/watcher-darwin-arm64": "2.5.6", "@parcel/watcher-darwin-x64": "2.5.6", "@parcel/watcher-freebsd-x64": "2.5.6", "@parcel/watcher-linux-arm-glibc": "2.5.6", "@parcel/watcher-linux-arm-musl": "2.5.6", "@parcel/watcher-linux-arm64-glibc": "2.5.6", "@parcel/watcher-linux-arm64-musl": "2.5.6", "@parcel/watcher-linux-x64-glibc": "2.5.6", "@parcel/watcher-linux-x64-musl": "2.5.6", "@parcel/watcher-win32-arm64": "2.5.6", "@parcel/watcher-win32-ia32": "2.5.6", "@parcel/watcher-win32-x64": "2.5.6" } }, "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ=="],
@@ -210,45 +215,45 @@
"@popperjs/core": ["@popperjs/core@2.11.8", "http://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", {}, "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="], "@popperjs/core": ["@popperjs/core@2.11.8", "http://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", {}, "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.16", "", { "os": "android", "cpu": "arm64" }, "sha512-rhY3k7Bsae9qQfOtph2Pm2jZEA+s8Gmjoz4hhmx70K9iMQ/ddeae+xhRQcM5IuVx5ry1+bGfkvMn7D6MJggVSA=="], "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.5", "", { "os": "android", "cpu": "arm64" }, "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rNz0yK078yrNn3DrdgN+PKiMOW8HfQ92jQiXxwX8yW899ayV00MLVdaCNeVBhG/TbH3ouYVObo8/yrkiectkcQ=="], "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-r/OmdR00HmD4i79Z//xO06uEPOq5hRXdhw7nzkxQxwSavs3PSHa1ijntdpOiZ2mzOQ3fVVu8C1M19FoNM+dMUQ=="], "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.16", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KcRE5w8h0OnjUatG8pldyD14/CQ5Phs1oxfR+3pKDjboHRo9+MkqQaiIZlZRpsxC15paeXme/I127tUa9TXJ6g=="], "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.16", "", { "os": "linux", "cpu": "arm" }, "sha512-bT0guA1bpxEJ/ZhTRniQf7rNF8ybvXOuWbNIeLABaV5NGjx4EtOWBTSRGWFU9ZWVkPOZ+HNFP8RMcBokBiZ0Kg=="], "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.5", "", { "os": "linux", "cpu": "arm" }, "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-+tHktCHWV8BDQSjemUqm/Jl/TPk3QObCTIjmdDy/nlupcujZghmKK2962LYrqFpWu+ai01AN/REOH3NEpqvYQg=="], "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-3fPzdREH806oRLxpTWW1Gt4tQHs0TitZFOECB2xzCFLPKnSOy90gwA7P29cksYilFO6XVRY1kzga0cL2nRjKPg=="], "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.16", "", { "os": "linux", "cpu": "ppc64" }, "sha512-EKwI1tSrLs7YVw+JPJT/G2dJQ1jl9qlTTTEG0V2Ok/RdOenRfBw2PQdLPyjhIu58ocdBfP7vIRN/pvMsPxs/AQ=="], "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.16", "", { "os": "linux", "cpu": "s390x" }, "sha512-Uknladnb3Sxqu6SEcqBldQyJUpk8NleooZEc0MbRBJ4inEhRYWZX0NJu12vNf2mqAq7gsofAxHrGghiUYjhaLQ=="], "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.16", "", { "os": "linux", "cpu": "x64" }, "sha512-FIb8+uG49sZBtLTn+zt1AJ20TqVcqWeSIyoVt0or7uAWesgKaHbiBh6OpA/k9v0LTt+PTrb1Lao133kP4uVxkg=="], "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.16", "", { "os": "linux", "cpu": "x64" }, "sha512-RuERhF9/EgWxZEXYWCOaViUWHIboceK4/ivdtQ3R0T44NjLkIIlGIAVAuCddFxsZ7vnRHtNQUrt2vR2n2slB2w=="], "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.16", "", { "os": "none", "cpu": "arm64" }, "sha512-mXcXnvd9GpazCxeUCCnZ2+YF7nut+ZOEbE4GtaiPtyY6AkhZWbK70y1KK3j+RDhjVq5+U8FySkKRb/+w0EeUwA=="], "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.5", "", { "os": "none", "cpu": "arm64" }, "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw=="],
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.16", "", { "dependencies": { "@emnapi/core": "1.9.2", "@emnapi/runtime": "1.9.2", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-3Q2KQxnC8IJOLqXmUMoYwyIPZU9hzRbnHaoV3Euz+VVnjZKcY8ktnNP8T9R4/GGQtb27C/UYKABxesKWb8lsvQ=="], "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.5", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-tj7XRemQcOcFwv7qhpUxMTBbI5mWMlE4c1Omhg5+h8GuLXzyj8HviYgR+bB2DMDgRqUE+jiDleqSCRjx4aYk/Q=="], "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.16", "", { "os": "win32", "cpu": "x64" }, "sha512-PH5DRZT+F4f2PTXRXR8uJxnBq2po/xFtddyabTJVJs/ZYVHqXPEgNIr35IHTEa6bpa0Q8Awg+ymkTaGnKITw4g=="], "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "http://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "http://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
"@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@5.10.0", "http://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.10.0.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/types": "^8.56.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "estraverse": "^5.3.0", "picomatch": "^4.0.3" }, "peerDependencies": { "eslint": "^9.0.0 || ^10.0.0" } }, "sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ=="], "@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@5.10.0", "http://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.10.0.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/types": "^8.56.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "estraverse": "^5.3.0", "picomatch": "^4.0.3" }, "peerDependencies": { "eslint": "^9.0.0 || ^10.0.0" } }, "sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ=="],
"@tanstack/query-core": ["@tanstack/query-core@5.99.2", "http://registry.npmjs.org/@tanstack/query-core/-/query-core-5.99.2.tgz", {}, "sha512-1HunU0bXVsR1ZJMZbcOPE6VtaBJxsW809RE9xPe4Gz7MlB0GWwQvuTPhMoEmQ/hIzFKJ/DWAuttIe7BOaWx0tA=="], "@tanstack/query-core": ["@tanstack/query-core@5.101.4", "", {}, "sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw=="],
"@tanstack/react-query": ["@tanstack/react-query@5.99.2", "http://registry.npmjs.org/@tanstack/react-query/-/react-query-5.99.2.tgz", { "dependencies": { "@tanstack/query-core": "5.99.2" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-vM91UEe45QUS9ED6OklsVL15i8qKcRqNwpWzPTVWvRPRSEgDudDgHpvyTjcdlwHcrKNa80T+xXYcchT2noPnZA=="], "@tanstack/react-query": ["@tanstack/react-query@5.101.4", "", { "dependencies": { "@tanstack/query-core": "5.101.4" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "http://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], "@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
"@types/d3-array": ["@types/d3-array@3.2.2", "http://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], "@types/d3-array": ["@types/d3-array@3.2.2", "http://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
@@ -256,6 +261,8 @@
"@types/d3-format": ["@types/d3-format@3.0.4", "http://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="], "@types/d3-format": ["@types/d3-format@3.0.4", "http://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="],
"@types/d3-geo": ["@types/d3-geo@3.1.0", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="],
"@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "http://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "http://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="],
"@types/d3-path": ["@types/d3-path@3.1.1", "http://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], "@types/d3-path": ["@types/d3-path@3.1.1", "http://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="],
@@ -274,6 +281,8 @@
"@types/estree": ["@types/estree@1.0.8", "http://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/estree": ["@types/estree@1.0.8", "http://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "http://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/json-schema": ["@types/json-schema@7.0.15", "http://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/parse-json": ["@types/parse-json@4.0.2", "http://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="], "@types/parse-json": ["@types/parse-json@4.0.2", "http://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="],
@@ -286,7 +295,7 @@
"@typescript-eslint/types": ["@typescript-eslint/types@8.58.1", "http://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.1.tgz", {}, "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw=="], "@typescript-eslint/types": ["@typescript-eslint/types@8.58.1", "http://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.1.tgz", {}, "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "http://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.4", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-XcCQz0TBpBgljhj0gMuuDj49i6Ytqh5q1osT/Gp5uAVJUCTWxyskk/l1jwYYiu2xcNHHipdMz40EGfM1VdamVg=="],
"acorn": ["acorn@8.16.0", "http://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], "acorn": ["acorn@8.16.0", "http://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
@@ -320,7 +329,7 @@
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.20", "http://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.10.20", "http://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ=="],
"bezier-easing": ["bezier-easing@2.1.0", "http://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz", {}, "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig=="], "bezier-easing": ["bezier-easing@3.0.1", "", {}, "sha512-vvvg10Anq7py0+KkIqlwJpUVdde9Z8kSzeUzAkQNYAJdA3mZnLG9r9ExdwtjcASCYER6UkNHdBNHHMnVimnU7A=="],
"brace-expansion": ["brace-expansion@1.1.11", "http://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], "brace-expansion": ["brace-expansion@1.1.11", "http://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
@@ -338,7 +347,9 @@
"caniuse-lite": ["caniuse-lite@1.0.30001788", "http://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", {}, "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ=="], "caniuse-lite": ["caniuse-lite@1.0.30001788", "http://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", {}, "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ=="],
"chokidar": ["chokidar@4.0.3", "http://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], "cap-widget": ["cap-widget@0.1.56", "", {}, "sha512-mAoFWzouxCjvi0B0Y0s7cRGGaLvJxifMVeNIHiFCkZme/9BOASWliN0aqz9wTUdlrt3Awm0UEA8h8DV1IDUuug=="],
"chokidar": ["chokidar@5.0.0", "http://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
"clsx": ["clsx@2.1.1", "http://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "clsx": ["clsx@2.1.1", "http://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
@@ -358,6 +369,8 @@
"d3-format": ["d3-format@3.1.2", "http://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], "d3-format": ["d3-format@3.1.2", "http://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="],
"d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="],
"d3-interpolate": ["d3-interpolate@3.0.1", "http://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], "d3-interpolate": ["d3-interpolate@3.0.1", "http://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
"d3-path": ["d3-path@3.1.0", "http://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], "d3-path": ["d3-path@3.1.0", "http://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="],
@@ -418,7 +431,7 @@
"escape-string-regexp": ["escape-string-regexp@4.0.0", "http://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "escape-string-regexp": ["escape-string-regexp@4.0.0", "http://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@10.2.1", "http://registry.npmjs.org/eslint/-/eslint-10.2.1.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.5.5", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q=="], "eslint": ["eslint@10.8.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ=="],
"eslint-plugin-react": ["eslint-plugin-react@7.37.5", "http://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="], "eslint-plugin-react": ["eslint-plugin-react@7.37.5", "http://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="],
@@ -486,7 +499,7 @@
"glob-parent": ["glob-parent@6.0.2", "http://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], "glob-parent": ["glob-parent@6.0.2", "http://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"globals": ["globals@17.5.0", "http://registry.npmjs.org/globals/-/globals-17.5.0.tgz", {}, "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g=="], "globals": ["globals@17.8.0", "", {}, "sha512-Zz/LMDZScFmkakeL2cTHzf+PbWKdpU3uclqkZT7TjDG58j5WPt0PpA+n9uPI24fZtlw07q0OtEi84K+umsRzqQ=="],
"globalthis": ["globalthis@1.0.4", "http://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], "globalthis": ["globalthis@1.0.4", "http://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
@@ -584,7 +597,7 @@
"js-tokens": ["js-tokens@4.0.0", "http://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-tokens": ["js-tokens@4.0.0", "http://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"js-yaml": ["js-yaml@4.1.1", "http://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], "js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="],
"jsesc": ["jsesc@3.1.0", "http://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "jsesc": ["jsesc@3.1.0", "http://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
@@ -650,7 +663,7 @@
"ms": ["ms@2.1.3", "http://registry.npmjs.org/ms/-/ms-2.1.3.tgz", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "ms": ["ms@2.1.3", "http://registry.npmjs.org/ms/-/ms-2.1.3.tgz", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.11", "http://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="],
"natural-compare": ["natural-compare@1.4.0", "http://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], "natural-compare": ["natural-compare@1.4.0", "http://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
@@ -700,7 +713,7 @@
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "http://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "http://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
"postcss": ["postcss@8.5.10", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ=="], "postcss": ["postcss@8.5.23", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg=="],
"prelude-ls": ["prelude-ls@1.2.1", "http://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], "prelude-ls": ["prelude-ls@1.2.1", "http://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
@@ -710,21 +723,21 @@
"queue-microtask": ["queue-microtask@1.2.3", "http://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], "queue-microtask": ["queue-microtask@1.2.3", "http://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"react": ["react@19.2.5", "http://registry.npmjs.org/react/-/react-19.2.5.tgz", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="], "react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
"react-dom": ["react-dom@19.2.5", "http://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.5" } }, "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag=="], "react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="],
"react-is": ["react-is@19.2.5", "http://registry.npmjs.org/react-is/-/react-is-19.2.5.tgz", {}, "sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ=="], "react-is": ["react-is@19.2.8", "", {}, "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ=="],
"react-transition-group": ["react-transition-group@4.4.5", "http://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="], "react-transition-group": ["react-transition-group@4.4.5", "http://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="],
"readdirp": ["readdirp@4.1.2", "http://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], "readdirp": ["readdirp@5.0.0", "http://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "http://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "http://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "http://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "http://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
"reselect": ["reselect@5.1.1", "http://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], "reselect": ["reselect@5.2.0", "", {}, "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw=="],
"resolve": ["resolve@2.0.0-next.6", "http://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="], "resolve": ["resolve@2.0.0-next.6", "http://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="],
@@ -732,7 +745,7 @@
"reusify": ["reusify@1.1.0", "http://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], "reusify": ["reusify@1.1.0", "http://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"rolldown": ["rolldown@1.0.0-rc.16", "", { "dependencies": { "@oxc-project/types": "=0.126.0", "@rolldown/pluginutils": "1.0.0-rc.16" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.16", "@rolldown/binding-darwin-arm64": "1.0.0-rc.16", "@rolldown/binding-darwin-x64": "1.0.0-rc.16", "@rolldown/binding-freebsd-x64": "1.0.0-rc.16", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.16", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.16", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.16", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.16", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.16", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.16", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.16", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.16", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.16", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.16", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.16" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g=="], "rolldown": ["rolldown@1.1.5", "", { "dependencies": { "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.5", "@rolldown/binding-darwin-arm64": "1.1.5", "@rolldown/binding-darwin-x64": "1.1.5", "@rolldown/binding-freebsd-x64": "1.1.5", "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", "@rolldown/binding-linux-arm64-gnu": "1.1.5", "@rolldown/binding-linux-arm64-musl": "1.1.5", "@rolldown/binding-linux-ppc64-gnu": "1.1.5", "@rolldown/binding-linux-s390x-gnu": "1.1.5", "@rolldown/binding-linux-x64-gnu": "1.1.5", "@rolldown/binding-linux-x64-musl": "1.1.5", "@rolldown/binding-openharmony-arm64": "1.1.5", "@rolldown/binding-wasm32-wasi": "1.1.5", "@rolldown/binding-win32-arm64-msvc": "1.1.5", "@rolldown/binding-win32-x64-msvc": "1.1.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA=="],
"run-parallel": ["run-parallel@1.2.0", "http://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], "run-parallel": ["run-parallel@1.2.0", "http://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
@@ -742,7 +755,7 @@
"safe-regex-test": ["safe-regex-test@1.1.0", "http://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], "safe-regex-test": ["safe-regex-test@1.1.0", "http://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
"sass": ["sass@1.99.0", "http://registry.npmjs.org/sass/-/sass-1.99.0.tgz", { "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.1.5", "source-map-js": ">=0.6.2 <2.0.0" }, "optionalDependencies": { "@parcel/watcher": "^2.4.1" }, "bin": "sass.js" }, "sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q=="], "sass": ["sass@1.102.0", "", { "dependencies": { "chokidar": "^5.0.0", "immutable": "^5.1.5", "source-map-js": ">=0.6.2 <2.0.0" }, "optionalDependencies": { "@parcel/watcher": "^2.4.1" }, "bin": { "sass": "sass.js" } }, "sha512-NSOyTnaQF7rTAEOtI2fwb386vL+akyiQLBZu8Na7hXCb+umJy0GAqlcMIaqACZ6Z1VgTBS4K9PG6B3IdjHGJsw=="],
"scheduler": ["scheduler@0.27.0", "http://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "scheduler": ["scheduler@0.27.0", "http://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
@@ -790,7 +803,7 @@
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "http://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "http://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
"tinyglobby": ["tinyglobby@0.2.16", "http://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], "tinyglobby": ["tinyglobby@0.2.17", "http://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
"to-regex-range": ["to-regex-range@5.0.1", "http://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "to-regex-range": ["to-regex-range@5.0.1", "http://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
@@ -816,7 +829,7 @@
"use-sync-external-store": ["use-sync-external-store@1.6.0", "http://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], "use-sync-external-store": ["use-sync-external-store@1.6.0", "http://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
"vite": ["vite@8.0.9", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.10", "rolldown": "1.0.0-rc.16", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw=="], "vite": ["vite@8.1.5", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.17", "rolldown": "~1.1.5", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw=="],
"vite-plugin-symfony": ["vite-plugin-symfony@8.2.4", "http://registry.npmjs.org/vite-plugin-symfony/-/vite-plugin-symfony-8.2.4.tgz", { "dependencies": { "debug": "^4.4.1", "fast-glob": "^3.3.3", "picocolors": "^1.1.1", "sirv": "^3.0.1" }, "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-ph98EMPx8FhA6QIp43ZiK0zjODV9jumB7EHXfZEhRme2lo9oBa9sAXCNCCIvdVk/m9EWkNZpcRBjequjXZiSuA=="], "vite-plugin-symfony": ["vite-plugin-symfony@8.2.4", "http://registry.npmjs.org/vite-plugin-symfony/-/vite-plugin-symfony-8.2.4.tgz", { "dependencies": { "debug": "^4.4.1", "fast-glob": "^3.3.3", "picocolors": "^1.1.1", "sirv": "^3.0.1" }, "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-ph98EMPx8FhA6QIp43ZiK0zjODV9jumB7EHXfZEhRme2lo9oBa9sAXCNCCIvdVk/m9EWkNZpcRBjequjXZiSuA=="],
@@ -842,6 +855,8 @@
"zod-validation-error": ["zod-validation-error@4.0.2", "http://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], "zod-validation-error": ["zod-validation-error@4.0.2", "http://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
"@base-ui/utils/@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="],
"@emotion/babel-plugin/convert-source-map": ["convert-source-map@1.9.0", "http://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], "@emotion/babel-plugin/convert-source-map": ["convert-source-map@1.9.0", "http://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "http://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "http://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
@@ -850,6 +865,26 @@
"@eslint/eslintrc/globals": ["globals@14.0.0", "http://registry.npmjs.org/globals/-/globals-14.0.0.tgz", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], "@eslint/eslintrc/globals": ["globals@14.0.0", "http://registry.npmjs.org/globals/-/globals-14.0.0.tgz", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
"@mui/material/@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="],
"@mui/private-theming/@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="],
"@mui/styled-engine/@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="],
"@mui/system/@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="],
"@mui/types/@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="],
"@mui/utils/@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="],
"@mui/x-charts/@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="],
"@mui/x-charts-vendor/@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="],
"@mui/x-internal-gestures/@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="],
"@mui/x-internals/@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="],
"babel-plugin-macros/resolve": ["resolve@1.22.12", "http://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], "babel-plugin-macros/resolve": ["resolve@1.22.12", "http://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="],
"eslint/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "http://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], "eslint/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "http://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
@@ -866,7 +901,7 @@
"prop-types/react-is": ["react-is@16.13.1", "http://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], "prop-types/react-is": ["react-is@16.13.1", "http://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.16", "", {}, "sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA=="], "vite/picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
"@eslint/config-array/minimatch/brace-expansion": ["brace-expansion@5.0.5", "http://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], "@eslint/config-array/minimatch/brace-expansion": ["brace-expansion@5.0.5", "http://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
+45 -6
View File
@@ -10,6 +10,7 @@ services:
environment: environment:
SERVER_NAME: ${SERVER_NAME:-:80} SERVER_NAME: ${SERVER_NAME:-:80}
APP_ENV: prod APP_ENV: prod
APP_VERSION: ${APP_VERSION:-1.0.0}
APP_SECRET: ${APP_SECRET} APP_SECRET: ${APP_SECRET}
APP_PUBLIC_HOSTNAME: ${APP_PUBLIC_HOSTNAME:-localhost} APP_PUBLIC_HOSTNAME: ${APP_PUBLIC_HOSTNAME:-localhost}
APP_CONTACT_MAIL_ADDRESS: ${APP_CONTACT_MAIL_ADDRESS:-7system7@gmail.com} APP_CONTACT_MAIL_ADDRESS: ${APP_CONTACT_MAIL_ADDRESS:-7system7@gmail.com}
@@ -25,8 +26,8 @@ services:
MERCURE_JWT_TOKEN: ${MERCURE_JWT_TOKEN} MERCURE_JWT_TOKEN: ${MERCURE_JWT_TOKEN}
MERCURE_SUBSCRIBER_JWT: ${MERCURE_SUBSCRIBER_JWT} MERCURE_SUBSCRIBER_JWT: ${MERCURE_SUBSCRIBER_JWT}
MAILER_DSN: smtp://mail:25?verify_peer=0 MAILER_DSN: smtp://mail:25?verify_peer=0
RECAPTCHA_SITE_KEY: ${RECAPTCHA_SITE_KEY} CAP_API_ENDPOINT: ${CAP_API_ENDPOINT}
RECAPTCHA_SECRET_KEY: ${RECAPTCHA_SECRET_KEY} CAP_SECRET_KEY: ${CAP_SECRET_KEY}
WEBAUTHN_RP_ID: ${WEBAUTHN_RP_ID:-localhost} WEBAUTHN_RP_ID: ${WEBAUTHN_RP_ID:-localhost}
WEBAUTHN_ORIGIN: ${WEBAUTHN_ORIGIN:-https://localhost} WEBAUTHN_ORIGIN: ${WEBAUTHN_ORIGIN:-https://localhost}
MINIO_ROOT_USER: ${MINIO_ROOT_USER} MINIO_ROOT_USER: ${MINIO_ROOT_USER}
@@ -47,6 +48,45 @@ services:
condition: service_healthy condition: service_healthy
mail: mail:
condition: service_started condition: service_started
scheduler:
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
container_name: '${APP_NAME}-scheduler'
command: ['php', 'bin/console', 'messenger:consume', 'scheduler_default', '--env=prod', '--no-interaction', '--memory-limit=128M']
environment:
APP_ENV: prod
APP_SECRET: ${APP_SECRET}
APP_PUBLIC_HOSTNAME: ${APP_PUBLIC_HOSTNAME:-localhost}
DATABASE_URL: >-
postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}?serverVersion=${POSTGRES_VERSION}&charset=utf8
POSTGRES_URL: db
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
MERCURE_URL: http://localhost/.well-known/mercure
MERCURE_PUBLIC_URL: https://${APP_PUBLIC_HOSTNAME:-localhost}/.well-known/mercure
MERCURE_JWT_SECRET: ${MERCURE_JWT_SECRET}
MERCURE_JWT_TOKEN: ${MERCURE_JWT_TOKEN}
MERCURE_SUBSCRIBER_JWT: ${MERCURE_SUBSCRIBER_JWT}
MAILER_DSN: smtp://mail:25?verify_peer=0
CAP_API_ENDPOINT: ${CAP_API_ENDPOINT}
CAP_SECRET_KEY: ${CAP_SECRET_KEY}
WEBAUTHN_RP_ID: ${WEBAUTHN_RP_ID:-localhost}
WEBAUTHN_ORIGIN: ${WEBAUTHN_ORIGIN:-https://localhost}
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
MINIO_ENDPOINT: http://minio:9000
MINIO_PUBLIC_URL: ${MINIO_PUBLIC_URL:-http://localhost:9000}
TRUSTED_PROXIES: ${TRUSTED_PROXIES:-127.0.0.1}
volumes:
- app_var:/app/var
depends_on:
db:
condition: service_healthy
mail:
condition: service_started
minio: minio:
image: minio/minio:RELEASE.2025-09-07T16-13-09Z-cpuv1 image: minio/minio:RELEASE.2025-09-07T16-13-09Z-cpuv1
restart: unless-stopped restart: unless-stopped
@@ -68,6 +108,7 @@ services:
start_period: 10s start_period: 10s
minio_init: minio_init:
image: minio/minio:RELEASE.2025-09-07T16-13-09Z-cpuv1 image: minio/minio:RELEASE.2025-09-07T16-13-09Z-cpuv1
profiles: ['bootstrap']
restart: "no" restart: "no"
container_name: '${APP_NAME}-minio-init' container_name: '${APP_NAME}-minio-init'
depends_on: depends_on:
@@ -105,7 +146,7 @@ services:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB} POSTGRES_DB: ${POSTGRES_DB}
volumes: volumes:
- postgres_data:/var/lib/postgresql/data - postgres_data:/var/lib/postgresql
healthcheck: healthcheck:
test: [ "CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}" ] test: [ "CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}" ]
interval: 5s interval: 5s
@@ -113,7 +154,7 @@ services:
retries: 10 retries: 10
start_period: 10s start_period: 10s
ports: ports:
- "15432:5432" - "5452:5432"
volumes: volumes:
app_var: app_var:
postgres_data: postgres_data:
@@ -121,5 +162,3 @@ volumes:
caddy_config: caddy_config:
postfix_spool: postfix_spool:
minio_data: minio_data:
+19 -17
View File
@@ -1,6 +1,6 @@
{ {
"name": "splendid-bear/mineseeker", "name": "splendid-bear/mineseeker",
"version": "2026.2.1", "version": "2026.3.0",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"author": "https://www.splendidbear.org", "author": "https://www.splendidbear.org",
"bugs": "https://source.splendidbear.org", "bugs": "https://source.splendidbear.org",
@@ -27,32 +27,34 @@
"scheb/2fa-backup-code": "^8.5", "scheb/2fa-backup-code": "^8.5",
"scheb/2fa-bundle": "^8.5", "scheb/2fa-bundle": "^8.5",
"scheb/2fa-totp": "^8.5", "scheb/2fa-totp": "^8.5",
"symfony/console": "7.4.*", "symfony/console": "8.1.*",
"symfony/flex": "^2.10.0", "symfony/flex": "^2.10.0",
"symfony/form": "7.4.*", "symfony/form": "8.1.*",
"symfony/framework-bundle": "7.4.*", "symfony/framework-bundle": "8.1.*",
"symfony/http-client": "7.4.*", "symfony/http-client": "8.1.*",
"symfony/mailer": "7.4.*", "symfony/mailer": "8.1.*",
"symfony/mercure": "^0.7", "symfony/mercure": "^0.7",
"symfony/mercure-bundle": "^0.4", "symfony/mercure-bundle": "^0.4",
"symfony/messenger": "8.1.*",
"symfony/monolog-bundle": "^4.0", "symfony/monolog-bundle": "^4.0",
"symfony/security-bundle": "7.4.*", "symfony/scheduler": "8.1.*",
"symfony/translation": "7.4.*", "symfony/security-bundle": "8.1.*",
"symfony/twig-bundle": "7.4.*", "symfony/translation": "8.1.*",
"symfony/validator": "7.4.*", "symfony/twig-bundle": "8.1.*",
"symfony/yaml": "7.4.*", "symfony/validator": "8.1.*",
"symfony/yaml": "8.1.*",
"web-auth/webauthn-framework": "^5.2" "web-auth/webauthn-framework": "^5.2"
}, },
"require-dev": { "require-dev": {
"dama/doctrine-test-bundle": "^8.6", "dama/doctrine-test-bundle": "^8.6",
"phpunit/phpunit": "^13.1", "phpunit/phpunit": "^13.1",
"roave/security-advisories": "dev-master", "roave/security-advisories": "dev-master",
"symfony/browser-kit": "7.4.*", "symfony/browser-kit": "8.1.*",
"symfony/css-selector": "7.4.*", "symfony/css-selector": "8.1.*",
"symfony/dotenv": "7.4.*", "symfony/dotenv": "8.1.*",
"symfony/maker-bundle": "^1.5", "symfony/maker-bundle": "^1.5",
"symfony/stopwatch": "7.4.*", "symfony/stopwatch": "8.1.*",
"symfony/web-profiler-bundle": "7.4.*", "symfony/web-profiler-bundle": "8.1.*",
"zenstruck/foundry": "^2.9" "zenstruck/foundry": "^2.9"
}, },
"config": { "config": {
@@ -102,7 +104,7 @@
"extra": { "extra": {
"symfony": { "symfony": {
"allow-contrib": false, "allow-contrib": false,
"require": "7.4.*" "require": "8.1.*"
} }
} }
} }
Generated
+1881 -1648
View File
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
framework:
messenger:
# Uncomment this (and the failed transport below) to send failed messages to this transport for later handling.
# failure_transport: failed
transports:
# https://symfony.com/doc/current/messenger.html#transport-configuration
# async: '%env(MESSENGER_TRANSPORT_DSN)%'
# failed: 'doctrine://default?queue_name=failed'
sync: 'sync://'
routing:
# Route your messages to the transports
# 'App\Message\YourMessage': async
# when@test:
# framework:
# messenger:
# transports:
# # replace with your transport name here (e.g., my_transport: 'in-memory://')
# # For more Messenger testing tools, see https://github.com/zenstruck/messenger-test
# async: 'in-memory://'
+1 -1
View File
@@ -4,4 +4,4 @@ twig:
strict_variables: '%kernel.debug%' strict_variables: '%kernel.debug%'
globals: globals:
version: "%jotunheimr.version%" version: "%jotunheimr.version%"
recaptcha_site_key: "%env(RECAPTCHA_SITE_KEY)%" cap_api_endpoint: "%env(CAP_API_ENDPOINT)%"
+1
View File
@@ -1,6 +1,7 @@
when@dev: &dev when@dev: &dev
# See full configuration: https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#full-default-bundle-configuration # See full configuration: https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#full-default-bundle-configuration
zenstruck_foundry: zenstruck_foundry:
enable_auto_refresh_with_lazy_objects: true
persistence: persistence:
# Flush only once per call of `PersistentObjectFactory::create()` # Flush only once per call of `PersistentObjectFactory::create()`
flush_once: true flush_once: true
+2 -2
View File
@@ -1,3 +1,3 @@
_errors: _errors:
resource: '@FrameworkBundle/Resources/config/routing/errors.xml' resource: '@FrameworkBundle/Resources/config/routing/errors.php'
prefix: /_error prefix: /_error
+2 -2
View File
@@ -1,7 +1,7 @@
web_profiler_wdt: web_profiler_wdt:
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml' resource: '@WebProfilerBundle/Resources/config/routing/wdt.php'
prefix: /_wdt prefix: /_wdt
web_profiler_profiler: web_profiler_profiler:
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml' resource: '@WebProfilerBundle/Resources/config/routing/profiler.php'
prefix: /_profiler prefix: /_profiler
+1 -1
View File
@@ -2,7 +2,7 @@
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration # https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters: parameters:
locale: 'en' locale: 'en'
jotunheimr.version: 1.1.0-20191026 jotunheimr.version: '%env(APP_VERSION)%'
services: services:
# default configuration for services in *this* file # default configuration for services in *this* file
+5
View File
@@ -8,6 +8,11 @@ until php -r "new PDO('pgsql:host=db;port=5432;dbname=${POSTGRES_DB}', '${POSTGR
done done
echo "[entrypoint] Database is ready." echo "[entrypoint] Database is ready."
if [ "$#" -gt 0 ]; then
echo "[entrypoint] Starting worker command: $*"
exec "$@"
fi
echo "[entrypoint] Clearing and warming Symfony cache..." echo "[entrypoint] Clearing and warming Symfony cache..."
php bin/console cache:clear php bin/console cache:clear
+6 -2
View File
@@ -1,8 +1,12 @@
#!/bin/sh #!/bin/sh
set -e set -e
echo "[minio-init] Configuring alias..." echo "[minio-init] Waiting for MinIO..."
mc alias set local http://minio:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" until mc alias set local http://minio:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" > /dev/null 2>&1; do
sleep 2
done
echo "[minio-init] Configuring bucket..."
if mc ls local/mineseeker > /dev/null 2>&1; then if mc ls local/mineseeker > /dev/null 2>&1; then
echo "[minio-init] Already initialized, skipping." echo "[minio-init] Already initialized, skipping."
+19
View File
@@ -0,0 +1,19 @@
# MinIO Initialization
The `minio_init` Compose service creates the `mineseeker` bucket and applies
its public read policy. It is an idempotent bootstrap task: after the bucket
exists, it exits without changing it.
It belongs to the `bootstrap` Compose profile, so normal `docker compose up`
does not create a stopped `mineseeker-minio-init` container.
`make start`, `make start-build`, and the production deployment workflow run
the bootstrap task automatically with `docker compose run --rm minio_init`.
They also remove any stopped legacy initializer container first. The temporary
container is removed as soon as the task succeeds.
To run it manually when needed:
```bash
docker compose run --rm minio_init
```
+6
View File
@@ -57,6 +57,12 @@ Quick command reference for testing:
## CI/CD & Deployment ## CI/CD & Deployment
### [Scheduled Tasks](./SCHEDULER.md)
Symfony Scheduler tasks and the Docker worker that runs them.
### [MinIO Initialization](./MINIO_INITIALIZATION.md)
Automatic, one-shot setup of the MinIO bucket and its access policy.
### [CI/CD Integration Guide](./CI_CD.md) ### [CI/CD Integration Guide](./CI_CD.md)
Comprehensive guide for continuous integration and deployment: Comprehensive guide for continuous integration and deployment:
- Gitea Actions workflows (CI and CD pipelines) - Gitea Actions workflows (CI and CD pipelines)
+31
View File
@@ -0,0 +1,31 @@
# Scheduled Tasks
MineSeeker uses Symfony Scheduler for recurring maintenance work. The Docker
Compose `scheduler` service runs the Scheduler worker continuously and is
restarted automatically if it exits.
## Pending-account cleanup
`PurgeExpiredPendingUsersCommand` is scheduled every hour. It deletes only
unverified users whose activation-token expiry has passed.
To inspect the registered schedule locally:
```bash
php bin/console debug:scheduler
```
To run the cleanup manually without deleting data:
```bash
php bin/console app:users:purge-expired-pending --dry-run
```
The production worker is started by Docker Compose:
```bash
docker compose up -d scheduler
docker compose logs -f scheduler
```
No host-level cron entry is required.
+21 -2
View File
@@ -2,6 +2,25 @@
MineSeeker-specific testing setup and workflows. For general PHPUnit/Symfony testing, see [Symfony Testing Docs](https://symfony.com/doc/current/testing.html). MineSeeker-specific testing setup and workflows. For general PHPUnit/Symfony testing, see [Symfony Testing Docs](https://symfony.com/doc/current/testing.html).
## Example of the current tests
```shell
$ bin/phpunit (master->origin/master|✚1…2⚑1)
PHPUnit 13.1.7 by Sebastian Bergmann and contributors.
Runtime: PHP 8.5.5
Configuration: /var/www/splendid/Mine/phpunit.dist.xml
................................................................. 65 / 71 ( 91%)
...... 71 / 71 (100%)
Time: 00:07.319, Memory: 86.50 MB
OK (71 tests, 227 assertions)
Faker seed used: 918823
```
## Quick Start ## Quick Start
```bash ```bash
@@ -143,9 +162,9 @@ class MyControllerTest extends WebTestCase
{ {
$user = UserFactory::createOne(); $user = UserFactory::createOne();
$client = static::createClient(); $client = static::createClient();
$client->request('GET', '/profile'); $client->request('GET', '/profile');
self::assertResponseRedirects('/login'); self::assertResponseRedirects('/login');
} }
} }
+17 -16
View File
@@ -1,6 +1,6 @@
{ {
"name": "mine-seeker", "name": "mine-seeker",
"version": "2026.2.1", "version": "2026.3.0",
"author": "https://www.splendidbear.org", "author": "https://www.splendidbear.org",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"bugs": "https://source.splendidbear.org", "bugs": "https://source.splendidbear.org",
@@ -16,30 +16,31 @@
"dependencies": { "dependencies": {
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1", "@emotion/styled": "^11.14.1",
"@fontsource/changa-one": "^5.2.8", "@fontsource/changa-one": "^5.3.0",
"@fontsource/open-sans": "^5.2.7", "@fontsource/open-sans": "^5.3.0",
"@fontsource/rajdhani": "^5.2.7", "@fontsource/rajdhani": "^5.3.0",
"@fortawesome/fontawesome-free": "^7.2.0", "@fortawesome/fontawesome-free": "^7.3.1",
"@mui/material": "^9.0.0", "@mui/material": "^9.2.0",
"@mui/x-charts": "^9.0.2", "@mui/x-charts": "^9.10.1",
"@tanstack/react-query": "^5.99.2", "@tanstack/react-query": "^5.101.4",
"cap-widget": "^0.1.56",
"howler": "^2.2.4", "howler": "^2.2.4",
"lodash": "^4.18.1", "lodash": "^4.18.1",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react": "^19.2.5", "react": "^19.2.8",
"react-dom": "^19.2.5" "react-dom": "^19.2.8"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.3.5", "@eslint/eslintrc": "^3.3.6",
"@eslint/js": "10.0.1", "@eslint/js": "10.0.1",
"@stylistic/eslint-plugin": "5.10.0", "@stylistic/eslint-plugin": "5.10.0",
"@vitejs/plugin-react": "^6.0.1", "@vitejs/plugin-react": "^6.0.4",
"eslint": "10.2.1", "eslint": "^10.8.0",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "7.1.1", "eslint-plugin-react-hooks": "7.1.1",
"globals": "17.5.0", "globals": "^17.8.0",
"sass": "^1.99.0", "sass": "^1.102.0",
"vite": "^8.0.9", "vite": "^8.1.5",
"vite-plugin-symfony": "^8.2.4" "vite-plugin-symfony": "^8.2.4"
}, },
"scripts": { "scripts": {
-1
View File
@@ -16,7 +16,6 @@
<server name="APP_ENV" value="test" force="true" /> <server name="APP_ENV" value="test" force="true" />
<server name="SHELL_VERBOSITY" value="-1" /> <server name="SHELL_VERBOSITY" value="-1" />
<env name="SYMFONY_DEPRECATIONS_HELPER" value="disabled" /> <env name="SYMFONY_DEPRECATIONS_HELPER" value="disabled" />
<env name="DATABASE_URL" value="postgresql://system7:bazmeg@127.0.0.1:15432/mineseeker_test?serverVersion=16&amp;charset=utf8" />
<env name="DAMA_DISABLE_STATIC_CONNECTION" value="0" /> <env name="DAMA_DISABLE_STATIC_CONNECTION" value="0" />
</php> </php>
@@ -0,0 +1,78 @@
<?php declare(strict_types=1);
/*
* This file is part of the SplendidBear Websites' projects.
*
* Copyright (c) 2026 @ www.splendidbear.org
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Command;
use App\Repository\UserRepository;
use DateTime;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Scheduler\Attribute\AsPeriodicTask;
/**
* Class PurgeExpiredPendingUsersCommand
*
* @package App\Command
* @author Lang <https://www.splendidbear.org>
* @category Class
* @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
* @link www.splendidbear.org
* @since 2026. 07. 27.
*/
#[AsCommand(
name: 'app:users:purge-expired-pending',
description: 'Delete unverified accounts whose activation token has expired.',
)]
#[AsPeriodicTask(frequency: '1 hour')]
final class PurgeExpiredPendingUsersCommand extends Command
{
public function __construct(private readonly UserRepository $userRepository)
{
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$now = new DateTime();
$count = $this->userRepository->countExpiredPendingUsers($now);
if ($input->getOption('dry-run')) {
$io->note(sprintf('%d expired pending account(s) would be deleted.', $count));
return Command::SUCCESS;
}
if ($count === 0) {
$io->success('No expired pending accounts found.');
return Command::SUCCESS;
}
$deleted = $this->userRepository->deleteExpiredPendingUsers($now);
$io->success(sprintf('Deleted %d expired pending account(s).', $deleted));
return Command::SUCCESS;
}
protected function configure(): void
{
$this->addOption(
'dry-run',
null,
InputOption::VALUE_NONE,
'Report how many accounts would be deleted without deleting them.',
);
}
}
+106
View File
@@ -0,0 +1,106 @@
<?php declare(strict_types=1);
/*
* This file is part of the SplendidBear Websites' projects.
*
* Copyright (c) 2026 @ www.splendidbear.org
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Controller;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;
/**
* Class ApiAuthController
*
* Provides a JSON login endpoint for native desktop clients.
* This endpoint is intentionally exempt from the CAPTCHA listener
* because desktop clients cannot display or solve the Cap widget.
*
* After a successful password login, if the user has TOTP enabled the response
* returns { requiresTwoFactor: true }. The client must then POST the 6-digit
* code to the standard /2fa_check endpoint (which is already exempt from
* the CAPTCHA listener via LoginCaptchaListener).
*
* @package App\Controller
* @author Lang <https://www.splendidbear.org>
* @category Class
* @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
* @link www.splendidbear.org
* @since 2026. 04. 26.
*/
#[AsController]
class ApiAuthController extends AbstractController
{
public function __construct(
private readonly EntityManagerInterface $em,
private readonly UserPasswordHasherInterface $passwordHasher,
private readonly Security $security,
) {
}
/**
* POST /api/auth/login
*
* Request body (JSON): { "username": "...", "password": "..." }
*
* Responses:
* 200 { "success": true, "requiresTwoFactor": false }
* 200 { "success": true, "requiresTwoFactor": true }
* 400 { "success": false, "error": "..." }
* 401 { "success": false, "error": "..." }
*/
#[Route('/api/auth/login', name: 'MineSeekerBundle_api_auth_login', methods: ['POST'])]
public function login(Request $request): JsonResponse
{
$data = $request->toArray();
$username = trim($data['username'] ?? '');
$password = $data['password'] ?? '';
if ($username === '' || $password === '') {
return $this->json(
['success' => false, 'error' => 'Username and password are required.'],
Response::HTTP_BAD_REQUEST
);
}
/** @var User|null $user */
$user = $this->em->getRepository(User::class)->findOneBy(['username' => $username]);
if ($user === null || !$this->passwordHasher->isPasswordValid($user, $password)) {
return $this->json(
['success' => false, 'error' => 'Invalid username or password.'],
Response::HTTP_UNAUTHORIZED
);
}
if (!$user->isVerified) {
return $this->json(
['success' => false, 'error' => 'Account not yet activated. Check your email.'],
Response::HTTP_UNAUTHORIZED
);
}
// Log the user in via the Symfony security system.
// If TOTP is enabled, scheb/2fa will place the session into
// IS_AUTHENTICATED_2FA_IN_PROGRESS state, and the client must
// complete 2FA by POSTing the code to /2fa_check.
$this->security->login($user, 'form_login');
return $this->json([
'success' => true,
'requiresTwoFactor' => $user->isTotpAuthenticationEnabled(),
]);
}
}
+1
View File
@@ -60,6 +60,7 @@ class GameController extends AbstractController
{ {
return $this->render('Game/play.html.twig', [ return $this->render('Game/play.html.twig', [
'env' => $this->env, 'env' => $this->env,
'game_assoc' => $gameAssoc,
'mercure_hub_url' => $this->mercurePublicUrl, 'mercure_hub_url' => $this->mercurePublicUrl,
'mercure_subscriber_jwt' => $this->mercureJwtService->mintSubscriberToken( 'mercure_subscriber_jwt' => $this->mercureJwtService->mintSubscriberToken(
$gameAssoc ?? '', $this->opponentNameService->resolveUserName(), $gameAssoc ?? '', $this->opponentNameService->resolveUserName(),
+6 -3
View File
@@ -27,6 +27,7 @@ use Doctrine\ORM\EntityManagerInterface;
use League\Flysystem\FilesystemException; use League\Flysystem\FilesystemException;
use League\Flysystem\FilesystemOperator; use League\Flysystem\FilesystemOperator;
use Liip\ImagineBundle\Imagine\Cache\CacheManager; use Liip\ImagineBundle\Imagine\Cache\CacheManager;
use Liip\ImagineBundle\Service\FilterService;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use RuntimeException; use RuntimeException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -140,6 +141,7 @@ class ProfileController extends AbstractController
Request $request, Request $request,
EntityManagerInterface $em, EntityManagerInterface $em,
CacheManager $cacheManager, CacheManager $cacheManager,
FilterService $filterService,
#[Autowire(service: 'mineseeker.media.storage')] FilesystemOperator $mediaStorage, #[Autowire(service: 'mineseeker.media.storage')] FilesystemOperator $mediaStorage,
): JsonResponse { ): JsonResponse {
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED'); $this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
@@ -153,8 +155,8 @@ class ProfileController extends AbstractController
return $this->json(['error' => 'No file uploaded.'], 400); return $this->json(['error' => 'No file uploaded.'], 400);
} }
if ($file->getSize() > 2 * 1024 * 1024) { if ($file->getSize() > 10 * 1024 * 1024) {
return $this->json(['error' => 'File is too large. Maximum 2 MB.'], 400); return $this->json(['error' => 'File is too large. Maximum 10 MB.'], 400);
} }
$allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; $allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
@@ -182,6 +184,7 @@ class ProfileController extends AbstractController
$mediaStorage->writeStream($newPath, $stream); $mediaStorage->writeStream($newPath, $stream);
} catch (FilesystemException $e) { } catch (FilesystemException $e) {
$this->logger->error('Unable to write new avatar: ' . $e->getMessage()); $this->logger->error('Unable to write new avatar: ' . $e->getMessage());
fclose($stream);
throw new RuntimeException('Unable to write new avatar: ' . $e->getMessage()); throw new RuntimeException('Unable to write new avatar: ' . $e->getMessage());
} }
fclose($stream); fclose($stream);
@@ -190,7 +193,7 @@ class ProfileController extends AbstractController
$em->flush(); $em->flush();
return $this->json([ return $this->json([
'thumbUrl' => $cacheManager->generateUrl($newPath, 'avatar_thumb'), 'thumbUrl' => $filterService->getUrlOfFilteredImage($newPath, 'avatar_thumb'),
]); ]);
} }
+6 -5
View File
@@ -18,7 +18,6 @@ use App\Repository\UserRepository;
use App\Service\Email\SendActivationEmailService; use App\Service\Email\SendActivationEmailService;
use App\Service\Email\SendPasswordResetEmailService; use App\Service\Email\SendPasswordResetEmailService;
use App\Service\Email\SendUserActivationNotificationService; use App\Service\Email\SendUserActivationNotificationService;
use App\Service\Email\SendUserRegistrationNotificationService;
use DateTime; use DateTime;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use LogicException; use LogicException;
@@ -44,6 +43,8 @@ use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
#[AsController] #[AsController]
class SecurityController extends AbstractController class SecurityController extends AbstractController
{ {
private const string ACTIVATION_TOKEN_TTL = '+24 hours';
public function __construct( public function __construct(
private readonly EntityManagerInterface $em, private readonly EntityManagerInterface $em,
private readonly RequestStack $requestStack, private readonly RequestStack $requestStack,
@@ -53,7 +54,6 @@ class SecurityController extends AbstractController
private readonly SendActivationEmailService $activationEmail, private readonly SendActivationEmailService $activationEmail,
private readonly SendPasswordResetEmailService $passwordResetEmail, private readonly SendPasswordResetEmailService $passwordResetEmail,
private readonly SendUserActivationNotificationService $activationNotificationEmail, private readonly SendUserActivationNotificationService $activationNotificationEmail,
private readonly SendUserRegistrationNotificationService $registrationNotificationEmail,
) { ) {
} }
@@ -92,6 +92,7 @@ class SecurityController extends AbstractController
$user->isVerified = false; $user->isVerified = false;
$user->verificationToken = $token; $user->verificationToken = $token;
$user->verificationTokenExpiresAt = new DateTime(self::ACTIVATION_TOKEN_TTL);
$user->password = $this->passwordHasher->hashPassword($user, $form->get('plainPassword')->getData()); $user->password = $this->passwordHasher->hashPassword($user, $form->get('plainPassword')->getData());
$this->em->persist($user); $this->em->persist($user);
@@ -109,7 +110,6 @@ class SecurityController extends AbstractController
} }
$this->activationEmail->send($user, $activationUrl); $this->activationEmail->send($user, $activationUrl);
$this->registrationNotificationEmail->send($user, new DateTime());
$this->addFlash('verify_email', $user->email); $this->addFlash('verify_email', $user->email);
@@ -194,13 +194,14 @@ class SecurityController extends AbstractController
{ {
$user = $this->em->getRepository(User::class)->findOneBy(['verificationToken' => $token]); $user = $this->em->getRepository(User::class)->findOneBy(['verificationToken' => $token]);
if (!$user) { if (!$user || $user->verificationTokenExpiresAt === null || $user->verificationTokenExpiresAt <= new DateTime()) {
$this->addFlash('error', 'This activation link is invalid or has already been used.'); $this->addFlash('error', 'This activation link is invalid, expired, or has already been used.');
return $this->redirectToRoute('MineSeekerBundle_login'); return $this->redirectToRoute('MineSeekerBundle_login');
} }
$user->isVerified = true; $user->isVerified = true;
$user->verificationToken = null; $user->verificationToken = null;
$user->verificationTokenExpiresAt = null;
$this->em->flush(); $this->em->flush();
$this->activationNotificationEmail->send($user, new DateTime()); $this->activationNotificationEmail->send($user, new DateTime());
+3
View File
@@ -62,6 +62,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, TotpTwo
#[Column(length: 64, nullable: true)] #[Column(length: 64, nullable: true)]
public ?string $verificationToken = null; public ?string $verificationToken = null;
#[Column(type: Types::DATETIME_MUTABLE, nullable: true)]
public ?DateTime $verificationTokenExpiresAt = null;
#[Column(length: 64, nullable: true)] #[Column(length: 64, nullable: true)]
public ?string $resetToken = null; public ?string $resetToken = null;
+4 -4
View File
@@ -19,7 +19,7 @@ use Symfony\Component\Security\Http\Event\CheckPassportEvent;
/** /**
* Class LoginCaptchaListener * Class LoginCaptchaListener
* *
* Validates the Google reCAPTCHA v3 token during form-login authentication. * Validates the Cap CAPTCHA token during form-login authentication.
* Fires on CheckPassportEvent, which is dispatched after credentials are * Fires on CheckPassportEvent, which is dispatched after credentials are
* collected but before the user is authenticated. * collected but before the user is authenticated.
* *
@@ -53,12 +53,12 @@ readonly class LoginCaptchaListener
return; return;
} }
$token = $request->request->getString('g-recaptcha-response'); $token = $request->request->getString('cap-token');
if ($this->recaptcha->verify($token, $request->getClientIp())) { if ($this->recaptcha->verify($token)) {
return; return;
} }
throw new CustomUserMessageAuthenticationException('reCAPTCHA verification failed. Please try again.'); throw new CustomUserMessageAuthenticationException('CAPTCHA verification failed. Please try again.');
} }
} }
+4 -5
View File
@@ -22,8 +22,8 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/** /**
* Class RecaptchaType * Class RecaptchaType
* *
* Reads the Google reCAPTCHA v3 token from the raw POST field * Reads the Cap CAPTCHA token from the raw POST field
* `g-recaptcha-response` (populated by JS before form submit) and injects * `cap-token` (auto-injected by the cap-widget web component) and injects
* it as this field's value before validation runs. * it as this field's value before validation runs.
* *
* @package App\Form * @package App\Form
@@ -41,9 +41,8 @@ class RecaptchaType extends AbstractType
{ {
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (PreSubmitEvent $event): void { $builder->addEventListener(FormEvents::PRE_SUBMIT, function (PreSubmitEvent $event): void {
$request = $this->requestStack->getCurrentRequest(); $request = $this->requestStack->getCurrentRequest();
$token = $request?->request->getString('g-recaptcha-response') ?? ''; $token = $request?->request->getString('cap-token') ?? '';
// For forms that set the token directly on the field (e.g. registration_form[recaptcha]) // For forms that set the token directly on the field, fall back to the submitted value.
// rather than via a standalone g-recaptcha-response input, fall back to the submitted value.
if ($token === '') { if ($token === '') {
$token = (string) ($event->getData() ?? ''); $token = (string) ($event->getData() ?? '');
} }
@@ -0,0 +1,45 @@
<?php declare(strict_types=1);
/*
* This file is part of the SplendidBear Websites' projects.
*
* Copyright (c) 2026 @ www.splendidbear.org
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Migrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Class Version20260727120000
*
* @package App\Migrations
* @author Lang <https://www.splendidbear.org>
* @category Class
* @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
* @link www.splendidbear.org
* @since 2026. 07. 27.
*/
final class Version20260727120000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add expiry timestamps for account activation tokens.';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE app_user ADD verification_token_expires_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
$this->addSql("UPDATE app_user SET verification_token_expires_at = CURRENT_TIMESTAMP + INTERVAL '24 hours' WHERE is_verified = FALSE AND verification_token IS NOT NULL");
$this->addSql('CREATE INDEX IDX_APP_USER_PENDING_ACTIVATION_EXPIRY ON app_user (verification_token_expires_at) WHERE is_verified = FALSE AND verification_token_expires_at IS NOT NULL');
}
public function down(Schema $schema): void
{
$this->addSql('DROP INDEX IDX_APP_USER_PENDING_ACTIVATION_EXPIRY');
$this->addSql('ALTER TABLE app_user DROP verification_token_expires_at');
}
}
+29
View File
@@ -11,6 +11,7 @@
namespace App\Repository; namespace App\Repository;
use App\Entity\User; use App\Entity\User;
use DateTimeInterface;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NonUniqueResultException;
use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ManagerRegistry;
@@ -87,6 +88,34 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader
} }
} }
public function countExpiredPendingUsers(DateTimeInterface $now): int
{
$qb = $this->createQueryBuilder('u');
return (int) $qb
->select($qb->expr()->count('u.id'))
->where($qb->expr()->eq('u.isVerified', ':isVerified'))
->andWhere($qb->expr()->lte('u.verificationTokenExpiresAt', ':now'))
->setParameter('isVerified', false)
->setParameter('now', $now)
->getQuery()
->getSingleScalarResult();
}
public function deleteExpiredPendingUsers(DateTimeInterface $now): int
{
$qb = $this->createQueryBuilder('u');
return $qb
->delete()
->where($qb->expr()->eq('u.isVerified', ':isVerified'))
->andWhere($qb->expr()->lte('u.verificationTokenExpiresAt', ':now'))
->setParameter('isVerified', false)
->setParameter('now', $now)
->getQuery()
->execute();
}
public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
{ {
if (!$user instanceof User) { if (!$user instanceof User) {
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace App;
use Symfony\Component\Scheduler\Attribute\AsSchedule;
use Symfony\Component\Scheduler\Schedule as SymfonySchedule;
use Symfony\Component\Scheduler\ScheduleProviderInterface;
use Symfony\Contracts\Cache\CacheInterface;
/**
* Class Schedule
*
* @package App
* @author Lang <https://www.splendidbear.org>
* @category Class
* @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
* @link www.splendidbear.org
* @since 2026. 07. 27.
*/
#[AsSchedule]
class Schedule implements ScheduleProviderInterface
{
public function __construct(private CacheInterface $cache) { }
public function getSchedule(): SymfonySchedule
{
return (new SymfonySchedule())
->stateful($this->cache) // ensure missed tasks are executed
->processOnlyLastMissedRun(true) // ensure only last missed task is run
// add your own tasks here
// see https://symfony.com/doc/current/scheduler.html#attaching-recurring-messages-to-a-schedule
;
}
}
+2 -1
View File
@@ -11,6 +11,7 @@
namespace App\Security; namespace App\Security;
use App\Entity\User; use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserInterface;
@@ -40,5 +41,5 @@ final class UserChecker implements UserCheckerInterface
} }
} }
public function checkPostAuth(UserInterface $user): void { } public function checkPostAuth(UserInterface $user, ?TokenInterface $token = null): void { }
} }
-4
View File
@@ -186,7 +186,6 @@ final class BattleCardGenerator
$this->centeredText($im, 'mineseeker.hu', 16, self::WIDTH / 2, self::HEIGHT - 20, $muted); $this->centeredText($im, 'mineseeker.hu', 16, self::WIDTH / 2, self::HEIGHT - 20, $muted);
imagepng($im, $dest); imagepng($im, $dest);
imagedestroy($im);
} }
/** Draw avatar or initials centered on $cx, $cy. */ /** Draw avatar or initials centered on $cx, $cy. */
@@ -263,9 +262,6 @@ final class BattleCardGenerator
} }
} }
imagedestroy($avatarImg);
imagedestroy($resized);
imagedestroy($mask);
} else { } else {
/** Draw circular background with initials*/ /** Draw circular background with initials*/
imagefilledellipse($im, (int)$cx, (int)$cy, self::AVATAR_SIZE, self::AVATAR_SIZE, $color); imagefilledellipse($im, (int)$cx, (int)$cy, self::AVATAR_SIZE, self::AVATAR_SIZE, $color);
@@ -1,75 +0,0 @@
<?php declare(strict_types=1);
/**
* This file is part of the SplendidBear Websites' projects.
*
* Copyright (c) 2026 @ www.splendidbear.org
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*
* This file is part of the SplendidBear Websites' projects.
*
* Copyright (c) 2026 @ www.splendidbear.org
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Service\Email;
use App\Entity\User;
use DateTime;
use Exception;
use Psr\Log\LoggerInterface;
use RuntimeException;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\MailerInterface;
/**
* Class SendUserRegistrationNotificationService
*
* @package App\Service\Email
* @author Lang <https://www.splendidbear.org>
* @category Class
* @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
* @link www.splendidbear.org
* @since 2026. 04. 20.
*/
readonly final class SendUserRegistrationNotificationService
{
public function __construct(
#[Autowire(env: 'APP_CONTACT_MAIL_ADDRESS')]
private string $appContactMailAddress,
private LoggerInterface $logger,
private MailerInterface $mailer,
) {
}
public function send(User $user, DateTime $registeredAt): void
{
try {
$this->mailer->send(
new TemplatedEmail()
->from('noreply@mineseeker.hu')
->to($this->appContactMailAddress)
->subject("🎉 New User Registration: {$user->getUsername()}")
->htmlTemplate('emails/user_registration_notification.html.twig')
->context([
'user' => $user,
'registeredAt' => $registeredAt,
])
);
} catch (TransportExceptionInterface|Exception $e) {
$this->logger->error("Failed to send user registration notification: {$e->getMessage()}", [
'exception' => $e,
'user' => $user->getUsername(),
]);
throw new RuntimeException("Failed to send user registration notification: {$e->getMessage()}");
}
}
}
+13 -25
View File
@@ -26,50 +26,38 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
*/ */
readonly final class RecaptchaService readonly final class RecaptchaService
{ {
private const string SITEVERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify';
/**
* Minimum score to accept a request (0.0 = bot, 1.0 = human).
* 0.5 is Google's recommended default threshold.
*/
private const float SCORE_THRESHOLD = 0.5;
public function __construct( public function __construct(
private HttpClientInterface $httpClient, private HttpClientInterface $httpClient,
private LoggerInterface $logger, private LoggerInterface $logger,
#[Autowire(env: 'RECAPTCHA_SECRET_KEY')] #[Autowire(env: 'CAP_API_ENDPOINT')]
private string $apiEndpoint,
#[Autowire(env: 'CAP_SECRET_KEY')]
private string $secretKey, private string $secretKey,
) {} ) {}
public function verify(string $token, string $remoteIp = ''): bool public function verify(string $token): bool
{ {
if ($token === '') { if ($token === '') {
return false; return false;
} }
try { try {
$body = ['secret' => $this->secretKey, 'response' => $token]; $siteverifyUrl = rtrim($this->apiEndpoint, '/') . '/siteverify';
if ($remoteIp !== '') {
$body['remoteip'] = $remoteIp;
}
$data = $this->httpClient $data = $this->httpClient
->request('POST', self::SITEVERIFY_URL, ['body' => $body]) ->request('POST', $siteverifyUrl, [
'body' => ['secret' => $this->secretKey, 'response' => $token],
])
->toArray(); ->toArray();
$this->logger->info('reCAPTCHA verify response', [ $this->logger->info('Cap verify response', [
'success' => $data['success'] ?? null, 'success' => $data['success'] ?? null,
'score' => $data['score'] ?? null, 'token_length' => strlen($token),
'hostname' => $data['hostname'] ?? null,
'error-codes' => $data['error-codes'] ?? [],
'token_length' => strlen($token),
]); ]);
return ($data['success'] ?? false) === true return ($data['success'] ?? false) === true;
&& ($data['score'] ?? 0.0) >= self::SCORE_THRESHOLD;
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->logger->error('reCAPTCHA verification failed: ' . $e->getMessage()); $this->logger->error('Cap verification failed: ' . $e->getMessage());
return false; return false;
} }
+1 -6
View File
@@ -11,7 +11,6 @@
namespace App\Validator; namespace App\Validator;
use App\Service\RecaptchaService; use App\Service\RecaptchaService;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedTypeException;
@@ -30,7 +29,6 @@ final class RecaptchaValidator extends ConstraintValidator
{ {
public function __construct( public function __construct(
private readonly RecaptchaService $recaptcha, private readonly RecaptchaService $recaptcha,
private readonly RequestStack $requestStack,
) { ) {
} }
@@ -40,10 +38,7 @@ final class RecaptchaValidator extends ConstraintValidator
throw new UnexpectedTypeException($constraint, Recaptcha::class); throw new UnexpectedTypeException($constraint, Recaptcha::class);
} }
$request = $this->requestStack->getCurrentRequest(); if ($this->recaptcha->verify((string)$value)) {
$remoteIp = $request !== null ? ((string)$request->getClientIp()) : '';
if ($this->recaptcha->verify((string)$value, $remoteIp)) {
return; return;
} }
+24
View File
@@ -316,6 +316,18 @@
"config/packages/mercure.yaml" "config/packages/mercure.yaml"
] ]
}, },
"symfony/messenger": {
"version": "7.4",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.0",
"ref": "d8936e2e2230637ef97e5eecc0eea074eecae58b"
},
"files": [
"config/packages/messenger.yaml"
]
},
"symfony/mime": { "symfony/mime": {
"version": "v4.3.5" "version": "v4.3.5"
}, },
@@ -375,6 +387,18 @@
"ref": "cda8b550123383d25827705d05a42acf6819fe4e" "ref": "cda8b550123383d25827705d05a42acf6819fe4e"
} }
}, },
"symfony/scheduler": {
"version": "7.4",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.2",
"ref": "caea3c928ee9e1b21288fd76aef36f16ea355515"
},
"files": [
"src/Schedule.php"
]
},
"symfony/security": { "symfony/security": {
"version": "v4.0.9" "version": "v4.0.9"
}, },
+6 -1
View File
@@ -265,7 +265,12 @@
</nav> </nav>
</div> </div>
<div class="footer-copy"> <div class="footer-copy">
<p>&copy; {{ "now"|date("Y") }} MineSeeker &nbsp;&bull;&nbsp; All rights reserved &nbsp;&bull;&nbsp; <p>&copy; {{ "now"|date("Y") }} MineSeeker
<span class="footer-version" title="MineSeeker version {{ version }}">
<i class="fas fa-code-branch" aria-hidden="true"></i>
<span>v{{ version }}</span>
</span>
&nbsp;&bull;&nbsp; All rights reserved &nbsp;&bull;&nbsp;
<a href="https://www.splendidbear.org">www.splendidbear.org</a> <a href="https://www.splendidbear.org">www.splendidbear.org</a>
</p> </p>
</div> </div>
+10 -6
View File
@@ -3,18 +3,23 @@
{% block title %} - Play!{% endblock %} {% block title %} - Play!{% endblock %}
{% block body %} {% block body %}
<a class="back-from-game" href="{{ path('MineSeekerBundle_homepage') }}"> <div class="game-brand-bar">
<img src="{{ asset('/images/mine-logo-txt.png') }}" alt="Mineseeker Logo"> <a class="back-from-game" href="{{ path('MineSeekerBundle_homepage') }}">
</a> <img src="{{ asset('/images/mine-logo-txt.png') }}" alt="Mineseeker Logo">
</a>
<a class="game-cap-badge" href="https://trycap.dev" target="_blank" rel="noopener noreferrer">
<i class="fas fa-shield-halved"></i> Protected by Cap
</a>
</div>
<div class="mine-container"> <div class="mine-container">
<div id="mine-wrapper" <div id="mine-wrapper"
data-env="{{ env }}" data-env="{{ env }}"
data-game-id="{{ app.request.get('gameAssoc') }}" data-game-id="{{ game_assoc }}"
data-opponent-name="{{ opponent_name }}" data-opponent-name="{{ opponent_name }}"
data-is-authenticated="{{ app.user ? '1' : '0' }}" data-is-authenticated="{{ app.user ? '1' : '0' }}"
data-mercure-hub-url="{{ mercure_hub_url }}" data-mercure-hub-url="{{ mercure_hub_url }}"
data-mercure-subscriber-jwt="{{ mercure_subscriber_jwt }}" data-mercure-subscriber-jwt="{{ mercure_subscriber_jwt }}"
data-recaptcha-site-key="{{ recaptcha_site_key }}"> data-cap-api-endpoint="{{ cap_api_endpoint }}">
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
@@ -39,6 +44,5 @@
{% block javascripts %} {% block javascripts %}
{{ parent() }} {{ parent() }}
<script src="https://www.google.com/recaptcha/api.js?render={{ recaptcha_site_key }}" async defer></script>
{{ vite_entry_script_tags('mineseeker', { dependency: 'react' }) }} {{ vite_entry_script_tags('mineseeker', { dependency: 'react' }) }}
{% endblock %} {% endblock %}
+9 -6
View File
@@ -37,6 +37,14 @@
</p> </p>
<div class="auth-card" style="max-width: 600px; margin: 0 auto;"> <div class="auth-card" style="max-width: 600px; margin: 0 auto;">
<div class="auth-title-row">
<h2 class="auth-title">Contact Form</h2>
<a class="auth-cap-badge" href="https://trycap.dev" target="_blank" rel="noopener noreferrer">
<i class="fas fa-shield-halved"></i> Protected by Cap
</a>
</div>
<p class="auth-sub">Your message is protected by automated abuse checks.</p>
<div data-cap-api-endpoint="{{ cap_api_endpoint }}" style="display: none;" aria-hidden="true"></div>
{{ form_start(form, {attr: {class: 'auth-form'}}) }} {{ form_start(form, {attr: {class: 'auth-form'}}) }}
<div class="auth-field"> <div class="auth-field">
@@ -127,10 +135,5 @@
{% block javascripts %} {% block javascripts %}
{{ parent() }} {{ parent() }}
<script src="https://www.google.com/recaptcha/api.js?render={{ recaptcha_site_key }}" async defer></script> {{ vite_entry_script_tags('cap') }}
{{ vite_entry_script_tags('contact', { dependency: 'react' }) }}
<div id="contact-form-wrapper"
data-site-key="{{ recaptcha_site_key }}"
data-recaptcha-field-id="{{ form.recaptcha.vars.id }}">
</div>
{% endblock %} {% endblock %}
+8 -17
View File
@@ -34,8 +34,14 @@
</div> </div>
{% else %} {% else %}
<div class="auth-card"> <div class="auth-card">
<h2 class="auth-title">Forgot Password</h2> <div class="auth-title-row">
<h2 class="auth-title">Forgot Password</h2>
<a class="auth-cap-badge" href="https://trycap.dev" target="_blank" rel="noopener noreferrer">
<i class="fas fa-shield-halved"></i> Protected by Cap
</a>
</div>
<p class="auth-sub">Enter your email and we'll send you a reset link</p> <p class="auth-sub">Enter your email and we'll send you a reset link</p>
<div data-cap-api-endpoint="{{ cap_api_endpoint }}" style="display: none;" aria-hidden="true"></div>
{{ form_start(form, {attr: {class: 'auth-form'}}) }} {{ form_start(form, {attr: {class: 'auth-form'}}) }}
@@ -75,20 +81,5 @@
{% block javascripts %} {% block javascripts %}
{{ parent() }} {{ parent() }}
<script src="https://www.google.com/recaptcha/api.js?render={{ recaptcha_site_key }}" async defer></script> {{ vite_entry_script_tags('cap') }}
<script>
(function () {
const form = document.querySelector('.auth-form');
if (!form) return;
form.addEventListener('submit', function (e) {
e.preventDefault();
grecaptcha.ready(function () {
grecaptcha.execute('{{ recaptcha_site_key }}', {action: 'forgot_password'}).then(function (token) {
document.getElementById('{{ form.recaptcha.vars.id }}').value = token;
form.submit();
});
});
});
}());
</script>
{% endblock %} {% endblock %}
+9 -18
View File
@@ -36,7 +36,12 @@
{% endfor %} {% endfor %}
<div class="auth-card"> <div class="auth-card">
<h2 class="auth-title">Sign In</h2> <div class="auth-title-row">
<h2 class="auth-title">Sign In</h2>
<a class="auth-cap-badge" href="https://trycap.dev" target="_blank" rel="noopener noreferrer">
<i class="fas fa-shield-halved"></i> Protected by Cap
</a>
</div>
<p class="auth-sub">Welcome back, commander</p> <p class="auth-sub">Welcome back, commander</p>
{% if error %} {% if error %}
@@ -46,6 +51,8 @@
</div> </div>
{% endif %} {% endif %}
<div data-cap-api-endpoint="{{ cap_api_endpoint }}" style="display: none;" aria-hidden="true"></div>
<form class="auth-form" method="post" action="{{ path('MineSeekerBundle_login') }}"> <form class="auth-form" method="post" action="{{ path('MineSeekerBundle_login') }}">
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}"/> <input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}"/>
@@ -92,8 +99,6 @@
</p> </p>
</div> </div>
<input type="hidden" id="g-recaptcha-response" name="g-recaptcha-response"/>
<button type="submit" class="auth-submit"> <button type="submit" class="auth-submit">
<i class="fas fa-right-to-bracket"></i> Sign In <i class="fas fa-right-to-bracket"></i> Sign In
</button> </button>
@@ -121,20 +126,6 @@
{% block javascripts %} {% block javascripts %}
{{ parent() }} {{ parent() }}
<script src="https://www.google.com/recaptcha/api.js?render={{ recaptcha_site_key }}" async defer></script> {{ vite_entry_script_tags('cap') }}
<script>
document.querySelector('.auth-form').addEventListener('submit', function (e) {
e.preventDefault();
const form = this;
grecaptcha.ready(function () {
grecaptcha.execute('{{ recaptcha_site_key }}', {action: 'login'}).then(function (token) {
document.getElementById('g-recaptcha-response').value = token;
form.submit();
});
});
});
</script>
{{ vite_entry_script_tags('passkey', { dependency: 'react' }) }} {{ vite_entry_script_tags('passkey', { dependency: 'react' }) }}
{% endblock %} {% endblock %}
+9 -5
View File
@@ -2,7 +2,9 @@
{% macro stat_val(value, suffix) %} {% macro stat_val(value, suffix) %}
{%- set abbr = value >= 1000 -%} {%- set abbr = value >= 1000 -%}
<span class="profile-stat__value"{% if abbr %} title="{{ value }}"{% endif %}>{% if abbr %}{{ (value / 1000)|round(1, 'floor') }}k{% else %}{{ value }}{% endif %}{% if suffix %}<small>{{ suffix }}</small>{% endif %}</span> <span
class="profile-stat__value"{% if abbr %} title="{{ value }}"{% endif %}>{% if abbr %}{{ (value / 1000)|round(1, 'floor') }}k{% else %}{{ value }}{% endif %}{% if suffix %}
<small>{{ suffix }}</small>{% endif %}</span>
{% endmacro %} {% endmacro %}
{% block title %} - Profile{% endblock %} {% block title %} - Profile{% endblock %}
@@ -31,7 +33,7 @@
<div class="profile-header"> <div class="profile-header">
<div id="profile-avatar-root" <div id="profile-avatar-root"
data-upload-url="{{ path('MineSeekerBundle_profile_avatar') }}" data-upload-url="{{ path('MineSeekerBundle_profile_avatar') }}"
data-thumb-url="{{ app.user.avatarPath ? app.user.avatarPath|imagine_filter('avatar_thumb') : '' }}" data-thumb-url="{{ app.user.avatarPath ? path('liip_imagine_filter', {path: app.user.avatarPath, filter: 'avatar_thumb'}) : '' }}"
data-initials="{{ app.user.username|slice(0, 2)|upper }}"> data-initials="{{ app.user.username|slice(0, 2)|upper }}">
</div> </div>
<div class="profile-info"> <div class="profile-info">
@@ -134,11 +136,13 @@
{% set result = game.result %} {% set result = game.result %}
{% set is_finished = game.resign is not null {% set is_finished = game.resign is not null
or (my_points is not null and opp_points is not null or (my_points is not null and opp_points is not null
and (my_points > 25 or opp_points > 25)) %} and (my_points > 25 or opp_points > 25)) %}
{% set is_anonymous = game.oppIsGuest %} {% set is_anonymous = game.oppIsGuest %}
<div class="profile-game profile-game--{{ result }}{% if not is_finished and not is_anonymous %} profile-game--ongoing{% elseif is_anonymous %} profile-game--abandoned{% endif %}{% if loop.index0 >= 5 %} profile-game--hidden{% endif %}" data-game-index="{{ loop.index0 }}"> <div
class="profile-game profile-game--{{ result }}{% if not is_finished and not is_anonymous %} profile-game--ongoing{% elseif is_anonymous %} profile-game--abandoned{% endif %}{% if loop.index0 >= 5 %} profile-game--hidden{% endif %}"
data-game-index="{{ loop.index0 }}">
<span class="profile-game__badge"> <span class="profile-game__badge">
{% if is_finished %} {% if is_finished %}
{{ result == 'win' ? 'Win' : (result == 'loss' ? 'Loss' : 'Draw') }} {{ result == 'win' ? 'Win' : (result == 'loss' ? 'Loss' : 'Draw') }}
+8 -17
View File
@@ -42,8 +42,14 @@
</div> </div>
{% else %} {% else %}
<div class="auth-card"> <div class="auth-card">
<h2 class="auth-title">Create Account</h2> <div class="auth-title-row">
<h2 class="auth-title">Create Account</h2>
<a class="auth-cap-badge" href="https://trycap.dev" target="_blank" rel="noopener noreferrer">
<i class="fas fa-shield-halved"></i> Protected by Cap
</a>
</div>
<p class="auth-sub">Join the battle — no subscription required</p> <p class="auth-sub">Join the battle — no subscription required</p>
<div data-cap-api-endpoint="{{ cap_api_endpoint }}" style="display: none;" aria-hidden="true"></div>
{{ form_start(form, {attr: {class: 'auth-form'}}) }} {{ form_start(form, {attr: {class: 'auth-form'}}) }}
@@ -153,20 +159,5 @@
{% block javascripts %} {% block javascripts %}
{{ parent() }} {{ parent() }}
<script src="https://www.google.com/recaptcha/api.js?render={{ recaptcha_site_key }}" async defer></script> {{ vite_entry_script_tags('cap') }}
<script>
(function () {
const form = document.querySelector('.auth-form');
if (!form) return;
form.addEventListener('submit', function (e) {
e.preventDefault();
grecaptcha.ready(function () {
grecaptcha.execute('{{ recaptcha_site_key }}', {action: 'register'}).then(function (token) {
document.getElementById('{{ form.recaptcha.vars.id }}').value = token;
form.submit();
});
});
});
}());
</script>
{% endblock %} {% endblock %}
+8 -16
View File
@@ -16,8 +16,14 @@
{% block body %} {% block body %}
<div class="auth-page"> <div class="auth-page">
<div class="auth-card"> <div class="auth-card">
<h2 class="auth-title">Reset Password</h2> <div class="auth-title-row">
<h2 class="auth-title">Reset Password</h2>
<a class="auth-cap-badge" href="https://trycap.dev" target="_blank" rel="noopener noreferrer">
<i class="fas fa-shield-halved"></i> Protected by Cap
</a>
</div>
<p class="auth-sub">Choose a new password for your account</p> <p class="auth-sub">Choose a new password for your account</p>
<div data-cap-api-endpoint="{{ cap_api_endpoint }}" style="display: none;" aria-hidden="true"></div>
{{ form_start(form, {attr: {class: 'auth-form'}}) }} {{ form_start(form, {attr: {class: 'auth-form'}}) }}
@@ -65,19 +71,5 @@
{% block javascripts %} {% block javascripts %}
{{ parent() }} {{ parent() }}
<script src="https://www.google.com/recaptcha/api.js?render={{ recaptcha_site_key }}" async defer></script> {{ vite_entry_script_tags('cap') }}
<script>
(function () {
document.querySelector('.auth-form').addEventListener('submit', function (e) {
e.preventDefault();
const form = this;
grecaptcha.ready(function () {
grecaptcha.execute('{{ recaptcha_site_key }}', {action: 'reset_password'}).then(function (token) {
document.getElementById('{{ form.recaptcha.vars.id }}').value = token;
form.submit();
});
});
});
}());
</script>
{% endblock %} {% endblock %}
+14
View File
@@ -28,6 +28,20 @@
<link rel="icon" href="{{ asset('/images/favicon/favicon.ico') }}" type="image/x-icon"> <link rel="icon" href="{{ asset('/images/favicon/favicon.ico') }}" type="image/x-icon">
{% block metas %}{% endblock %} {% block metas %}{% endblock %}
<title>MineSeeker{% block title %}{% endblock %}</title> <title>MineSeeker{% block title %}{% endblock %}</title>
<script
defer src="https://umami.splendidbear.org/script.js"
data-website-id="825e02a9-d675-4cbd-9e68-72b98de2e4e9"
>
</script>
<script
defer
src="https://umami.splendidbear.org/recorder.js"
data-website-id="825e02a9-d675-4cbd-9e68-72b98de2e4e9"
data-sample-rate="0.15"
data-mask-level="moderate"
data-max-duration="300000"
>
</script>
{% block stylesheets %}{% endblock %} {% block stylesheets %}{% endblock %}
</head> </head>
@@ -1,92 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>New User Registration</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
border-radius: 8px 8px 0 0;
text-align: center;
}
.header h1 {
margin: 0;
font-size: 24px;
}
.content {
background: #f9f9f9;
padding: 30px;
border-radius: 0 0 8px 8px;
}
.field {
margin-bottom: 20px;
}
.field-label {
font-weight: 600;
color: #666;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 5px;
}
.field-value {
background: white;
padding: 12px;
border-radius: 4px;
border-left: 3px solid #667eea;
}
.footer {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #ddd;
font-size: 12px;
color: #999;
text-align: center;
}
</style>
</head>
<body>
<div class="header">
<h1>👤 New User Registration</h1>
</div>
<div class="content">
<div class="field">
<div class="field-label">Username</div>
<div class="field-value">
<strong>{{ user.username }}</strong>
</div>
</div>
<div class="field">
<div class="field-label">Email</div>
<div class="field-value">
<a href="mailto:{{ user.email }}">{{ user.email }}</a>
</div>
</div>
<div class="field">
<div class="field-label">Details</div>
<div class="field-value">
<strong>Registered:</strong> {{ registeredAt|date('Y-m-d H:i:s') }}<br>
<strong>Status:</strong> {% if user.isVerified %}✓ Verified{% else %}⏳ Awaiting Email Verification{% endif %}<br>
<strong>Email Verified:</strong> {% if user.isVerified %}Yes{% else %}No - activation link sent{% endif %}
</div>
</div>
<div class="footer">
A new user has registered on MineSeeker.<br>
User must verify their email before account is fully activated.
</div>
</div>
</body>
</html>
@@ -0,0 +1,74 @@
<?php declare(strict_types=1);
/*
* This file is part of the SplendidBear Websites' projects.
*
* Copyright (c) 2026 @ www.splendidbear.org
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Command;
use App\Command\PurgeExpiredPendingUsersCommand;
use App\Entity\User;
use App\Tests\Factory\UserFactory;
use App\Tests\WebTestCase;
use DateTime;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\TestDox;
use Symfony\Component\Console\Tester\CommandTester;
#[TestDox('Purge Expired Pending Users Command')]
class PurgeExpiredPendingUsersCommandTest extends WebTestCase
{
#[Test]
#[TestDox('Deletes expired unverified users only')]
public function deletesOnlyExpiredUnverifiedUsers(): void
{
$expiredUser = UserFactory::createOne([
'isVerified' => false,
'verificationToken' => 'expired-token',
'verificationTokenExpiresAt' => new DateTime('-1 minute'),
]);
$pendingUser = UserFactory::createOne([
'isVerified' => false,
'verificationToken' => 'valid-token',
'verificationTokenExpiresAt' => new DateTime('+1 day'),
]);
$verifiedUser = UserFactory::createOne([
'isVerified' => true,
'verificationToken' => 'verified-token',
'verificationTokenExpiresAt' => new DateTime('-1 minute'),
]);
$command = static::getContainer()->get(PurgeExpiredPendingUsersCommand::class);
$tester = new CommandTester($command);
$tester->execute([]);
$em = static::getContainer()->get(EntityManagerInterface::class);
$em->clear();
self::assertNull($em->find(User::class, $expiredUser->id));
self::assertNotNull($em->find(User::class, $pendingUser->id));
self::assertNotNull($em->find(User::class, $verifiedUser->id));
}
#[Test]
#[TestDox('Dry run keeps expired unverified users')]
public function dryRunDoesNotDeleteExpiredUsers(): void
{
$expiredUser = UserFactory::createOne([
'isVerified' => false,
'verificationToken' => 'expired-token',
'verificationTokenExpiresAt' => new DateTime('-1 minute'),
]);
$command = static::getContainer()->get(PurgeExpiredPendingUsersCommand::class);
$tester = new CommandTester($command);
$tester->execute(['--dry-run' => true]);
$em = static::getContainer()->get(EntityManagerInterface::class);
self::assertNotNull($em->find(User::class, $expiredUser->id));
}
}
+23 -2
View File
@@ -10,6 +10,8 @@
namespace App\Tests\Controller; namespace App\Tests\Controller;
use DateTime;
use Doctrine\ORM\EntityManagerInterface;
use App\Tests\Factory\UserFactory; use App\Tests\Factory\UserFactory;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\Attributes\TestDox;
@@ -103,6 +105,25 @@ class SecurityControllerTest extends WebTestCase
self::assertResponseRedirects('/login'); self::assertResponseRedirects('/login');
} }
#[Test]
#[TestDox('Expired account activation token does not activate the account')]
public function expiredActivationTokenDoesNotActivateAccount(): void
{
$user = UserFactory::createOne([
'isVerified' => false,
'verificationToken' => 'expired-activation-token',
'verificationTokenExpiresAt' => new DateTime('-1 minute'),
]);
$this->client->request('GET', '/activate/expired-activation-token');
self::assertResponseRedirects('/login');
static::getContainer()->get(EntityManagerInterface::class)->refresh($user);
self::assertFalse($user->isVerified);
self::assertSame('expired-activation-token', $user->verificationToken);
}
#[Test] #[Test]
#[TestDox('Password reset with invalid token redirects to forgot password')] #[TestDox('Password reset with invalid token redirects to forgot password')]
public function resetPasswordWithInvalidTokenShowsError(): void public function resetPasswordWithInvalidTokenShowsError(): void
@@ -117,7 +138,7 @@ class SecurityControllerTest extends WebTestCase
public function authenticatedUserRedirectsFromLogin(): void public function authenticatedUserRedirectsFromLogin(): void
{ {
$user = UserFactory::createOne(); $user = UserFactory::createOne();
$this->client->loginUser($user->_real()); $this->client->loginUser($user);
$this->client->request('GET', '/login'); $this->client->request('GET', '/login');
@@ -129,7 +150,7 @@ class SecurityControllerTest extends WebTestCase
public function authenticatedUserRedirectsFromRegister(): void public function authenticatedUserRedirectsFromRegister(): void
{ {
$user = UserFactory::createOne(); $user = UserFactory::createOne();
$this->client->loginUser($user->_real()); $this->client->loginUser($user);
$this->client->request('GET', '/register'); $this->client->request('GET', '/register');
+3 -3
View File
@@ -11,7 +11,7 @@
namespace App\Tests\Factory; namespace App\Tests\Factory;
use App\Entity\ContactMessage; use App\Entity\ContactMessage;
use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Persistence\PersistentObjectFactory;
/** /**
* Class ContactMessageFactory * Class ContactMessageFactory
@@ -23,9 +23,9 @@ use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory;
* @link www.splendidbear.org * @link www.splendidbear.org
* @since 2026. 04. 21. * @since 2026. 04. 21.
* *
* @extends PersistentProxyObjectFactory<ContactMessage> * @extends PersistentObjectFactory<ContactMessage>
*/ */
class ContactMessageFactory extends PersistentProxyObjectFactory class ContactMessageFactory extends PersistentObjectFactory
{ {
protected function defaults(): array protected function defaults(): array
{ {
+3 -3
View File
@@ -12,7 +12,7 @@ namespace App\Tests\Factory;
use App\Entity\Gamer; use App\Entity\Gamer;
use DateTime; use DateTime;
use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Persistence\PersistentObjectFactory;
/** /**
* Class GamerFactory * Class GamerFactory
@@ -24,9 +24,9 @@ use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory;
* @link www.splendidbear.org * @link www.splendidbear.org
* @since 2026. 04. 21. * @since 2026. 04. 21.
* *
* @extends PersistentProxyObjectFactory<Gamer> * @extends PersistentObjectFactory<Gamer>
*/ */
class GamerFactory extends PersistentProxyObjectFactory class GamerFactory extends PersistentObjectFactory
{ {
protected function defaults(): array protected function defaults(): array
{ {
+4 -4
View File
@@ -12,7 +12,7 @@ namespace App\Tests\Factory;
use App\Entity\Grid; use App\Entity\Grid;
use App\Entity\GridRow; use App\Entity\GridRow;
use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Persistence\PersistentObjectFactory;
/** /**
* Class GridFactory * Class GridFactory
@@ -24,9 +24,9 @@ use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory;
* @link www.splendidbear.org * @link www.splendidbear.org
* @since 2026. 04. 21. * @since 2026. 04. 21.
* *
* @extends PersistentProxyObjectFactory<Grid> * @extends PersistentObjectFactory<Grid>
*/ */
class GridFactory extends PersistentProxyObjectFactory class GridFactory extends PersistentObjectFactory
{ {
protected function defaults(): array protected function defaults(): array
{ {
@@ -45,7 +45,7 @@ class GridFactory extends PersistentProxyObjectFactory
return $this->afterInstantiate(function (Grid $grid): void { return $this->afterInstantiate(function (Grid $grid): void {
for ($i = 0; $i < 16; $i++) { for ($i = 0; $i < 16; $i++) {
/** @var GridRow $factory */ /** @var GridRow $factory */
$factory = GridRowFactory::new()->create()->_real(); $factory = GridRowFactory::new()->create();
$grid->addGridRow($factory); $grid->addGridRow($factory);
} }
}); });
+3 -3
View File
@@ -11,7 +11,7 @@
namespace App\Tests\Factory; namespace App\Tests\Factory;
use App\Entity\GridRow; use App\Entity\GridRow;
use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Persistence\PersistentObjectFactory;
/** /**
* Class GridRowFactory * Class GridRowFactory
@@ -23,9 +23,9 @@ use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory;
* @link www.splendidbear.org * @link www.splendidbear.org
* @since 2026. 04. 21. * @since 2026. 04. 21.
* *
* @extends PersistentProxyObjectFactory<GridRow> * @extends PersistentObjectFactory<GridRow>
*/ */
class GridRowFactory extends PersistentProxyObjectFactory class GridRowFactory extends PersistentObjectFactory
{ {
protected function defaults(): array protected function defaults(): array
{ {
+3 -3
View File
@@ -13,7 +13,7 @@ namespace App\Tests\Factory;
use App\Entity\PlayedGame; use App\Entity\PlayedGame;
use DateTime; use DateTime;
use Symfony\Component\Uid\Uuid; use Symfony\Component\Uid\Uuid;
use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Persistence\PersistentObjectFactory;
/** /**
* Class PlayedGameFactory * Class PlayedGameFactory
@@ -25,9 +25,9 @@ use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory;
* @link www.splendidbear.org * @link www.splendidbear.org
* @since 2026. 04. 21. * @since 2026. 04. 21.
* *
* @extends PersistentProxyObjectFactory<PlayedGame> * @extends PersistentObjectFactory<PlayedGame>
*/ */
class PlayedGameFactory extends PersistentProxyObjectFactory class PlayedGameFactory extends PersistentObjectFactory
{ {
protected function defaults(): array protected function defaults(): array
{ {
+3 -3
View File
@@ -12,7 +12,7 @@ namespace App\Tests\Factory;
use App\Entity\Step; use App\Entity\Step;
use DateTime; use DateTime;
use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Persistence\PersistentObjectFactory;
/** /**
* Class StepFactory * Class StepFactory
@@ -24,9 +24,9 @@ use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory;
* @link www.splendidbear.org * @link www.splendidbear.org
* @since 2026. 04. 21. * @since 2026. 04. 21.
* *
* @extends PersistentProxyObjectFactory<Step> * @extends PersistentObjectFactory<Step>
*/ */
class StepFactory extends PersistentProxyObjectFactory class StepFactory extends PersistentObjectFactory
{ {
protected function defaults(): array protected function defaults(): array
{ {
+3 -3
View File
@@ -11,7 +11,7 @@
namespace App\Tests\Factory; namespace App\Tests\Factory;
use App\Entity\User; use App\Entity\User;
use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Persistence\PersistentObjectFactory;
/** /**
* Class UserFactory * Class UserFactory
@@ -23,9 +23,9 @@ use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory;
* @link www.splendidbear.org * @link www.splendidbear.org
* @since 2026. 04. 21. * @since 2026. 04. 21.
* *
* @extends PersistentProxyObjectFactory<User> * @extends PersistentObjectFactory<User>
*/ */
class UserFactory extends PersistentProxyObjectFactory class UserFactory extends PersistentObjectFactory
{ {
protected function defaults(): array protected function defaults(): array
{ {
+3 -3
View File
@@ -12,7 +12,7 @@ namespace App\Tests\Factory;
use App\Entity\WebAuthnCredential; use App\Entity\WebAuthnCredential;
use DateTime; use DateTime;
use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Persistence\PersistentObjectFactory;
/** /**
* Class WebAuthnCredentialFactory * Class WebAuthnCredentialFactory
@@ -24,9 +24,9 @@ use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory;
* @link www.splendidbear.org * @link www.splendidbear.org
* @since 2026. 04. 21. * @since 2026. 04. 21.
* *
* @extends PersistentProxyObjectFactory<WebAuthnCredential> * @extends PersistentObjectFactory<WebAuthnCredential>
*/ */
class WebAuthnCredentialFactory extends PersistentProxyObjectFactory class WebAuthnCredentialFactory extends PersistentObjectFactory
{ {
protected function defaults(): array protected function defaults(): array
{ {
+3 -3
View File
@@ -44,7 +44,7 @@ class FactoryExampleTest extends WebTestCase
{ {
$user = UserFactory::createOne(); $user = UserFactory::createOne();
self::assertInstanceOf(User::class, $user->_real()); self::assertInstanceOf(User::class, $user);
self::assertNotNull($user->username); self::assertNotNull($user->username);
self::assertNotNull($user->email); self::assertNotNull($user->email);
self::assertTrue($user->isVerified); self::assertTrue($user->isVerified);
@@ -87,7 +87,7 @@ class FactoryExampleTest extends WebTestCase
->withRegisteredPlayers() ->withRegisteredPlayers()
->create(); ->create();
self::assertInstanceOf(PlayedGame::class, $game->_real()); self::assertInstanceOf(PlayedGame::class, $game);
self::assertNotNull($game->red); self::assertNotNull($game->red);
self::assertNotNull($game->blue); self::assertNotNull($game->blue);
self::assertNull($game->redAnon); self::assertNull($game->redAnon);
@@ -181,7 +181,7 @@ class FactoryExampleTest extends WebTestCase
->recentlyUsed() ->recentlyUsed()
->create(['user' => $user]); ->create(['user' => $user]);
self::assertEquals($user->_real(), $credential->user); self::assertEquals($user, $credential->user);
self::assertEquals('YubiKey 5C', $credential->credentialName); self::assertEquals('YubiKey 5C', $credential->credentialName);
self::assertNotNull($credential->lastUsedAt); self::assertNotNull($credential->lastUsedAt);
} }
+25 -88
View File
@@ -34,16 +34,17 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
#[TestDox('Recaptcha Service')] #[TestDox('Recaptcha Service')]
class RecaptchaServiceTest extends TestCase class RecaptchaServiceTest extends TestCase
{ {
private const SECRET_KEY = 'test-secret-key'; private const API_ENDPOINT = 'http://localhost:3000/test-site-key/';
private const SECRET_KEY = 'test-secret-key';
#[Test] #[Test]
#[TestDox('Verify returns false for empty token')] #[TestDox('Verify returns false for empty token')]
public function verifyReturnsFalseForEmptyToken(): void public function verifyReturnsFalseForEmptyToken(): void
{ {
$httpClient = $this->createMock(HttpClientInterface::class); $httpClient = $this->createMock(HttpClientInterface::class);
$logger = $this->createMock(LoggerInterface::class); $logger = $this->createMock(LoggerInterface::class);
$service = new RecaptchaService($httpClient, $logger, self::SECRET_KEY); $service = new RecaptchaService($httpClient, $logger, self::API_ENDPOINT, self::SECRET_KEY);
$result = $service->verify(''); $result = $service->verify('');
@@ -55,15 +56,14 @@ class RecaptchaServiceTest extends TestCase
public function verifyReturnsFalseWhenApiReturnsFailure(): void public function verifyReturnsFalseWhenApiReturnsFailure(): void
{ {
$mockResponse = new MockResponse(json_encode([ $mockResponse = new MockResponse(json_encode([
'success' => false, 'success' => false,
'error-codes' => ['invalid-input-secret'],
], JSON_THROW_ON_ERROR)); ], JSON_THROW_ON_ERROR));
$httpClient = new MockHttpClient($mockResponse); $httpClient = new MockHttpClient($mockResponse);
$logger = $this->createMock(LoggerInterface::class); $logger = $this->createMock(LoggerInterface::class);
$logger->expects($this->once())->method('info'); $logger->expects($this->once())->method('info');
$service = new RecaptchaService($httpClient, $logger, self::SECRET_KEY); $service = new RecaptchaService($httpClient, $logger, self::API_ENDPOINT, self::SECRET_KEY);
$result = $service->verify('invalid-token'); $result = $service->verify('invalid-token');
@@ -71,47 +71,24 @@ class RecaptchaServiceTest extends TestCase
} }
#[Test] #[Test]
#[TestDox('Verify returns true when api returns success and high score')] #[TestDox('Verify returns true when api returns success')]
public function verifyReturnsTrueWhenApiReturnsSuccessAndHighScore(): void public function verifyReturnsTrueWhenApiReturnsSuccess(): void
{ {
$mockResponse = new MockResponse(json_encode([ $mockResponse = new MockResponse(json_encode([
'success' => true, 'success' => true,
'score' => 0.8,
'hostname' => 'test.com',
], JSON_THROW_ON_ERROR)); ], JSON_THROW_ON_ERROR));
$httpClient = new MockHttpClient($mockResponse); $httpClient = new MockHttpClient($mockResponse);
$logger = $this->createMock(LoggerInterface::class); $logger = $this->createMock(LoggerInterface::class);
$logger->expects($this->once())->method('info'); $logger->expects($this->once())->method('info');
$service = new RecaptchaService($httpClient, $logger, self::SECRET_KEY); $service = new RecaptchaService($httpClient, $logger, self::API_ENDPOINT, self::SECRET_KEY);
$result = $service->verify('valid-token'); $result = $service->verify('valid-token');
$this->assertTrue($result); $this->assertTrue($result);
} }
#[Test]
#[TestDox('Verify returns false when score below threshold')]
public function verifyReturnsFalseWhenScoreBelowThreshold(): void
{
$mockResponse = new MockResponse(json_encode([
'success' => true,
'score' => 0.3,
'hostname' => 'test.com',
], JSON_THROW_ON_ERROR));
$httpClient = new MockHttpClient($mockResponse);
$logger = $this->createMock(LoggerInterface::class);
$logger->expects($this->once())->method('info');
$service = new RecaptchaService($httpClient, $logger, self::SECRET_KEY);
$result = $service->verify('low-score-token');
$this->assertFalse($result);
}
#[Test] #[Test]
#[TestDox('Verify returns false when api throws exception')] #[TestDox('Verify returns false when api throws exception')]
public function verifyReturnsFalseWhenApiThrowsException(): void public function verifyReturnsFalseWhenApiThrowsException(): void
@@ -119,10 +96,10 @@ class RecaptchaServiceTest extends TestCase
$mockResponse = new MockResponse('', ['http_code' => 500]); $mockResponse = new MockResponse('', ['http_code' => 500]);
$httpClient = new MockHttpClient($mockResponse); $httpClient = new MockHttpClient($mockResponse);
$logger = $this->createMock(LoggerInterface::class); $logger = $this->createMock(LoggerInterface::class);
$logger->expects($this->once())->method('error'); $logger->expects($this->once())->method('error');
$service = new RecaptchaService($httpClient, $logger, self::SECRET_KEY); $service = new RecaptchaService($httpClient, $logger, self::API_ENDPOINT, self::SECRET_KEY);
$result = $service->verify('test-token'); $result = $service->verify('test-token');
@@ -130,59 +107,19 @@ class RecaptchaServiceTest extends TestCase
} }
#[Test] #[Test]
#[TestDox('Verify includes remote ip when provided')] #[TestDox('Siteverify URL is derived from api endpoint')]
public function verifyIncludesRemoteIpWhenProvided(): void public function siteverifyUrlIsDerivedFromApiEndpoint(): void
{ {
$mockResponse = new MockResponse(json_encode([ $mockResponse = new MockResponse(json_encode(['success' => true], JSON_THROW_ON_ERROR));
'success' => true,
'score' => 0.9,
], JSON_THROW_ON_ERROR));
$httpClient = new MockHttpClient($mockResponse); $httpClient = new MockHttpClient(function (string $method, string $url) use ($mockResponse): MockResponse {
$logger = $this->createMock(LoggerInterface::class); $this->assertSame('http://localhost:3000/test-site-key/siteverify', $url);
$logger->expects($this->once())->method('info'); return $mockResponse;
});
$service = new RecaptchaService($httpClient, $logger, self::SECRET_KEY); $logger = $this->createMock(LoggerInterface::class);
$service = new RecaptchaService($httpClient, $logger, self::API_ENDPOINT, self::SECRET_KEY);
$result = $service->verify('test-token', '192.168.1.1'); $service->verify('test-token');
$this->assertTrue($result);
} }
}
#[Test]
#[TestDox('Verify with score at threshold')]
public function verifyWithScoreAtThreshold(): void
{
$mockResponse = new MockResponse(json_encode([
'success' => true,
'score' => 0.5,
], JSON_THROW_ON_ERROR));
$httpClient = new MockHttpClient($mockResponse);
$logger = $this->createMock(LoggerInterface::class);
$service = new RecaptchaService($httpClient, $logger, self::SECRET_KEY);
$result = $service->verify('threshold-token');
$this->assertTrue($result);
}
#[Test]
#[TestDox('Verify with no score fails')]
public function verifyWithNoScoreFails(): void
{
$mockResponse = new MockResponse(json_encode([
'success' => true,
], JSON_THROW_ON_ERROR));
$httpClient = new MockHttpClient($mockResponse);
$logger = $this->createMock(LoggerInterface::class);
$service = new RecaptchaService($httpClient, $logger, self::SECRET_KEY);
$result = $service->verify('no-score-token');
$this->assertFalse($result);
}
}
+1 -1
View File
@@ -27,7 +27,7 @@ export default defineConfig({
mineseeker: './assets/js/app.jsx', mineseeker: './assets/js/app.jsx',
passkey: './assets/js/passkey.jsx', passkey: './assets/js/passkey.jsx',
profile: './assets/js/profile.jsx', profile: './assets/js/profile.jsx',
contact: './assets/js/contact.jsx', cap: './assets/js/cap.js',
mineseekerStyle: './assets/css/style.mineseeker.scss', mineseekerStyle: './assets/css/style.mineseeker.scss',
homeStyle: './assets/css/style.layout.scss', homeStyle: './assets/css/style.layout.scss',
passkeyStyle: './assets/css/passkey.scss', passkeyStyle: './assets/css/passkey.scss',