Added support for login and providing the godot client webapp

develop
Hecht 5 months ago
parent 35101b5cec
commit 0bc36b69b9

4
.gitignore vendored

@ -1,4 +1,8 @@
/.* /.*
public/godot/index.pck
public/godot/index.wasm
public/godot/index.pck.br
public/godot/index.wasm.br
###> symfony/framework-bundle ### ###> symfony/framework-bundle ###
/.env.local /.env.local

@ -14,6 +14,7 @@
"nelmio/cors-bundle": "^2.4", "nelmio/cors-bundle": "^2.4",
"phpdocumentor/reflection-docblock": "^5.3", "phpdocumentor/reflection-docblock": "^5.3",
"phpstan/phpdoc-parser": "^1.25", "phpstan/phpdoc-parser": "^1.25",
"symfony/apache-pack": "^1.0",
"symfony/asset": "6.4.*", "symfony/asset": "6.4.*",
"symfony/console": "6.4.*", "symfony/console": "6.4.*",
"symfony/dotenv": "6.4.*", "symfony/dotenv": "6.4.*",

40
composer.lock generated

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "cb35f34a2e8fe45ee2ca7ec7965bfaed", "content-hash": "2cfe24bccdeb45c935548631fee3ec64",
"packages": [ "packages": [
{ {
"name": "api-platform/core", "name": "api-platform/core",
@ -2078,18 +2078,44 @@
}, },
"time": "2021-07-14T16:46:02+00:00" "time": "2021-07-14T16:46:02+00:00"
}, },
{
"name": "symfony/apache-pack",
"version": "v1.0.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/apache-pack.git",
"reference": "3aa5818d73ad2551281fc58a75afd9ca82622e6c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/apache-pack/zipball/3aa5818d73ad2551281fc58a75afd9ca82622e6c",
"reference": "3aa5818d73ad2551281fc58a75afd9ca82622e6c",
"shasum": ""
},
"type": "symfony-pack",
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "A pack for Apache support in Symfony",
"support": {
"issues": "https://github.com/symfony/apache-pack/issues",
"source": "https://github.com/symfony/apache-pack/tree/master"
},
"time": "2017-12-12T01:46:35+00:00"
},
{ {
"name": "symfony/asset", "name": "symfony/asset",
"version": "v6.4.3", "version": "v6.4.7",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/asset.git", "url": "https://github.com/symfony/asset.git",
"reference": "14b1c0fddb64af6ea626af51bb3c47af9fa19cb7" "reference": "743757ab1dfa7114a8a3c63aeb376dfcf4b0a191"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/asset/zipball/14b1c0fddb64af6ea626af51bb3c47af9fa19cb7", "url": "https://api.github.com/repos/symfony/asset/zipball/743757ab1dfa7114a8a3c63aeb376dfcf4b0a191",
"reference": "14b1c0fddb64af6ea626af51bb3c47af9fa19cb7", "reference": "743757ab1dfa7114a8a3c63aeb376dfcf4b0a191",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2129,7 +2155,7 @@
"description": "Manages URL generation and versioning of web assets such as CSS stylesheets, JavaScript files and image files", "description": "Manages URL generation and versioning of web assets such as CSS stylesheets, JavaScript files and image files",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/asset/tree/v6.4.3" "source": "https://github.com/symfony/asset/tree/v6.4.7"
}, },
"funding": [ "funding": [
{ {
@ -2145,7 +2171,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-01-23T14:51:35+00:00" "time": "2024-04-18T09:22:46+00:00"
}, },
{ {
"name": "symfony/cache", "name": "symfony/cache",

@ -12,7 +12,9 @@
<operation class="ApiPlatform\Metadata\Get" controller="App\Controller\GetUserByName" uriTemplate="users/authName/{authName}" description="Receives a user by its authentication name"> <operation class="ApiPlatform\Metadata\Get" controller="App\Controller\GetUserByName" uriTemplate="users/authName/{authName}" description="Receives a user by its authentication name">
<uriVariables><uriVariable parameterName="authName"></uriVariable></uriVariables> <uriVariables><uriVariable parameterName="authName"></uriVariable></uriVariables>
</operation> </operation>
<operation class="ApiPlatform\Metadata\Patch" /> <operation class="ApiPlatform\Metadata\Patch">
<denormalizationContext><values><value name="groups"><values><value>public</value></values></value></values></denormalizationContext>
</operation>
</operations> </operations>
</resource> </resource>
@ -129,8 +131,7 @@
<operations> <operations>
<operation class="ApiPlatform\Metadata\Get" /> <operation class="ApiPlatform\Metadata\Get" />
<operation class="ApiPlatform\Metadata\GetCollection" /> <operation class="ApiPlatform\Metadata\GetCollection" />
<operation class="ApiPlatform\Metadata\Post" security="is_granted('ROLE_ADMIN')"> <operation class="ApiPlatform\Metadata\Post" security="is_granted('ROLE_ADMIN')" />
</operation>
</operations> </operations>
</resource> </resource>

@ -8,3 +8,4 @@ nelmio_cors:
max_age: 3600 max_age: 3600
paths: paths:
'^/': null '^/': null

@ -0,0 +1,151 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240508185559 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE "character" (id UUID NOT NULL, dojo_id UUID DEFAULT NULL, name VARCHAR(64) NOT NULL, strength INT NOT NULL, constitution INT NOT NULL, agility INT NOT NULL, chi INT NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_937AB03432F09E9C ON "character" (dojo_id)');
$this->addSql('COMMENT ON COLUMN "character".id IS \'(DC2Type:ulid)\'');
$this->addSql('COMMENT ON COLUMN "character".dojo_id IS \'(DC2Type:ulid)\'');
$this->addSql('CREATE TABLE character_technique (character_id UUID NOT NULL, technique_id UUID NOT NULL, PRIMARY KEY(character_id, technique_id))');
$this->addSql('CREATE INDEX IDX_506B3A7A1136BE75 ON character_technique (character_id)');
$this->addSql('CREATE INDEX IDX_506B3A7A1F8ACB26 ON character_technique (technique_id)');
$this->addSql('COMMENT ON COLUMN character_technique.character_id IS \'(DC2Type:ulid)\'');
$this->addSql('COMMENT ON COLUMN character_technique.technique_id IS \'(DC2Type:ulid)\'');
$this->addSql('CREATE TABLE city (id UUID NOT NULL, dungeon_id UUID NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_2D5B0234B606863 ON city (dungeon_id)');
$this->addSql('COMMENT ON COLUMN city.id IS \'(DC2Type:ulid)\'');
$this->addSql('COMMENT ON COLUMN city.dungeon_id IS \'(DC2Type:ulid)\'');
$this->addSql('CREATE TABLE country (id UUID NOT NULL, capital_id UUID NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_5373C966FC2D9FF7 ON country (capital_id)');
$this->addSql('COMMENT ON COLUMN country.id IS \'(DC2Type:ulid)\'');
$this->addSql('COMMENT ON COLUMN country.capital_id IS \'(DC2Type:ulid)\'');
$this->addSql('CREATE TABLE dojo (id UUID NOT NULL, village_id UUID DEFAULT NULL, owner_id UUID NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_9494CCB15E237E06 ON dojo (name)');
$this->addSql('CREATE INDEX IDX_9494CCB15E0D5582 ON dojo (village_id)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_9494CCB17E3C61F9 ON dojo (owner_id)');
$this->addSql('COMMENT ON COLUMN dojo.id IS \'(DC2Type:ulid)\'');
$this->addSql('COMMENT ON COLUMN dojo.village_id IS \'(DC2Type:ulid)\'');
$this->addSql('COMMENT ON COLUMN dojo.owner_id IS \'(DC2Type:ulid)\'');
$this->addSql('CREATE TABLE dungeon (id UUID NOT NULL, city_id UUID NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_3FFA1F908BAC62AF ON dungeon (city_id)');
$this->addSql('COMMENT ON COLUMN dungeon.id IS \'(DC2Type:ulid)\'');
$this->addSql('COMMENT ON COLUMN dungeon.city_id IS \'(DC2Type:ulid)\'');
$this->addSql('CREATE TABLE fight (id UUID NOT NULL, winner_id UUID DEFAULT NULL, tournament_id UUID NOT NULL, start_date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, events JSON NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_21AA44565DFCD4B8 ON fight (winner_id)');
$this->addSql('CREATE INDEX IDX_21AA445633D1A3E7 ON fight (tournament_id)');
$this->addSql('COMMENT ON COLUMN fight.id IS \'(DC2Type:ulid)\'');
$this->addSql('COMMENT ON COLUMN fight.winner_id IS \'(DC2Type:ulid)\'');
$this->addSql('COMMENT ON COLUMN fight.tournament_id IS \'(DC2Type:ulid)\'');
$this->addSql('CREATE TABLE fight_character (fight_id UUID NOT NULL, character_id UUID NOT NULL, PRIMARY KEY(fight_id, character_id))');
$this->addSql('CREATE INDEX IDX_BBD57C41AC6657E4 ON fight_character (fight_id)');
$this->addSql('CREATE INDEX IDX_BBD57C411136BE75 ON fight_character (character_id)');
$this->addSql('COMMENT ON COLUMN fight_character.fight_id IS \'(DC2Type:ulid)\'');
$this->addSql('COMMENT ON COLUMN fight_character.character_id IS \'(DC2Type:ulid)\'');
$this->addSql('CREATE TABLE prefecture (id UUID NOT NULL, capital_id UUID NOT NULL, country_id UUID NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_ABE6511AFC2D9FF7 ON prefecture (capital_id)');
$this->addSql('CREATE INDEX IDX_ABE6511AF92F3E70 ON prefecture (country_id)');
$this->addSql('COMMENT ON COLUMN prefecture.id IS \'(DC2Type:ulid)\'');
$this->addSql('COMMENT ON COLUMN prefecture.capital_id IS \'(DC2Type:ulid)\'');
$this->addSql('COMMENT ON COLUMN prefecture.country_id IS \'(DC2Type:ulid)\'');
$this->addSql('CREATE TABLE technique (id UUID NOT NULL, prerequisite_id UUID DEFAULT NULL, name VARCHAR(255) NOT NULL, costs INT NOT NULL, damage VARCHAR(255) NOT NULL, energy VARCHAR(255) NOT NULL, accuracy VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_D73B98415E237E06 ON technique (name)');
$this->addSql('CREATE INDEX IDX_D73B9841276AF86B ON technique (prerequisite_id)');
$this->addSql('COMMENT ON COLUMN technique.id IS \'(DC2Type:ulid)\'');
$this->addSql('COMMENT ON COLUMN technique.prerequisite_id IS \'(DC2Type:ulid)\'');
$this->addSql('CREATE TABLE tournament (id UUID NOT NULL, winner_id UUID DEFAULT NULL, name VARCHAR(255) NOT NULL, start_date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_BD5FB8D95DFCD4B8 ON tournament (winner_id)');
$this->addSql('COMMENT ON COLUMN tournament.id IS \'(DC2Type:ulid)\'');
$this->addSql('COMMENT ON COLUMN tournament.winner_id IS \'(DC2Type:ulid)\'');
$this->addSql('CREATE TABLE tournament_character (tournament_id UUID NOT NULL, character_id UUID NOT NULL, PRIMARY KEY(tournament_id, character_id))');
$this->addSql('CREATE INDEX IDX_43FA182533D1A3E7 ON tournament_character (tournament_id)');
$this->addSql('CREATE INDEX IDX_43FA18251136BE75 ON tournament_character (character_id)');
$this->addSql('COMMENT ON COLUMN tournament_character.tournament_id IS \'(DC2Type:ulid)\'');
$this->addSql('COMMENT ON COLUMN tournament_character.character_id IS \'(DC2Type:ulid)\'');
$this->addSql('CREATE TABLE "user" (id UUID NOT NULL, dojo_id UUID DEFAULT NULL, auth_name VARCHAR(32) NOT NULL, properties JSON NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_8D93D64932F09E9C ON "user" (dojo_id)');
$this->addSql('COMMENT ON COLUMN "user".id IS \'(DC2Type:ulid)\'');
$this->addSql('COMMENT ON COLUMN "user".dojo_id IS \'(DC2Type:ulid)\'');
$this->addSql('CREATE TABLE village (id UUID NOT NULL, prefecture_id UUID NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_4E6C7FAA9D39C865 ON village (prefecture_id)');
$this->addSql('COMMENT ON COLUMN village.id IS \'(DC2Type:ulid)\'');
$this->addSql('COMMENT ON COLUMN village.prefecture_id IS \'(DC2Type:ulid)\'');
$this->addSql('ALTER TABLE "character" ADD CONSTRAINT FK_937AB03432F09E9C FOREIGN KEY (dojo_id) REFERENCES dojo (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE character_technique ADD CONSTRAINT FK_506B3A7A1136BE75 FOREIGN KEY (character_id) REFERENCES "character" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE character_technique ADD CONSTRAINT FK_506B3A7A1F8ACB26 FOREIGN KEY (technique_id) REFERENCES technique (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE city ADD CONSTRAINT FK_2D5B0234B606863 FOREIGN KEY (dungeon_id) REFERENCES dungeon (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE country ADD CONSTRAINT FK_5373C966FC2D9FF7 FOREIGN KEY (capital_id) REFERENCES city (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE dojo ADD CONSTRAINT FK_9494CCB15E0D5582 FOREIGN KEY (village_id) REFERENCES village (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE dojo ADD CONSTRAINT FK_9494CCB17E3C61F9 FOREIGN KEY (owner_id) REFERENCES "user" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE dungeon ADD CONSTRAINT FK_3FFA1F908BAC62AF FOREIGN KEY (city_id) REFERENCES city (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE fight ADD CONSTRAINT FK_21AA44565DFCD4B8 FOREIGN KEY (winner_id) REFERENCES "character" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE fight ADD CONSTRAINT FK_21AA445633D1A3E7 FOREIGN KEY (tournament_id) REFERENCES tournament (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE fight_character ADD CONSTRAINT FK_BBD57C41AC6657E4 FOREIGN KEY (fight_id) REFERENCES fight (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE fight_character ADD CONSTRAINT FK_BBD57C411136BE75 FOREIGN KEY (character_id) REFERENCES "character" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE prefecture ADD CONSTRAINT FK_ABE6511AFC2D9FF7 FOREIGN KEY (capital_id) REFERENCES city (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE prefecture ADD CONSTRAINT FK_ABE6511AF92F3E70 FOREIGN KEY (country_id) REFERENCES country (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE technique ADD CONSTRAINT FK_D73B9841276AF86B FOREIGN KEY (prerequisite_id) REFERENCES technique (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE tournament ADD CONSTRAINT FK_BD5FB8D95DFCD4B8 FOREIGN KEY (winner_id) REFERENCES "character" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE tournament_character ADD CONSTRAINT FK_43FA182533D1A3E7 FOREIGN KEY (tournament_id) REFERENCES tournament (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE tournament_character ADD CONSTRAINT FK_43FA18251136BE75 FOREIGN KEY (character_id) REFERENCES "character" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE "user" ADD CONSTRAINT FK_8D93D64932F09E9C FOREIGN KEY (dojo_id) REFERENCES dojo (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE village ADD CONSTRAINT FK_4E6C7FAA9D39C865 FOREIGN KEY (prefecture_id) REFERENCES prefecture (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE "character" DROP CONSTRAINT FK_937AB03432F09E9C');
$this->addSql('ALTER TABLE character_technique DROP CONSTRAINT FK_506B3A7A1136BE75');
$this->addSql('ALTER TABLE character_technique DROP CONSTRAINT FK_506B3A7A1F8ACB26');
$this->addSql('ALTER TABLE city DROP CONSTRAINT FK_2D5B0234B606863');
$this->addSql('ALTER TABLE country DROP CONSTRAINT FK_5373C966FC2D9FF7');
$this->addSql('ALTER TABLE dojo DROP CONSTRAINT FK_9494CCB15E0D5582');
$this->addSql('ALTER TABLE dojo DROP CONSTRAINT FK_9494CCB17E3C61F9');
$this->addSql('ALTER TABLE dungeon DROP CONSTRAINT FK_3FFA1F908BAC62AF');
$this->addSql('ALTER TABLE fight DROP CONSTRAINT FK_21AA44565DFCD4B8');
$this->addSql('ALTER TABLE fight DROP CONSTRAINT FK_21AA445633D1A3E7');
$this->addSql('ALTER TABLE fight_character DROP CONSTRAINT FK_BBD57C41AC6657E4');
$this->addSql('ALTER TABLE fight_character DROP CONSTRAINT FK_BBD57C411136BE75');
$this->addSql('ALTER TABLE prefecture DROP CONSTRAINT FK_ABE6511AFC2D9FF7');
$this->addSql('ALTER TABLE prefecture DROP CONSTRAINT FK_ABE6511AF92F3E70');
$this->addSql('ALTER TABLE technique DROP CONSTRAINT FK_D73B9841276AF86B');
$this->addSql('ALTER TABLE tournament DROP CONSTRAINT FK_BD5FB8D95DFCD4B8');
$this->addSql('ALTER TABLE tournament_character DROP CONSTRAINT FK_43FA182533D1A3E7');
$this->addSql('ALTER TABLE tournament_character DROP CONSTRAINT FK_43FA18251136BE75');
$this->addSql('ALTER TABLE "user" DROP CONSTRAINT FK_8D93D64932F09E9C');
$this->addSql('ALTER TABLE village DROP CONSTRAINT FK_4E6C7FAA9D39C865');
$this->addSql('DROP TABLE "character"');
$this->addSql('DROP TABLE character_technique');
$this->addSql('DROP TABLE city');
$this->addSql('DROP TABLE country');
$this->addSql('DROP TABLE dojo');
$this->addSql('DROP TABLE dungeon');
$this->addSql('DROP TABLE fight');
$this->addSql('DROP TABLE fight_character');
$this->addSql('DROP TABLE prefecture');
$this->addSql('DROP TABLE technique');
$this->addSql('DROP TABLE tournament');
$this->addSql('DROP TABLE tournament_character');
$this->addSql('DROP TABLE "user"');
$this->addSql('DROP TABLE village');
}
}

@ -0,0 +1,75 @@
# Use the front controller as index file. It serves as a fallback solution when
# every other rewrite/redirect fails (e.g. in an aliased environment without
# mod_rewrite). Additionally, this reduces the matching process for the
# start page (path "/") because otherwise Apache will apply the rewriting rules
# to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl).
DirectoryIndex index.php
# By default, Apache does not evaluate symbolic links if you did not enable this
# feature in your server configuration. Uncomment the following line if you
# install assets as symlinks or if you experience problems related to symlinks
# when compiling LESS/Sass/CoffeScript assets.
# Options +SymLinksIfOwnerMatch
# Disabling MultiViews prevents unwanted negotiation, e.g. "/index" should not resolve
# to the front controller "/index.php" but be rewritten to "/index.php/index".
<IfModule mod_negotiation.c>
Options -MultiViews
</IfModule>
<IfModule mod_rewrite.c>
# This Option needs to be enabled for RewriteRule, otherwise it will show an error like
# 'Options FollowSymLinks or SymLinksIfOwnerMatch is off which implies that RewriteRule directive is forbidden'
Options +SymLinksIfOwnerMatch
RewriteEngine On
# Determine the RewriteBase automatically and set it as environment variable.
# If you are using Apache aliases to do mass virtual hosting or installed the
# project in a subdirectory, the base path will be prepended to allow proper
# resolution of the index.php file and to redirect to the correct URI. It will
# work in environments without path prefix as well, providing a safe, one-size
# fits all solution. But as you do not need it in this case, you can comment
# the following 2 lines to eliminate the overhead.
RewriteCond %{REQUEST_URI}::$0 ^(/.+)/(.*)::\2$
RewriteRule .* - [E=BASE:%1]
# Sets the HTTP_AUTHORIZATION header removed by Apache
RewriteCond %{HTTP:Authorization} .+
RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0]
# Redirect to URI without front controller to prevent duplicate content
# (with and without `/index.php`). Only do this redirect on the initial
# rewrite by Apache and not on subsequent cycles. Otherwise we would get an
# endless redirect loop (request -> rewrite to front controller ->
# redirect -> request -> ...).
# So in case you get a "too many redirects" error or you always get redirected
# to the start page because your Apache does not expose the REDIRECT_STATUS
# environment variable, you have 2 choices:
# - disable this feature by commenting the following 2 lines or
# - use Apache >= 2.3.9 and replace all L flags by END flags and remove the
# following RewriteCond (best solution)
RewriteCond %{ENV:REDIRECT_STATUS} =""
RewriteRule ^index\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L]
# If the requested filename exists, simply serve it.
# We only want to let Apache serve files and not directories.
# Rewrite all other queries to the front controller.
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ %{ENV:BASE}/index.php [L]
</IfModule>
<IfModule !mod_rewrite.c>
<IfModule mod_alias.c>
# When mod_rewrite is not available, we instruct a temporary redirect of
# the start page to the front controller explicitly so that the website
# and the generated links can still be used.
RedirectMatch 307 ^/$ /index.php/
# RedirectTemp cannot be used instead
</IfModule>
</IfModule>
Header set Cross-Origin-Embedder-Policy "require-corp"
Header set Cross-Origin-Opener-Policy "same-origin"

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

@ -0,0 +1,213 @@
/**************************************************************************/
/* audio.worklet.js */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
class RingBuffer {
constructor(p_buffer, p_state, p_threads) {
this.buffer = p_buffer;
this.avail = p_state;
this.threads = p_threads;
this.rpos = 0;
this.wpos = 0;
}
data_left() {
return this.threads ? Atomics.load(this.avail, 0) : this.avail;
}
space_left() {
return this.buffer.length - this.data_left();
}
read(output) {
const size = this.buffer.length;
let from = 0;
let to_write = output.length;
if (this.rpos + to_write > size) {
const high = size - this.rpos;
output.set(this.buffer.subarray(this.rpos, size));
from = high;
to_write -= high;
this.rpos = 0;
}
if (to_write) {
output.set(this.buffer.subarray(this.rpos, this.rpos + to_write), from);
}
this.rpos += to_write;
if (this.threads) {
Atomics.add(this.avail, 0, -output.length);
Atomics.notify(this.avail, 0);
} else {
this.avail -= output.length;
}
}
write(p_buffer) {
const to_write = p_buffer.length;
const mw = this.buffer.length - this.wpos;
if (mw >= to_write) {
this.buffer.set(p_buffer, this.wpos);
this.wpos += to_write;
if (mw === to_write) {
this.wpos = 0;
}
} else {
const high = p_buffer.subarray(0, mw);
const low = p_buffer.subarray(mw);
this.buffer.set(high, this.wpos);
this.buffer.set(low);
this.wpos = low.length;
}
if (this.threads) {
Atomics.add(this.avail, 0, to_write);
Atomics.notify(this.avail, 0);
} else {
this.avail += to_write;
}
}
}
class GodotProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.threads = false;
this.running = true;
this.lock = null;
this.notifier = null;
this.output = null;
this.output_buffer = new Float32Array();
this.input = null;
this.input_buffer = new Float32Array();
this.port.onmessage = (event) => {
const cmd = event.data['cmd'];
const data = event.data['data'];
this.parse_message(cmd, data);
};
}
process_notify() {
if (this.notifier) {
Atomics.add(this.notifier, 0, 1);
Atomics.notify(this.notifier, 0);
}
}
parse_message(p_cmd, p_data) {
if (p_cmd === 'start' && p_data) {
const state = p_data[0];
let idx = 0;
this.threads = true;
this.lock = state.subarray(idx, ++idx);
this.notifier = state.subarray(idx, ++idx);
const avail_in = state.subarray(idx, ++idx);
const avail_out = state.subarray(idx, ++idx);
this.input = new RingBuffer(p_data[1], avail_in, true);
this.output = new RingBuffer(p_data[2], avail_out, true);
} else if (p_cmd === 'stop') {
this.running = false;
this.output = null;
this.input = null;
this.lock = null;
this.notifier = null;
} else if (p_cmd === 'start_nothreads') {
this.output = new RingBuffer(p_data[0], p_data[0].length, false);
} else if (p_cmd === 'chunk') {
this.output.write(p_data);
}
}
static array_has_data(arr) {
return arr.length && arr[0].length && arr[0][0].length;
}
process(inputs, outputs, parameters) {
if (!this.running) {
return false; // Stop processing.
}
if (this.output === null) {
return true; // Not ready yet, keep processing.
}
const process_input = GodotProcessor.array_has_data(inputs);
if (process_input) {
const input = inputs[0];
const chunk = input[0].length * input.length;
if (this.input_buffer.length !== chunk) {
this.input_buffer = new Float32Array(chunk);
}
if (!this.threads) {
GodotProcessor.write_input(this.input_buffer, input);
this.port.postMessage({ 'cmd': 'input', 'data': this.input_buffer });
} else if (this.input.space_left() >= chunk) {
GodotProcessor.write_input(this.input_buffer, input);
this.input.write(this.input_buffer);
} else {
this.port.postMessage('Input buffer is full! Skipping input frame.');
}
}
const process_output = GodotProcessor.array_has_data(outputs);
if (process_output) {
const output = outputs[0];
const chunk = output[0].length * output.length;
if (this.output_buffer.length !== chunk) {
this.output_buffer = new Float32Array(chunk);
}
if (this.output.data_left() >= chunk) {
this.output.read(this.output_buffer);
GodotProcessor.write_output(output, this.output_buffer);
if (!this.threads) {
this.port.postMessage({ 'cmd': 'read', 'data': chunk });
}
} else {
this.port.postMessage('Output buffer has not enough frames! Skipping output frame.');
}
}
this.process_notify();
return true;
}
static write_output(dest, source) {
const channels = dest.length;
for (let ch = 0; ch < channels; ch++) {
for (let sample = 0; sample < dest[ch].length; sample++) {
dest[ch][sample] = source[sample * channels + ch];
}
}
}
static write_input(dest, source) {
const channels = source.length;
for (let ch = 0; ch < channels; ch++) {
for (let sample = 0; sample < source[ch].length; sample++) {
dest[sample * channels + ch] = source[ch][sample];
}
}
}
}
registerProcessor('godot-processor', GodotProcessor);

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

@ -0,0 +1,161 @@
/**
* @license
* Copyright 2015 The Emscripten Authors
* SPDX-License-Identifier: MIT
*/
// Pthread Web Worker startup routine:
// This is the entry point file that is loaded first by each Web Worker
// that executes pthreads on the Emscripten application.
'use strict';
var Module = {};
// Thread-local guard variable for one-time init of the JS state
var initializedJS = false;
function assert(condition, text) {
if (!condition) abort('Assertion failed: ' + text);
}
function threadPrintErr() {
var text = Array.prototype.slice.call(arguments).join(' ');
console.error(text);
}
function threadAlert() {
var text = Array.prototype.slice.call(arguments).join(' ');
postMessage({cmd: 'alert', text: text, threadId: Module['_pthread_self']()});
}
// We don't need out() for now, but may need to add it if we want to use it
// here. Or, if this code all moves into the main JS, that problem will go
// away. (For now, adding it here increases code size for no benefit.)
var out = () => { throw 'out() is not defined in worker.js.'; }
var err = threadPrintErr;
self.alert = threadAlert;
Module['instantiateWasm'] = (info, receiveInstance) => {
// Instantiate from the module posted from the main thread.
// We can just use sync instantiation in the worker.
var module = Module['wasmModule'];
// We don't need the module anymore; new threads will be spawned from the main thread.
Module['wasmModule'] = null;
var instance = new WebAssembly.Instance(module, info);
// TODO: Due to Closure regression https://github.com/google/closure-compiler/issues/3193,
// the above line no longer optimizes out down to the following line.
// When the regression is fixed, we can remove this if/else.
return receiveInstance(instance);
}
// Turn unhandled rejected promises into errors so that the main thread will be
// notified about them.
self.onunhandledrejection = (e) => {
throw e.reason ?? e;
};
function handleMessage(e) {
try {
if (e.data.cmd === 'load') { // Preload command that is called once per worker to parse and load the Emscripten code.
// Until we initialize the runtime, queue up any further incoming messages.
let messageQueue = [];
self.onmessage = (e) => messageQueue.push(e);
// And add a callback for when the runtime is initialized.
self.startWorker = (instance) => {
Module = instance;
// Notify the main thread that this thread has loaded.
postMessage({ 'cmd': 'loaded' });
// Process any messages that were queued before the thread was ready.
for (let msg of messageQueue) {
handleMessage(msg);
}
// Restore the real message handler.
self.onmessage = handleMessage;
};
// Module and memory were sent from main thread
Module['wasmModule'] = e.data.wasmModule;
// Use `const` here to ensure that the variable is scoped only to
// that iteration, allowing safe reference from a closure.
for (const handler of e.data.handlers) {
Module[handler] = function() {
postMessage({ cmd: 'callHandler', handler, args: [...arguments] });
}
}
Module['wasmMemory'] = e.data.wasmMemory;
Module['buffer'] = Module['wasmMemory'].buffer;
Module['workerID'] = e.data.workerID;
Module['ENVIRONMENT_IS_PTHREAD'] = true;
if (typeof e.data.urlOrBlob == 'string') {
importScripts(e.data.urlOrBlob);
} else {
var objectUrl = URL.createObjectURL(e.data.urlOrBlob);
importScripts(objectUrl);
URL.revokeObjectURL(objectUrl);
}
Godot(Module);
} else if (e.data.cmd === 'run') {
// Pass the thread address to wasm to store it for fast access.
Module['__emscripten_thread_init'](e.data.pthread_ptr, /*isMainBrowserThread=*/0, /*isMainRuntimeThread=*/0, /*canBlock=*/1);
// Await mailbox notifications with `Atomics.waitAsync` so we can start
// using the fast `Atomics.notify` notification path.
Module['__emscripten_thread_mailbox_await'](e.data.pthread_ptr);
assert(e.data.pthread_ptr);
// Also call inside JS module to set up the stack frame for this pthread in JS module scope
Module['establishStackSpace']();
Module['PThread'].receiveObjectTransfer(e.data);
Module['PThread'].threadInitTLS();
if (!initializedJS) {
initializedJS = true;
}
try {
Module['invokeEntryPoint'](e.data.start_routine, e.data.arg);
} catch(ex) {
if (ex != 'unwind') {
// The pthread "crashed". Do not call `_emscripten_thread_exit` (which
// would make this thread joinable). Instead, re-throw the exception
// and let the top level handler propagate it back to the main thread.
throw ex;
}
}
} else if (e.data.cmd === 'cancel') { // Main thread is asking for a pthread_cancel() on this thread.
if (Module['_pthread_self']()) {
Module['__emscripten_thread_exit'](-1);
}
} else if (e.data.target === 'setimmediate') {
// no-op
} else if (e.data.cmd === 'checkMailbox') {
if (initializedJS) {
Module['checkMailbox']();
}
} else if (e.data.cmd) {
// The received message looks like something that should be handled by this message
// handler, (since there is a e.data.cmd field present), but is not one of the
// recognized commands:
err('worker.js received unknown command ' + e.data.cmd);
err(e.data);
}
} catch(ex) {
err('worker.js onmessage() captured an uncaught exception: ' + ex);
if (ex && ex.stack) err(ex.stack);
if (Module['__emscripten_thread_crashed']) {
Module['__emscripten_thread_crashed']();
}
throw ex;
}
};
self.onmessage = handleMessage;

@ -0,0 +1,12 @@
from php:8.2-apache
RUN apt update && \
apt install -y postgresql-server-dev-15 && \
docker-php-ext-install pdo_pgsql && \
a2enmod rewrite headers
RUN rm -rf /var/www/html && \
ln -sf /app/public /var/www/html && \
sed -i 's/80/8000/' /etc/apache2/ports.conf && \
sed -i 's/80/8000/' /etc/apache2/sites-enabled/000-default.conf

@ -0,0 +1,39 @@
<?php
namespace App\Command;
use App\Security\AccessTokenHandler;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(name: 'app:login-url', description: 'Add a short description for your command')]
class LoginUrlCommand extends Command
{
public function __construct(private AccessTokenHandler $handler)
{
parent::__construct();
}
protected function configure(): void
{
$this->addArgument('auth_name', InputArgument::REQUIRED, 'Authentication name to use.');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$auth_name = $input->getArgument('auth_name');
$msg = $this->handler->encodeMessage($auth_name, "AUTH_LOGIN_SEED");
$io->writeln('URL: <href=http://localhost:8000/login/' . $msg . '>/login</>');
$io->success("");
return Command::SUCCESS;
}
}

@ -0,0 +1,31 @@
<?php
namespace App\Controller;
use App\Security\AccessTokenHandler;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class ClientLoaderController extends AbstractController
{
public function __construct(private AccessTokenHandler $handler)
{}
#[Route('/login/{slug}', name: 'app_client_loader')]
public function index(string $slug): Response
{
$authName = $this->handler->parseMessage($slug, "AUTH_LOGIN_SEED", "5 min");
$size_pck = filesize("godot/index.pck");
$size_wasm = filesize("godot/index.wasm");
return $this->render('client_loader/index.html.twig',
[
'controller_name' => $authName,
'token' => $this->handler->encodeMessage($authName, 'AUTH_SEED'),
'size_wasm' => $size_wasm,
'size_pck' => $size_pck
]);
}
}

@ -167,6 +167,7 @@ class Character extends Thing
} }
/** /**
*
* @return Collection<int, Technique> * @return Collection<int, Technique>
*/ */
public function getTechniques(): Collection public function getTechniques(): Collection

@ -8,6 +8,7 @@ use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\OneToOne; use Doctrine\ORM\Mapping\OneToOne;
use Doctrine\ORM\Mapping\Table; use Doctrine\ORM\Mapping\Table;
use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Serializer\Attribute\Groups;
#[Entity(repositoryClass: 'App\Repository\UserRepository')] #[Entity(repositoryClass: 'App\Repository\UserRepository')]
#[Table(name: '`user`')] #[Table(name: '`user`')]
@ -15,18 +16,20 @@ class User extends Thing implements UserInterface
{ {
// from discord // from discord
#[ApiProperty(writable: false)]
#[Column(length: 32)] #[Column(length: 32)]
#[Groups('detail')]
public string $authName; public string $authName;
#[ApiProperty(writable: false)] #[ApiProperty(writable: false)]
#[OneToOne(inversedBy: 'owner')] #[OneToOne(inversedBy: 'owner')]
#[JoinColumn(onDelete: 'cascade', nullable: true)] #[JoinColumn(onDelete: 'cascade', nullable: true)]
#[Groups('detail')]
public ?Dojo $dojo; public ?Dojo $dojo;
// anonymous data used for the client // anonymous data used for the client
#[ApiProperty()] #[ApiProperty()]
#[Column(type: 'json')] #[Column(type: 'json')]
#[Groups('public')]
public mixed $properties; public mixed $properties;
public function __construct(string $authName) public function __construct(string $authName)

@ -1,6 +1,7 @@
<?php <?php
namespace App\Security; namespace App\Security;
use App\Kernel;
use App\Entity\User; use App\Entity\User;
use App\Repository\UserRepository; use App\Repository\UserRepository;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@ -14,7 +15,8 @@ use DateTimeZone;
class AccessTokenHandler implements AccessTokenHandlerInterface class AccessTokenHandler implements AccessTokenHandlerInterface
{ {
public function __construct(private UserRepository $userRepository, private LoggerInterface $logger) public function __construct(private Kernel $kernel, private UserRepository $userRepository,
private LoggerInterface $logger)
{} {}
public function getUserBadgeFrom(string $accessToken): UserBadge public function getUserBadgeFrom(string $accessToken): UserBadge
@ -23,14 +25,47 @@ class AccessTokenHandler implements AccessTokenHandlerInterface
throw new BadCredentialsException('Missing credentials.'); throw new BadCredentialsException('Missing credentials.');
} }
$sign_seed = sodium_base642bin($_ENV['AUTH_SEED'], SODIUM_BASE64_VARIANT_ORIGINAL); $this->logger->info("Environment is " . $this->kernel->getEnvironment());
$validity = "8 hour";
if ("dev" == $this->kernel->getEnvironment()) {
$validity = "1 year";
}
$tokenKeys = array(
"AUTH_SEED" => $validity
);
foreach ($tokenKeys as $key => $value) {
$authName = $this->parseMessage($accessToken, $key, $value);
if ($authName !== NULL) {
return new UserBadge($authName, fn (string $id) => $this->getOrCreateUser($id));
}
}
throw new BadCredentialsException('Invalid credentials.');
}
private function getOrCreateUser(string $authName): User
{
$user = $this->userRepository->findOneByAuthName($authName);
if (NULL === $user) {
$user = new User($authName);
}
return $user;
}
public function parseMessage(string $accessToken, string $seedKey, string $duration): ?string
{
$sign_seed = sodium_base642bin($_ENV[$seedKey], SODIUM_BASE64_VARIANT_ORIGINAL);
$sign_pair = sodium_crypto_sign_seed_keypair($sign_seed); $sign_pair = sodium_crypto_sign_seed_keypair($sign_seed);
$sign_public = sodium_crypto_sign_publickey($sign_pair); $sign_public = sodium_crypto_sign_publickey($sign_pair);
$message_signed = sodium_base642bin($accessToken, SODIUM_BASE64_VARIANT_URLSAFE); $message_signed = sodium_base642bin($accessToken, SODIUM_BASE64_VARIANT_URLSAFE);
$message = sodium_crypto_sign_open($message_signed, $sign_public); $message = sodium_crypto_sign_open($message_signed, $sign_public);
if ($message === FALSE) { if ($message === FALSE) {
throw new BadCredentialsException('Invalid credentials.'); return NULL;
} }
$arr = explode('|', $message); $arr = explode('|', $message);
@ -38,22 +73,24 @@ class AccessTokenHandler implements AccessTokenHandlerInterface
$ts = new DateTimeImmutable($arr[1], new DateTimeZone("UTC")); $ts = new DateTimeImmutable($arr[1], new DateTimeZone("UTC"));
$now = new DateTimeImmutable("now", new DateTimeZone("UTC")); $now = new DateTimeImmutable("now", new DateTimeZone("UTC"));
$ts = $ts->add(DateInterval::createFromDateString('5 min')); $ts = $ts->add(DateInterval::createFromDateString($duration));
if ($ts < $now) { if ($ts < $now) {
throw new BadCredentialsException('Token has already expired.'); throw new BadCredentialsException('Token has already expired.');
} }
$auth_name = $arr[0]; return $arr[0];
return new UserBadge($auth_name, fn (string $id) => $this->getOrCreateUser($id));
} }
private function getOrCreateUser(string $authName): User public function encodeMessage(string $authName, string $seedKey): string
{ {
$user = $this->userRepository->findOneByAuthName($authName); $now = new DateTimeImmutable("now", new DateTimeZone("UTC"));
if (NULL === $user) {
$user = new User($authName); $sign_seed = sodium_base642bin($_ENV[$seedKey], SODIUM_BASE64_VARIANT_ORIGINAL);
} $sign_pair = sodium_crypto_sign_seed_keypair($sign_seed);
return $user; $sign_secret = sodium_crypto_sign_secretkey($sign_pair);
$message = sodium_crypto_sign($authName . '|' . $now->format("c"), $sign_secret);
return sodium_bin2base64($message, SODIUM_BASE64_VARIANT_URLSAFE);
} }
} }

@ -33,9 +33,9 @@ class TournamentRegistrationStateProcessor implements ProcessorInterface
throw new BadRequestException("Character is already registered!"); throw new BadRequestException("Character is already registered!");
} }
$registration->tournament->addCharacter($registration->character); $tournament->addCharacter($character);
$this->em->persist($registration->tournament); $this->em->persist($tournament);
$this->em->persist($registration->character); $this->em->persist($character);
$this->em->flush(); $this->em->flush();
} }
} }

@ -0,0 +1,7 @@
#!/bin/bash
if ! podman inspect ag-server:latest >/dev/null 2>&1; then
podman build -f server.Dockerfile -t ag-server:latest
fi
podman run --net=host -v $(pwd):/app -it --rm ag-server:latest

@ -90,6 +90,18 @@
"tests/bootstrap.php" "tests/bootstrap.php"
] ]
}, },
"symfony/apache-pack": {
"version": "1.0",
"recipe": {
"repo": "github.com/symfony/recipes-contrib",
"branch": "main",
"version": "1.0",
"ref": "0f18b4decdf5695d692c1d0dfd65516a07a6adf1"
},
"files": [
"public/.htaccess"
]
},
"symfony/console": { "symfony/console": {
"version": "6.2", "version": "6.2",
"recipe": { "recipe": {

@ -0,0 +1,248 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no">
<title>Animegame-Client</title>
<style>
body {
touch-action: none;
margin: 0;
border: 0 none;
padding: 0;
text-align: center;
background-color: black;
}
#canvas {
display: block;
margin: 0;
color: white;
}
#canvas:focus {
outline: none;
}
.godot {
font-family: 'Noto Sans', 'Droid Sans', Arial, sans-serif;
color: #e0e0e0;
background-color: #3b3943;
background-image: linear-gradient(to bottom, #403e48, #35333c);
border: 1px solid #45434e;
box-shadow: 0 0 1px 1px #2f2d35;
}
/* Status display */
#status {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
/* don't consume click events - make children visible explicitly */
visibility: hidden;
}
#status-progress {
width: 366px;
height: 7px;
background-color: #38363A;
border: 1px solid #444246;
padding: 1px;
box-shadow: 0 0 2px 1px #1B1C22;
border-radius: 2px;
visibility: visible;
}
@media only screen and (orientation:portrait) {
#status-progress {
width: 61.8%;
}
}
#status-progress-inner {
height: 100%;
width: 0;
box-sizing: border-box;
transition: width 0.5s linear;
background-color: #202020;
border: 1px solid #222223;
box-shadow: 0 0 1px 1px #27282E;
border-radius: 3px;
}
#status-indeterminate {
height: 42px;
visibility: visible;
position: relative;
}
#status-indeterminate > div {
width: 4.5px;
height: 0;
border-style: solid;
border-width: 9px 3px 0 3px;
border-color: #2b2b2b transparent transparent transparent;
transform-origin: center 21px;
position: absolute;
}
#status-indeterminate > div:nth-child(1) { transform: rotate( 22.5deg); }
#status-indeterminate > div:nth-child(2) { transform: rotate( 67.5deg); }
#status-indeterminate > div:nth-child(3) { transform: rotate(112.5deg); }
#status-indeterminate > div:nth-child(4) { transform: rotate(157.5deg); }
#status-indeterminate > div:nth-child(5) { transform: rotate(202.5deg); }
#status-indeterminate > div:nth-child(6) { transform: rotate(247.5deg); }
#status-indeterminate > div:nth-child(7) { transform: rotate(292.5deg); }
#status-indeterminate > div:nth-child(8) { transform: rotate(337.5deg); }
#status-notice {
margin: 0 100px;
line-height: 1.3;
visibility: visible;
padding: 4px 6px;
visibility: visible;
}
</style>
<link id='-gd-engine-icon' rel='icon' type='image/png' href='/index.icon.png' />
<link rel='apple-touch-icon' href='./index.apple-touch-icon.png'/>
</head>
<body>
<canvas id="canvas">
HTML5 canvas appears to be unsupported in the current browser.<br >
Please try updating or use a different browser.
</canvas>
<div id="status">
<div id="status-progress" style="display: none;" oncontextmenu="event.preventDefault();">
<div id ="status-progress-inner"></div>
</div>
<div id="status-indeterminate" style="display: none;" oncontextmenu="event.preventDefault();">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<div id="status-notice" class="godot" style="display: none;"></div>
</div>
<script src="/index.js"></script>
<script>
const GODOT_CONFIG = {"args":[],"canvasResizePolicy":2,"executable":"index","experimentalVK":false,"fileSizes":{"/index.pck":{{ size_pck }},"/index.wasm":{{ size_wasm }}},"focusCanvas":true,"gdextensionLibs":[]};
const engine = new Engine(GODOT_CONFIG);
(function () {
const INDETERMINATE_STATUS_STEP_MS = 100;
const statusProgress = document.getElementById('status-progress');
const statusProgressInner = document.getElementById('status-progress-inner');
const statusIndeterminate = document.getElementById('status-indeterminate');
const statusNotice = document.getElementById('status-notice');
let initializing = true;
let statusMode = 'hidden';
let animationCallbacks = [];
function animate(time) {
animationCallbacks.forEach((callback) => callback(time));
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
function animateStatusIndeterminate(ms) {
const i = Math.floor((ms / INDETERMINATE_STATUS_STEP_MS) % 8);
if (statusIndeterminate.children[i].style.borderTopColor === '') {
Array.prototype.slice.call(statusIndeterminate.children).forEach((child) => {
child.style.borderTopColor = '';
});
statusIndeterminate.children[i].style.borderTopColor = '#dfdfdf';
}
}
function setStatusMode(mode) {
if (statusMode === mode || !initializing) {
return;
}
[statusProgress, statusIndeterminate, statusNotice].forEach((elem) => {
elem.style.display = 'none';
});
animationCallbacks = animationCallbacks.filter(function (value) {
return (value !== animateStatusIndeterminate);
});
switch (mode) {
case 'progress':
statusProgress.style.display = 'block';
break;
case 'indeterminate':
statusIndeterminate.style.display = 'block';
animationCallbacks.push(animateStatusIndeterminate);
break;
case 'notice':
statusNotice.style.display = 'block';
break;
case 'hidden':
break;
default:
throw new Error('Invalid status mode');
}
statusMode = mode;
}
function setStatusNotice(text) {
while (statusNotice.lastChild) {
statusNotice.removeChild(statusNotice.lastChild);
}
const lines = text.split('\n');
lines.forEach((line) => {
statusNotice.appendChild(document.createTextNode(line));
statusNotice.appendChild(document.createElement('br'));
});
}
function displayFailureNotice(err) {
const msg = err.message || err;
console.error(msg);
setStatusNotice(msg);
setStatusMode('notice');
initializing = false;
}
const missing = Engine.getMissingFeatures();
if (missing.length !== 0) {
const missingMsg = 'Error\nThe following features required to run Godot projects on the Web are missing:\n';
displayFailureNotice(missingMsg + missing.join('\n'));
} else {
setStatusMode('indeterminate');
engine.startGame({
'onProgress': function (current, total) {
if (total > 0) {
statusProgressInner.style.width = `${(current / total) * 100}%`;
setStatusMode('progress');
if (current === total) {
// wait for progress bar animation
setTimeout(() => {
setStatusMode('indeterminate');
}, 500);
}
} else {
setStatusMode('indeterminate');
}
},
}).then(() => {
setStatusMode('hidden');
initializing = false;
}, displayFailureNotice);
}
}());
</script>
</body>
</html>

@ -0,0 +1,22 @@
{% extends 'base.html.twig' %}
{% block title %}Hello ClientLoaderController!{% endblock %}
{% block body %}
<style>
.example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
.example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
</style>
<div class="example-wrapper">
<h1>Hello {{ controller_name }}! ✅</h1>
<p style="white-space:nowrap">Token: {{ token }}</p>
This friendly message is coming from:
<ul>
<li>Your controller at <code><a href="{{ '/home/pascal/git/animegame-dojo/src/Controller/ClientLoaderController.php'|file_link(0) }}">src/Controller/ClientLoaderController.php</a></code></li>
<li>Your template at <code><a href="{{ '/home/pascal/git/animegame-dojo/templates/client_loader/index.html.twig'|file_link(0) }}">templates/client_loader/index.html.twig</a></code></li>
</ul>
</div>
{% endblock %}

@ -0,0 +1,248 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no">
<title>Animegame-Client</title>
<style>
body {
touch-action: none;
margin: 0;
border: 0 none;
padding: 0;
text-align: center;
background-color: black;
}
#canvas {
display: block;
margin: 0;
color: white;
}
#canvas:focus {
outline: none;
}
.godot {
font-family: 'Noto Sans', 'Droid Sans', Arial, sans-serif;
color: #e0e0e0;
background-color: #3b3943;
background-image: linear-gradient(to bottom, #403e48, #35333c);
border: 1px solid #45434e;
box-shadow: 0 0 1px 1px #2f2d35;
}
/* Status display */
#status {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
/* don't consume click events - make children visible explicitly */
visibility: hidden;
}
#status-progress {
width: 366px;
height: 7px;
background-color: #38363A;
border: 1px solid #444246;
padding: 1px;
box-shadow: 0 0 2px 1px #1B1C22;
border-radius: 2px;
visibility: visible;
}
@media only screen and (orientation:portrait) {
#status-progress {
width: 61.8%;
}
}
#status-progress-inner {
height: 100%;
width: 0;
box-sizing: border-box;
transition: width 0.5s linear;
background-color: #202020;
border: 1px solid #222223;
box-shadow: 0 0 1px 1px #27282E;
border-radius: 3px;
}
#status-indeterminate {
height: 42px;
visibility: visible;
position: relative;
}
#status-indeterminate > div {
width: 4.5px;
height: 0;
border-style: solid;
border-width: 9px 3px 0 3px;
border-color: #2b2b2b transparent transparent transparent;
transform-origin: center 21px;
position: absolute;
}
#status-indeterminate > div:nth-child(1) { transform: rotate( 22.5deg); }
#status-indeterminate > div:nth-child(2) { transform: rotate( 67.5deg); }
#status-indeterminate > div:nth-child(3) { transform: rotate(112.5deg); }
#status-indeterminate > div:nth-child(4) { transform: rotate(157.5deg); }
#status-indeterminate > div:nth-child(5) { transform: rotate(202.5deg); }
#status-indeterminate > div:nth-child(6) { transform: rotate(247.5deg); }
#status-indeterminate > div:nth-child(7) { transform: rotate(292.5deg); }
#status-indeterminate > div:nth-child(8) { transform: rotate(337.5deg); }
#status-notice {
margin: 0 100px;
line-height: 1.3;
visibility: visible;
padding: 4px 6px;
visibility: visible;
}
</style>
<link id='-gd-engine-icon' rel='icon' type='image/png' href='{{ asset('godot/index.icon.png') }}' />
<link rel='apple-touch-icon' href='{{ asset('godot/index.apple-touch-icon.png') }}'/>
</head>
<body>
<canvas id="canvas">
HTML5 canvas appears to be unsupported in the current browser.<br >
Please try updating or use a different browser.
</canvas>
<div id="status">
<div id="status-progress" style="display: none;" oncontextmenu="event.preventDefault();">
<div id ="status-progress-inner"></div>
</div>
<div id="status-indeterminate" style="display: none;" oncontextmenu="event.preventDefault();">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<div id="status-notice" class="godot" style="display: none;"></div>
</div>
<script src="{{ asset('godot/index.js') }}"></script>
<script>
const GODOT_CONFIG = {"args":["--", "--token={{ token }}"],"canvasResizePolicy":2,"executable":"{{ asset('godot/index') }}","experimentalVK":false,"fileSizes":{"{{ asset('godot/index.pck') }}":355472,"{{ asset('godot/index.wasm') }}":35708238},"focusCanvas":true,"gdextensionLibs":[]};
const engine = new Engine(GODOT_CONFIG);
(function () {
const INDETERMINATE_STATUS_STEP_MS = 100;
const statusProgress = document.getElementById('status-progress');
const statusProgressInner = document.getElementById('status-progress-inner');
const statusIndeterminate = document.getElementById('status-indeterminate');
const statusNotice = document.getElementById('status-notice');
let initializing = true;
let statusMode = 'hidden';
let animationCallbacks = [];
function animate(time) {
animationCallbacks.forEach((callback) => callback(time));
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
function animateStatusIndeterminate(ms) {
const i = Math.floor((ms / INDETERMINATE_STATUS_STEP_MS) % 8);
if (statusIndeterminate.children[i].style.borderTopColor === '') {
Array.prototype.slice.call(statusIndeterminate.children).forEach((child) => {
child.style.borderTopColor = '';
});
statusIndeterminate.children[i].style.borderTopColor = '#dfdfdf';
}
}
function setStatusMode(mode) {
if (statusMode === mode || !initializing) {
return;
}
[statusProgress, statusIndeterminate, statusNotice].forEach((elem) => {
elem.style.display = 'none';
});
animationCallbacks = animationCallbacks.filter(function (value) {
return (value !== animateStatusIndeterminate);
});
switch (mode) {
case 'progress':
statusProgress.style.display = 'block';
break;
case 'indeterminate':
statusIndeterminate.style.display = 'block';
animationCallbacks.push(animateStatusIndeterminate);
break;
case 'notice':
statusNotice.style.display = 'block';
break;
case 'hidden':
break;
default:
throw new Error('Invalid status mode');
}
statusMode = mode;
}
function setStatusNotice(text) {
while (statusNotice.lastChild) {
statusNotice.removeChild(statusNotice.lastChild);
}
const lines = text.split('\n');
lines.forEach((line) => {
statusNotice.appendChild(document.createTextNode(line));
statusNotice.appendChild(document.createElement('br'));
});
}
function displayFailureNotice(err) {
const msg = err.message || err;
console.error(msg);
setStatusNotice(msg);
setStatusMode('notice');
initializing = false;
}
const missing = Engine.getMissingFeatures();
if (missing.length !== 0) {
const missingMsg = 'Error\nThe following features required to run Godot projects on the Web are missing:\n';
displayFailureNotice(missingMsg + missing.join('\n'));
} else {
setStatusMode('indeterminate');
engine.startGame({
'onProgress': function (current, total) {
if (total > 0) {
statusProgressInner.style.width = `${(current / total) * 100}%`;
setStatusMode('progress');
if (current === total) {
// wait for progress bar animation
setTimeout(() => {
setStatusMode('indeterminate');
}, 500);
}
} else {
setStatusMode('indeterminate');
}
},
}).then(() => {
setStatusMode('hidden');
initializing = false;
}, displayFailureNotice);
}
}());
</script>
</body>
</html>

@ -20,9 +20,9 @@ abstract class AbstractTest extends ApiTestCase
self::bootKernel(); self::bootKernel();
} }
private static function generateAuthToken(string $authName) private static function generateAuthToken(string $authName, string $seedKey)
{ {
$sign_seed = sodium_base642bin($_ENV['AUTH_SEED'], SODIUM_BASE64_VARIANT_ORIGINAL); $sign_seed = sodium_base642bin($_ENV[$seedKey], SODIUM_BASE64_VARIANT_ORIGINAL);
$sign_pair = sodium_crypto_sign_seed_keypair($sign_seed); $sign_pair = sodium_crypto_sign_seed_keypair($sign_seed);
$sign_secret = sodium_crypto_sign_secretkey($sign_pair); $sign_secret = sodium_crypto_sign_secretkey($sign_pair);
$now = new \DateTimeImmutable("now", new DateTimeZone("UTC")); $now = new \DateTimeImmutable("now", new DateTimeZone("UTC"));
@ -31,13 +31,13 @@ abstract class AbstractTest extends ApiTestCase
return sodium_bin2base64(sodium_crypto_sign($message, $sign_secret), SODIUM_BASE64_VARIANT_URLSAFE); return sodium_bin2base64(sodium_crypto_sign($message, $sign_secret), SODIUM_BASE64_VARIANT_URLSAFE);
} }
protected static function createClientWithToken($authName = "FooMan"): Client protected static function createClientWithToken($authName = "FooMan", string $seedKey = 'AUTH_SEED'): Client
{ {
return static::createClient([], return static::createClient([],
[ [
'headers' => [ 'headers' => [
'accept' => 'application/json', 'accept' => 'application/json',
'X-AUTH-TOKEN' => static::generateAuthToken($authName) 'X-AUTH-TOKEN' => static::generateAuthToken($authName, $seedKey)
] ]
]); ]);
} }

@ -0,0 +1,62 @@
<?php
namespace App\Tests;
use App\Factory\UserFactory;
class UserTest extends AbstractTest
{
/**
* Requirement: A user should be able to update his properties
*/
public function testUpdateUserProperties(): void
{
$requestUser = UserFactory::createOne();
$response = static::createClientWithToken($requestUser->authName)->request('PATCH', $this->getIri($requestUser),
[
'headers' => [
'content-type' => 'application/merge-patch+json'
],
'json' => [
'authName' => 'foo.bar',
'properties' => [
'a' => 1,
'b' => true,
'c' => null,
'd' => 'text'
]
]
]);
$this->assertResponseStatusCodeSame(200);
$this->assertEquals(1, $response->toArray()['properties']['a']);
$this->assertEquals(true, $response->toArray()['properties']['b']);
$this->assertEquals(null, $response->toArray()['properties']['c']);
$this->assertEquals('text', $response->toArray()['properties']['d']);
}
/**
* Requirement: A user should NOT be able to update his authName
*/
public function testUpdateUserAuthnameNotPossible(): void
{
$requestUser = UserFactory::createOne();
$response = static::createClientWithToken($requestUser->authName)->request('PATCH', $this->getIri($requestUser),
[
'headers' => [
'content-type' => 'application/merge-patch+json'
],
'json' => [
'authName' => 'foo.bar'
]
]);
$this->assertResponseStatusCodeSame(200);
$this->assertNotEquals("foo.bar", $response->toArray()['authName']);
}
}
Loading…
Cancel
Save