feat: add the Nexus Archive project
This commit is contained in:
parent
01d274b404
commit
641baae559
|
@ -0,0 +1,10 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
max_line_length = 80
|
||||
tab_width = 2
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"webextensions": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "script"
|
||||
},
|
||||
"root": true,
|
||||
"parser": "@babel/eslint-parser",
|
||||
"rules": {
|
||||
"curly": ["error", "all"],
|
||||
"eqeqeq": ["error", "always"],
|
||||
"indent": [
|
||||
"error",
|
||||
2
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"never",
|
||||
{
|
||||
"beforeStatementContinuationChars": "never"
|
||||
}
|
||||
],
|
||||
"strict": [
|
||||
"error",
|
||||
"global"
|
||||
],
|
||||
"yoda": [
|
||||
"error",
|
||||
"always"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
* text=auto
|
||||
*.css text eol=lf
|
||||
*.editorconfig text eol=lf
|
||||
*.html text eol=lf
|
||||
*.js text eol=lf
|
||||
*.json text eol=lf
|
||||
*.md text eol=lf
|
||||
*.txt text eol=lf
|
||||
*.png binary
|
|
@ -0,0 +1,3 @@
|
|||
/node_modules
|
||||
/web-ext-artifacts
|
||||
/.web-extension-id
|
|
@ -0,0 +1,13 @@
|
|||
# Version 1.1.1
|
||||
|
||||
- Fix version definition in manifest file
|
||||
|
||||
# Version 1.1.0
|
||||
|
||||
- Removed unnecessary entry for "unlimited storage" from required permissions
|
||||
- Removed unnecessary properties from submitted data
|
||||
- Removed some forgotten debugging code
|
||||
|
||||
# Version 1.0.0
|
||||
|
||||
- Created basic extension to automatically submit network traffic to the website
|
|
@ -0,0 +1,287 @@
|
|||
EUROPEAN UNION PUBLIC LICENCE v. 1.2
|
||||
EUPL © the European Union 2007, 2016
|
||||
|
||||
This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined
|
||||
below) which is provided under the terms of this Licence. Any use of the Work,
|
||||
other than as authorised under this Licence is prohibited (to the extent such
|
||||
use is covered by a right of the copyright holder of the Work).
|
||||
|
||||
The Work is provided under the terms of this Licence when the Licensor (as
|
||||
defined below) has placed the following notice immediately following the
|
||||
copyright notice for the Work:
|
||||
|
||||
Licensed under the EUPL
|
||||
|
||||
or has expressed by any other means his willingness to license under the EUPL.
|
||||
|
||||
1. Definitions
|
||||
|
||||
In this Licence, the following terms have the following meaning:
|
||||
|
||||
- ‘The Licence’: this Licence.
|
||||
|
||||
- ‘The Original Work’: the work or software distributed or communicated by the
|
||||
Licensor under this Licence, available as Source Code and also as Executable
|
||||
Code as the case may be.
|
||||
|
||||
- ‘Derivative Works’: the works or software that could be created by the
|
||||
Licensee, based upon the Original Work or modifications thereof. This Licence
|
||||
does not define the extent of modification or dependence on the Original Work
|
||||
required in order to classify a work as a Derivative Work; this extent is
|
||||
determined by copyright law applicable in the country mentioned in Article 15.
|
||||
|
||||
- ‘The Work’: the Original Work or its Derivative Works.
|
||||
|
||||
- ‘The Source Code’: the human-readable form of the Work which is the most
|
||||
convenient for people to study and modify.
|
||||
|
||||
- ‘The Executable Code’: any code which has generally been compiled and which is
|
||||
meant to be interpreted by a computer as a program.
|
||||
|
||||
- ‘The Licensor’: the natural or legal person that distributes or communicates
|
||||
the Work under the Licence.
|
||||
|
||||
- ‘Contributor(s)’: any natural or legal person who modifies the Work under the
|
||||
Licence, or otherwise contributes to the creation of a Derivative Work.
|
||||
|
||||
- ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of
|
||||
the Work under the terms of the Licence.
|
||||
|
||||
- ‘Distribution’ or ‘Communication’: any act of selling, giving, lending,
|
||||
renting, distributing, communicating, transmitting, or otherwise making
|
||||
available, online or offline, copies of the Work or providing access to its
|
||||
essential functionalities at the disposal of any other natural or legal
|
||||
person.
|
||||
|
||||
2. Scope of the rights granted by the Licence
|
||||
|
||||
The Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
|
||||
sublicensable licence to do the following, for the duration of copyright vested
|
||||
in the Original Work:
|
||||
|
||||
- use the Work in any circumstance and for all usage,
|
||||
- reproduce the Work,
|
||||
- modify the Work, and make Derivative Works based upon the Work,
|
||||
- communicate to the public, including the right to make available or display
|
||||
the Work or copies thereof to the public and perform publicly, as the case may
|
||||
be, the Work,
|
||||
- distribute the Work or copies thereof,
|
||||
- lend and rent the Work or copies thereof,
|
||||
- sublicense rights in the Work or copies thereof.
|
||||
|
||||
Those rights can be exercised on any media, supports and formats, whether now
|
||||
known or later invented, as far as the applicable law permits so.
|
||||
|
||||
In the countries where moral rights apply, the Licensor waives his right to
|
||||
exercise his moral right to the extent allowed by law in order to make effective
|
||||
the licence of the economic rights here above listed.
|
||||
|
||||
The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to
|
||||
any patents held by the Licensor, to the extent necessary to make use of the
|
||||
rights granted on the Work under this Licence.
|
||||
|
||||
3. Communication of the Source Code
|
||||
|
||||
The Licensor may provide the Work either in its Source Code form, or as
|
||||
Executable Code. If the Work is provided as Executable Code, the Licensor
|
||||
provides in addition a machine-readable copy of the Source Code of the Work
|
||||
along with each copy of the Work that the Licensor distributes or indicates, in
|
||||
a notice following the copyright notice attached to the Work, a repository where
|
||||
the Source Code is easily and freely accessible for as long as the Licensor
|
||||
continues to distribute or communicate the Work.
|
||||
|
||||
4. Limitations on copyright
|
||||
|
||||
Nothing in this Licence is intended to deprive the Licensee of the benefits from
|
||||
any exception or limitation to the exclusive rights of the rights owners in the
|
||||
Work, of the exhaustion of those rights or of other applicable limitations
|
||||
thereto.
|
||||
|
||||
5. Obligations of the Licensee
|
||||
|
||||
The grant of the rights mentioned above is subject to some restrictions and
|
||||
obligations imposed on the Licensee. Those obligations are the following:
|
||||
|
||||
Attribution right: The Licensee shall keep intact all copyright, patent or
|
||||
trademarks notices and all notices that refer to the Licence and to the
|
||||
disclaimer of warranties. The Licensee must include a copy of such notices and a
|
||||
copy of the Licence with every copy of the Work he/she distributes or
|
||||
communicates. The Licensee must cause any Derivative Work to carry prominent
|
||||
notices stating that the Work has been modified and the date of modification.
|
||||
|
||||
Copyleft clause: If the Licensee distributes or communicates copies of the
|
||||
Original Works or Derivative Works, this Distribution or Communication will be
|
||||
done under the terms of this Licence or of a later version of this Licence
|
||||
unless the Original Work is expressly distributed only under this version of the
|
||||
Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee
|
||||
(becoming Licensor) cannot offer or impose any additional terms or conditions on
|
||||
the Work or Derivative Work that alter or restrict the terms of the Licence.
|
||||
|
||||
Compatibility clause: If the Licensee Distributes or Communicates Derivative
|
||||
Works or copies thereof based upon both the Work and another work licensed under
|
||||
a Compatible Licence, this Distribution or Communication can be done under the
|
||||
terms of this Compatible Licence. For the sake of this clause, ‘Compatible
|
||||
Licence’ refers to the licences listed in the appendix attached to this Licence.
|
||||
Should the Licensee's obligations under the Compatible Licence conflict with
|
||||
his/her obligations under this Licence, the obligations of the Compatible
|
||||
Licence shall prevail.
|
||||
|
||||
Provision of Source Code: When distributing or communicating copies of the Work,
|
||||
the Licensee will provide a machine-readable copy of the Source Code or indicate
|
||||
a repository where this Source will be easily and freely available for as long
|
||||
as the Licensee continues to distribute or communicate the Work.
|
||||
|
||||
Legal Protection: This Licence does not grant permission to use the trade names,
|
||||
trademarks, service marks, or names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the copyright notice.
|
||||
|
||||
6. Chain of Authorship
|
||||
|
||||
The original Licensor warrants that the copyright in the Original Work granted
|
||||
hereunder is owned by him/her or licensed to him/her and that he/she has the
|
||||
power and authority to grant the Licence.
|
||||
|
||||
Each Contributor warrants that the copyright in the modifications he/she brings
|
||||
to the Work are owned by him/her or licensed to him/her and that he/she has the
|
||||
power and authority to grant the Licence.
|
||||
|
||||
Each time You accept the Licence, the original Licensor and subsequent
|
||||
Contributors grant You a licence to their contributions to the Work, under the
|
||||
terms of this Licence.
|
||||
|
||||
7. Disclaimer of Warranty
|
||||
|
||||
The Work is a work in progress, which is continuously improved by numerous
|
||||
Contributors. It is not a finished work and may therefore contain defects or
|
||||
‘bugs’ inherent to this type of development.
|
||||
|
||||
For the above reason, the Work is provided under the Licence on an ‘as is’ basis
|
||||
and without warranties of any kind concerning the Work, including without
|
||||
limitation merchantability, fitness for a particular purpose, absence of defects
|
||||
or errors, accuracy, non-infringement of intellectual property rights other than
|
||||
copyright as stated in Article 6 of this Licence.
|
||||
|
||||
This disclaimer of warranty is an essential part of the Licence and a condition
|
||||
for the grant of any rights to the Work.
|
||||
|
||||
8. Disclaimer of Liability
|
||||
|
||||
Except in the cases of wilful misconduct or damages directly caused to natural
|
||||
persons, the Licensor will in no event be liable for any direct or indirect,
|
||||
material or moral, damages of any kind, arising out of the Licence or of the use
|
||||
of the Work, including without limitation, damages for loss of goodwill, work
|
||||
stoppage, computer failure or malfunction, loss of data or any commercial
|
||||
damage, even if the Licensor has been advised of the possibility of such damage.
|
||||
However, the Licensor will be liable under statutory product liability laws as
|
||||
far such laws apply to the Work.
|
||||
|
||||
9. Additional agreements
|
||||
|
||||
While distributing the Work, You may choose to conclude an additional agreement,
|
||||
defining obligations or services consistent with this Licence. However, if
|
||||
accepting obligations, You may act only on your own behalf and on your sole
|
||||
responsibility, not on behalf of the original Licensor or any other Contributor,
|
||||
and only if You agree to indemnify, defend, and hold each Contributor harmless
|
||||
for any liability incurred by, or claims asserted against such Contributor by
|
||||
the fact You have accepted any warranty or additional liability.
|
||||
|
||||
10. Acceptance of the Licence
|
||||
|
||||
The provisions of this Licence can be accepted by clicking on an icon ‘I agree’
|
||||
placed under the bottom of a window displaying the text of this Licence or by
|
||||
affirming consent in any other similar way, in accordance with the rules of
|
||||
applicable law. Clicking on that icon indicates your clear and irrevocable
|
||||
acceptance of this Licence and all of its terms and conditions.
|
||||
|
||||
Similarly, you irrevocably accept this Licence and all of its terms and
|
||||
conditions by exercising any rights granted to You by Article 2 of this Licence,
|
||||
such as the use of the Work, the creation by You of a Derivative Work or the
|
||||
Distribution or Communication by You of the Work or copies thereof.
|
||||
|
||||
11. Information to the public
|
||||
|
||||
In case of any Distribution or Communication of the Work by means of electronic
|
||||
communication by You (for example, by offering to download the Work from a
|
||||
remote location) the distribution channel or media (for example, a website) must
|
||||
at least provide to the public the information requested by the applicable law
|
||||
regarding the Licensor, the Licence and the way it may be accessible, concluded,
|
||||
stored and reproduced by the Licensee.
|
||||
|
||||
12. Termination of the Licence
|
||||
|
||||
The Licence and the rights granted hereunder will terminate automatically upon
|
||||
any breach by the Licensee of the terms of the Licence.
|
||||
|
||||
Such a termination will not terminate the licences of any person who has
|
||||
received the Work from the Licensee under the Licence, provided such persons
|
||||
remain in full compliance with the Licence.
|
||||
|
||||
13. Miscellaneous
|
||||
|
||||
Without prejudice of Article 9 above, the Licence represents the complete
|
||||
agreement between the Parties as to the Work.
|
||||
|
||||
If any provision of the Licence is invalid or unenforceable under applicable
|
||||
law, this will not affect the validity or enforceability of the Licence as a
|
||||
whole. Such provision will be construed or reformed so as necessary to make it
|
||||
valid and enforceable.
|
||||
|
||||
The European Commission may publish other linguistic versions or new versions of
|
||||
this Licence or updated versions of the Appendix, so far this is required and
|
||||
reasonable, without reducing the scope of the rights granted by the Licence. New
|
||||
versions of the Licence will be published with a unique version number.
|
||||
|
||||
All linguistic versions of this Licence, approved by the European Commission,
|
||||
have identical value. Parties can take advantage of the linguistic version of
|
||||
their choice.
|
||||
|
||||
14. Jurisdiction
|
||||
|
||||
Without prejudice to specific agreement between parties,
|
||||
|
||||
- any litigation resulting from the interpretation of this License, arising
|
||||
between the European Union institutions, bodies, offices or agencies, as a
|
||||
Licensor, and any Licensee, will be subject to the jurisdiction of the Court
|
||||
of Justice of the European Union, as laid down in article 272 of the Treaty on
|
||||
the Functioning of the European Union,
|
||||
|
||||
- any litigation arising between other parties and resulting from the
|
||||
interpretation of this License, will be subject to the exclusive jurisdiction
|
||||
of the competent court where the Licensor resides or conducts its primary
|
||||
business.
|
||||
|
||||
15. Applicable Law
|
||||
|
||||
Without prejudice to specific agreement between parties,
|
||||
|
||||
- this Licence shall be governed by the law of the European Union Member State
|
||||
where the Licensor has his seat, resides or has his registered office,
|
||||
|
||||
- this licence shall be governed by Belgian law if the Licensor has no seat,
|
||||
residence or registered office inside a European Union Member State.
|
||||
|
||||
Appendix
|
||||
|
||||
‘Compatible Licences’ according to Article 5 EUPL are:
|
||||
|
||||
- GNU General Public License (GPL) v. 2, v. 3
|
||||
- GNU Affero General Public License (AGPL) v. 3
|
||||
- Open Software License (OSL) v. 2.1, v. 3.0
|
||||
- Eclipse Public License (EPL) v. 1.0
|
||||
- CeCILL v. 2.0, v. 2.1
|
||||
- Mozilla Public Licence (MPL) v. 2
|
||||
- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
|
||||
- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for
|
||||
works other than software
|
||||
- European Union Public Licence (EUPL) v. 1.1, v. 1.2
|
||||
- Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong
|
||||
Reciprocity (LiLiQ-R+).
|
||||
|
||||
The European Commission may update this Appendix to later versions of the above
|
||||
licences without producing a new version of the EUPL, as long as they provide
|
||||
the rights granted in Article 2 of this Licence and protect the covered Source
|
||||
Code from exclusive appropriation.
|
||||
|
||||
All other changes or additions to this Appendix require the production of a new
|
||||
EUPL version.
|
|
@ -0,0 +1,21 @@
|
|||
# Nexus Archive browser extension
|
||||
|
||||
A simple browser extension to submit network traffic to the <q>Nexus Archive</q>
|
||||
website.
|
||||
|
||||
## Licence
|
||||
|
||||
This project is licensed under [European Union Public Licence (EUPL)][EUPL].
|
||||
|
||||
For convenience an English text of the licence is included
|
||||
in [LICENSE.txt](LICENSE.txt) file.
|
||||
|
||||
## Changelog
|
||||
|
||||
You can read changelog in a [separate file][CHANGELOG].
|
||||
|
||||
[EUPL]:
|
||||
https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
|
||||
|
||||
[CHANGELOG]:
|
||||
CHANGELOG.md
|
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"author": "Krzysztof Andrzej Sikorski",
|
||||
"background": {
|
||||
"persistent": true,
|
||||
"scripts": [
|
||||
"src/preferences.js",
|
||||
"src/nexusData.js",
|
||||
"src/nexusDataQueue.js",
|
||||
"src/nexusDataSender.js",
|
||||
"src/webRequestMonitor.js",
|
||||
"src/background.js"
|
||||
]
|
||||
},
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "nexus-archive-tracker@zerozero.pl"
|
||||
}
|
||||
},
|
||||
"description": "A browser extension to automatically submit Nexus Clash network traffic to the \"Nexus Archive\" website.",
|
||||
"homepage_url": "https://discord.gg/zBVwzD3f8v",
|
||||
"icons": {
|
||||
"64": "icons/icon.png"
|
||||
},
|
||||
"manifest_version": 2,
|
||||
"name": "Nexus Archive Tracker",
|
||||
"options_ui": {
|
||||
"browser_style": true,
|
||||
"page": "options/index.html"
|
||||
},
|
||||
"permissions": [
|
||||
"storage",
|
||||
"webRequest",
|
||||
"webRequestBlocking",
|
||||
"*://*.nexusclash.com/*"
|
||||
],
|
||||
"short_name": "NA Tracker",
|
||||
"version": "1.1.1"
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en-GB">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Options - Nexus Archive Tracker</title>
|
||||
<link href="style.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<form id="optionsForm" method="post">
|
||||
<fieldset>
|
||||
<legend>Tracker options</legend>
|
||||
<p>
|
||||
<label>
|
||||
User access token:
|
||||
<input name="userAccessToken" required size="64" type="text" pattern="[0-9A-Fa-f]+">
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
Submit form's URL:
|
||||
<input name="trackerSubmitUrl" required size="64" type="url">
|
||||
</label>
|
||||
</p>
|
||||
</fieldset>
|
||||
<p>
|
||||
<button type="submit">Save</button>
|
||||
</p>
|
||||
</form>
|
||||
<script src="../src/preferences.js"></script>
|
||||
<script src="../src/options.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,3 @@
|
|||
body {
|
||||
padding: 0.5em 2ex;
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"babel": {},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.3",
|
||||
"@babel/eslint-parser": "^7.21.3",
|
||||
"eslint": "^8.36.0",
|
||||
"request": "^2.88.2",
|
||||
"web-ext": "^7.5.0"
|
||||
},
|
||||
"webExt": {
|
||||
"ignoreFiles": [
|
||||
"package.json",
|
||||
"package-lock.json"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
/* global NexusDataQueue, NexusDataSender, Preferences, WebRequestMonitor */
|
||||
'use strict'
|
||||
|
||||
const preferences = new Preferences()
|
||||
preferences.load()
|
||||
preferences.listenForStorageChanges()
|
||||
|
||||
const nexusDataQueue = new NexusDataQueue()
|
||||
|
||||
const nexusDataSender = new NexusDataSender(preferences)
|
||||
|
||||
const webRequestMonitor = new WebRequestMonitor(nexusDataQueue, nexusDataSender)
|
||||
webRequestMonitor.addListeners()
|
|
@ -0,0 +1,81 @@
|
|||
/* exported NexusData */
|
||||
'use strict'
|
||||
|
||||
class NexusData {
|
||||
constructor() {
|
||||
this._requestId = null
|
||||
this._requestStartedAt = null
|
||||
this._responseCompletedAt = null
|
||||
this._method = null
|
||||
this._url = null
|
||||
this._formData = null
|
||||
this._responseBodyParts = []
|
||||
}
|
||||
|
||||
get requestId() {
|
||||
return this._requestId
|
||||
}
|
||||
|
||||
set requestId(value) {
|
||||
this._requestId = value
|
||||
}
|
||||
|
||||
get requestStartedAt() {
|
||||
return this._requestStartedAt
|
||||
}
|
||||
|
||||
set requestStartedAt(value) {
|
||||
if (!(value instanceof Date)) {
|
||||
value = new Date(value)
|
||||
}
|
||||
this._requestStartedAt = value
|
||||
}
|
||||
|
||||
get responseCompletedAt() {
|
||||
return this._responseCompletedAt
|
||||
}
|
||||
|
||||
set responseCompletedAt(value) {
|
||||
if (!(value instanceof Date)) {
|
||||
value = new Date(value)
|
||||
}
|
||||
this._responseCompletedAt = value
|
||||
}
|
||||
|
||||
get method() {
|
||||
return this._method
|
||||
}
|
||||
|
||||
set method(value) {
|
||||
this._method = value
|
||||
}
|
||||
|
||||
get url() {
|
||||
return this._url
|
||||
}
|
||||
|
||||
set url(value) {
|
||||
this._url = value
|
||||
}
|
||||
|
||||
get formData() {
|
||||
return this._formData
|
||||
}
|
||||
|
||||
set formData(value) {
|
||||
this._formData = value
|
||||
}
|
||||
|
||||
get responseBody() {
|
||||
return this._responseBodyParts.join('')
|
||||
}
|
||||
|
||||
set responseBody(value) {
|
||||
this._responseBodyParts = []
|
||||
this.appendResponseBodyPart(value)
|
||||
}
|
||||
|
||||
appendResponseBodyPart(value) {
|
||||
this._responseBodyParts.push(value)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/* exported NexusDataQueue */
|
||||
/* global NexusData */
|
||||
'use strict'
|
||||
|
||||
class NexusDataQueue {
|
||||
constructor() {
|
||||
this._data = new Map()
|
||||
}
|
||||
|
||||
push(nexusData) {
|
||||
if (!(nexusData instanceof NexusData)) {
|
||||
window.console.error('[NexusDataQueue] push: argument is not an instance of NexusData!')
|
||||
}
|
||||
this._data.set(nexusData.requestId, nexusData)
|
||||
}
|
||||
|
||||
get(requestId) {
|
||||
if (this._data.has(requestId)) {
|
||||
return this._data.get(requestId)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
has(requestId) {
|
||||
return this._data.has(requestId)
|
||||
}
|
||||
|
||||
delete(requestId) {
|
||||
if (this._data.has(requestId)) {
|
||||
this._data.delete(requestId)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/* exported NexusDataSender */
|
||||
/* global NexusData, Preferences */
|
||||
'use strict'
|
||||
|
||||
class NexusDataSender {
|
||||
constructor(preferences) {
|
||||
if (!(preferences instanceof Preferences)) {
|
||||
window.console.error('[NexusDataSender] constructor: argument is not an instance of Preferences!')
|
||||
}
|
||||
this._preferences = preferences
|
||||
}
|
||||
|
||||
_formatDate(value) {
|
||||
return value instanceof Date ? value.toISOString() : null
|
||||
}
|
||||
|
||||
send(nexusData) {
|
||||
if (!(nexusData instanceof NexusData)) {
|
||||
window.console.error('[NexusDataSender] send: argument is not an instance of NexusData!')
|
||||
}
|
||||
|
||||
if (false === this._preferences.isConfigured()) {
|
||||
window.console.error('[NexusDataSender] send: preferences are not configured!')
|
||||
return
|
||||
}
|
||||
|
||||
const jsonData = {
|
||||
requestStartedAt: this._formatDate(nexusData.requestStartedAt),
|
||||
responseCompletedAt: this._formatDate(nexusData.responseCompletedAt),
|
||||
method: nexusData.method,
|
||||
url: nexusData.url,
|
||||
formData: nexusData.formData,
|
||||
responseBody: nexusData.responseBody,
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('userAccessToken', this._preferences.userAccessToken)
|
||||
formData.append('jsonData', JSON.stringify(jsonData, null, 2))
|
||||
|
||||
const fetchOptions = {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
mode: 'no-cors',
|
||||
}
|
||||
|
||||
window.fetch(this._preferences.trackerSubmitUrl, fetchOptions).catch(
|
||||
error => {
|
||||
window.console.error(`[NexusDataSender] Failed to send data: ${error}`)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/* global Preferences */
|
||||
'use strict'
|
||||
|
||||
const preferences = new Preferences()
|
||||
|
||||
const getOptionsForm = () => document.getElementById('optionsForm')
|
||||
|
||||
const saveForm = event => {
|
||||
event.preventDefault()
|
||||
|
||||
const optionsForm = getOptionsForm()
|
||||
preferences.userAccessToken = optionsForm.elements['userAccessToken'].value
|
||||
preferences.trackerSubmitUrl = optionsForm.elements['trackerSubmitUrl'].value
|
||||
preferences.save()
|
||||
}
|
||||
|
||||
const initForm = () => {
|
||||
const optionsForm = getOptionsForm()
|
||||
optionsForm.addEventListener('submit', saveForm)
|
||||
|
||||
preferences.load().then(
|
||||
() => {
|
||||
optionsForm.elements['userAccessToken'].value = preferences.userAccessToken
|
||||
optionsForm.elements['trackerSubmitUrl'].value = preferences.trackerSubmitUrl
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initForm)
|
|
@ -0,0 +1,93 @@
|
|||
/* exported Preferences */
|
||||
'use strict'
|
||||
|
||||
class Preferences {
|
||||
constructor() {
|
||||
this._userAccessToken = null
|
||||
this._trackerSubmitUrl = null
|
||||
}
|
||||
|
||||
get userAccessToken() {
|
||||
return this._userAccessToken
|
||||
}
|
||||
|
||||
set userAccessToken(value) {
|
||||
this._userAccessToken = value
|
||||
}
|
||||
|
||||
get trackerSubmitUrl() {
|
||||
return this._trackerSubmitUrl
|
||||
}
|
||||
|
||||
set trackerSubmitUrl(value) {
|
||||
this._trackerSubmitUrl = value
|
||||
}
|
||||
|
||||
get _storage() {
|
||||
return browser.storage.sync
|
||||
}
|
||||
|
||||
hasUserAccessToken() {
|
||||
return null !== this._userAccessToken && 0 < this._userAccessToken.length
|
||||
}
|
||||
|
||||
hasTrackerSubmitUrl() {
|
||||
return null !== this._trackerSubmitUrl && 0 < this._trackerSubmitUrl.length
|
||||
}
|
||||
|
||||
isConfigured() {
|
||||
return this.hasUserAccessToken() && this.hasTrackerSubmitUrl()
|
||||
}
|
||||
|
||||
load() {
|
||||
const storageGetter = this._storage.get(null)
|
||||
|
||||
storageGetter.then(
|
||||
results => {
|
||||
if (Object.prototype.hasOwnProperty.call(results, 'userAccessToken')) {
|
||||
this.userAccessToken = results.userAccessToken
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(results, 'trackerSubmitUrl')) {
|
||||
this.trackerSubmitUrl = results.trackerSubmitUrl
|
||||
}
|
||||
},
|
||||
error => {
|
||||
const message = `Error loading preferences: ${error}`
|
||||
window.console.error(message)
|
||||
window.alert(message)
|
||||
}
|
||||
)
|
||||
|
||||
return storageGetter
|
||||
}
|
||||
|
||||
save() {
|
||||
const storageSetter = this._storage.set({
|
||||
userAccessToken: this.userAccessToken,
|
||||
trackerSubmitUrl: this.trackerSubmitUrl
|
||||
})
|
||||
|
||||
storageSetter.catch(
|
||||
error => {
|
||||
const message = `Error saving preferences: ${error}`
|
||||
window.console.error(message)
|
||||
window.alert(message)
|
||||
}
|
||||
)
|
||||
|
||||
return storageSetter
|
||||
}
|
||||
|
||||
listenForStorageChanges() {
|
||||
browser.storage.onChanged.addListener((changes, areaName) => {
|
||||
if ('sync' === areaName) {
|
||||
if (Object.prototype.hasOwnProperty.call(changes, 'userAccessToken')) {
|
||||
this.userAccessToken = changes.userAccessToken.newValue
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(changes, 'trackerSubmitUrl')) {
|
||||
this.trackerSubmitUrl = changes.trackerSubmitUrl.newValue
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
/* exported WebRequestMonitor */
|
||||
/* global NexusData, NexusDataQueue, NexusDataSender */
|
||||
'use strict'
|
||||
|
||||
class WebRequestMonitor {
|
||||
constructor(nexusDataQueue, nexusDataSender) {
|
||||
if (!(nexusDataQueue instanceof NexusDataQueue)) {
|
||||
window.console.error('[NexusDataSender] constructor: argument is not an instance of NexusDataQueue!')
|
||||
}
|
||||
if (!(nexusDataSender instanceof NexusDataSender)) {
|
||||
window.console.error('[NexusDataSender] constructor: argument is not an instance of NexusDataSender!')
|
||||
}
|
||||
this._nexusDataQueue = nexusDataQueue
|
||||
this._nexusDataSender = nexusDataSender
|
||||
}
|
||||
|
||||
_attachResponseDataFilter(nexusData) {
|
||||
const decoder = new TextDecoder('utf-8')
|
||||
const encoder = new TextEncoder()
|
||||
const responseDataFilter = browser.webRequest.filterResponseData(nexusData.requestId)
|
||||
responseDataFilter.ondata = event => {
|
||||
const bodyPart = decoder.decode(event.data, {stream: true})
|
||||
responseDataFilter.write(encoder.encode(bodyPart))
|
||||
nexusData.appendResponseBodyPart(bodyPart)
|
||||
}
|
||||
responseDataFilter.onstop = event => {
|
||||
const bodyPart = decoder.decode(event.data, {stream: false})
|
||||
responseDataFilter.write(encoder.encode(bodyPart))
|
||||
responseDataFilter.close()
|
||||
nexusData.appendResponseBodyPart(bodyPart)
|
||||
}
|
||||
}
|
||||
|
||||
_onBeforeRequest(details) {
|
||||
const nexusData = new NexusData()
|
||||
nexusData.requestId = details.requestId
|
||||
nexusData.requestStartedAt = details.timeStamp
|
||||
nexusData.method = details.method
|
||||
nexusData.url = details.url
|
||||
if (
|
||||
null !== details.requestBody &&
|
||||
Object.prototype.hasOwnProperty.call(details.requestBody, 'formData')
|
||||
) {
|
||||
nexusData.formData = details.requestBody.formData
|
||||
}
|
||||
this._nexusDataQueue.push(nexusData)
|
||||
this._attachResponseDataFilter(nexusData)
|
||||
}
|
||||
|
||||
_onCompleted(details) {
|
||||
const requestId = details.requestId
|
||||
if (false !== this._nexusDataQueue.has(requestId)) {
|
||||
const nexusData = this._nexusDataQueue.get(requestId)
|
||||
nexusData.responseCompletedAt = details.timeStamp
|
||||
this._nexusDataQueue.delete(requestId)
|
||||
this._nexusDataSender.send(nexusData)
|
||||
}
|
||||
}
|
||||
|
||||
_onErrorOccurred(details) {
|
||||
this._nexusDataQueue.delete(details.requestId)
|
||||
window.console.error('[WebRequestMonitor] Error has occurred: ' + details.error)
|
||||
}
|
||||
|
||||
addListeners() {
|
||||
const requestFilters = {
|
||||
types: ['main_frame'],
|
||||
urls: [
|
||||
'*://*.nexusclash.com/*'
|
||||
]
|
||||
}
|
||||
browser.webRequest.onBeforeRequest.addListener(
|
||||
this._onBeforeRequest.bind(this),
|
||||
requestFilters,
|
||||
['blocking', 'requestBody']
|
||||
)
|
||||
browser.webRequest.onCompleted.addListener(
|
||||
this._onCompleted.bind(this),
|
||||
requestFilters
|
||||
)
|
||||
browser.webRequest.onErrorOccurred.addListener(
|
||||
this._onErrorOccurred.bind(this),
|
||||
requestFilters
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = LF
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
max_line_length = 80
|
||||
|
||||
[*.html]
|
||||
indent_size = 4
|
||||
|
||||
[*.php]
|
||||
# settings required by PSR-12 standard
|
||||
end_of_line = LF
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
max_line_length = 120
|
||||
|
||||
[*.twig]
|
||||
indent_size = 4
|
||||
|
||||
[*.yaml]
|
||||
indent_size = 4
|
|
@ -0,0 +1,12 @@
|
|||
# essential framework settings
|
||||
APP_ENV=dev
|
||||
APP_SECRET=
|
||||
APP_WORKER_PARSER_BATCH_SIZE=1
|
||||
APP_WORKER_PARSER_MAX_ITERATIONS=1
|
||||
APP_WORKER_PARSER_MAX_DURATION="1 second"
|
||||
|
||||
# doctrine settings
|
||||
DATABASE_URL="postgresql://symfony:ChangeMe@127.0.0.1:5432/app?serverVersion=13&charset=utf8"
|
||||
|
||||
# lock component settings
|
||||
LOCK_DSN=semaphore
|
|
@ -0,0 +1,8 @@
|
|||
/.env.local
|
||||
/.env.local.php
|
||||
/.env.*.local
|
||||
/config/secrets/prod/prod.decrypt.private.php
|
||||
/node_modules/
|
||||
/public/bundles/
|
||||
/var/
|
||||
/vendor/
|
|
@ -0,0 +1,41 @@
|
|||
# Version 1.0.0
|
||||
|
||||
- create interfaces and classes to represent leaderboard table as seen in game
|
||||
- create Doctrine entities to persist these leaderboard tables in database
|
||||
- implement general parser infrastructure to handle stored page views
|
||||
- implement parser for Breath 4 final leaderboards
|
||||
- implement a public page for browsing leaderboards
|
||||
- create and apply basic UI theme/styling, based on Tailwind CSS
|
||||
- some more internal code cleanups
|
||||
|
||||
# Version 0.5.0
|
||||
|
||||
- fix crash in token command on empty username input
|
||||
- create basic admin panel
|
||||
- convert all config files to PHP format
|
||||
- general code cleanup and refactoring
|
||||
- update installed dependencies to newer versions
|
||||
- update Symfony recipes metadata, port changes to appropriate files
|
||||
- start using Doctrine Migrations: create initial migration for existing tables
|
||||
|
||||
# Version 0.4.0
|
||||
|
||||
- remove unnecessary properties from NexusRawData
|
||||
|
||||
# Version 0.3.0
|
||||
|
||||
- remove NexusRequestLog entity and all related code (form, repository, etc)
|
||||
|
||||
# Version 0.2.0
|
||||
|
||||
- create new form to handle data format expected from browser extension
|
||||
|
||||
# Version 0.1.1
|
||||
|
||||
- fix parsing of JSON fields at cost of ignoring JSON syntax errors
|
||||
|
||||
# Version 0.1.0
|
||||
|
||||
- basic submit form to store request logs for further processing
|
||||
- primitive authentication via access tokens
|
||||
- simple console commands to create user accounts and access tokens
|
|
@ -0,0 +1,287 @@
|
|||
EUROPEAN UNION PUBLIC LICENCE v. 1.2
|
||||
EUPL © the European Union 2007, 2016
|
||||
|
||||
This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined
|
||||
below) which is provided under the terms of this Licence. Any use of the Work,
|
||||
other than as authorised under this Licence is prohibited (to the extent such
|
||||
use is covered by a right of the copyright holder of the Work).
|
||||
|
||||
The Work is provided under the terms of this Licence when the Licensor (as
|
||||
defined below) has placed the following notice immediately following the
|
||||
copyright notice for the Work:
|
||||
|
||||
Licensed under the EUPL
|
||||
|
||||
or has expressed by any other means his willingness to license under the EUPL.
|
||||
|
||||
1. Definitions
|
||||
|
||||
In this Licence, the following terms have the following meaning:
|
||||
|
||||
- ‘The Licence’: this Licence.
|
||||
|
||||
- ‘The Original Work’: the work or software distributed or communicated by the
|
||||
Licensor under this Licence, available as Source Code and also as Executable
|
||||
Code as the case may be.
|
||||
|
||||
- ‘Derivative Works’: the works or software that could be created by the
|
||||
Licensee, based upon the Original Work or modifications thereof. This Licence
|
||||
does not define the extent of modification or dependence on the Original Work
|
||||
required in order to classify a work as a Derivative Work; this extent is
|
||||
determined by copyright law applicable in the country mentioned in Article 15.
|
||||
|
||||
- ‘The Work’: the Original Work or its Derivative Works.
|
||||
|
||||
- ‘The Source Code’: the human-readable form of the Work which is the most
|
||||
convenient for people to study and modify.
|
||||
|
||||
- ‘The Executable Code’: any code which has generally been compiled and which is
|
||||
meant to be interpreted by a computer as a program.
|
||||
|
||||
- ‘The Licensor’: the natural or legal person that distributes or communicates
|
||||
the Work under the Licence.
|
||||
|
||||
- ‘Contributor(s)’: any natural or legal person who modifies the Work under the
|
||||
Licence, or otherwise contributes to the creation of a Derivative Work.
|
||||
|
||||
- ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of
|
||||
the Work under the terms of the Licence.
|
||||
|
||||
- ‘Distribution’ or ‘Communication’: any act of selling, giving, lending,
|
||||
renting, distributing, communicating, transmitting, or otherwise making
|
||||
available, online or offline, copies of the Work or providing access to its
|
||||
essential functionalities at the disposal of any other natural or legal
|
||||
person.
|
||||
|
||||
2. Scope of the rights granted by the Licence
|
||||
|
||||
The Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
|
||||
sublicensable licence to do the following, for the duration of copyright vested
|
||||
in the Original Work:
|
||||
|
||||
- use the Work in any circumstance and for all usage,
|
||||
- reproduce the Work,
|
||||
- modify the Work, and make Derivative Works based upon the Work,
|
||||
- communicate to the public, including the right to make available or display
|
||||
the Work or copies thereof to the public and perform publicly, as the case may
|
||||
be, the Work,
|
||||
- distribute the Work or copies thereof,
|
||||
- lend and rent the Work or copies thereof,
|
||||
- sublicense rights in the Work or copies thereof.
|
||||
|
||||
Those rights can be exercised on any media, supports and formats, whether now
|
||||
known or later invented, as far as the applicable law permits so.
|
||||
|
||||
In the countries where moral rights apply, the Licensor waives his right to
|
||||
exercise his moral right to the extent allowed by law in order to make effective
|
||||
the licence of the economic rights here above listed.
|
||||
|
||||
The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to
|
||||
any patents held by the Licensor, to the extent necessary to make use of the
|
||||
rights granted on the Work under this Licence.
|
||||
|
||||
3. Communication of the Source Code
|
||||
|
||||
The Licensor may provide the Work either in its Source Code form, or as
|
||||
Executable Code. If the Work is provided as Executable Code, the Licensor
|
||||
provides in addition a machine-readable copy of the Source Code of the Work
|
||||
along with each copy of the Work that the Licensor distributes or indicates, in
|
||||
a notice following the copyright notice attached to the Work, a repository where
|
||||
the Source Code is easily and freely accessible for as long as the Licensor
|
||||
continues to distribute or communicate the Work.
|
||||
|
||||
4. Limitations on copyright
|
||||
|
||||
Nothing in this Licence is intended to deprive the Licensee of the benefits from
|
||||
any exception or limitation to the exclusive rights of the rights owners in the
|
||||
Work, of the exhaustion of those rights or of other applicable limitations
|
||||
thereto.
|
||||
|
||||
5. Obligations of the Licensee
|
||||
|
||||
The grant of the rights mentioned above is subject to some restrictions and
|
||||
obligations imposed on the Licensee. Those obligations are the following:
|
||||
|
||||
Attribution right: The Licensee shall keep intact all copyright, patent or
|
||||
trademarks notices and all notices that refer to the Licence and to the
|
||||
disclaimer of warranties. The Licensee must include a copy of such notices and a
|
||||
copy of the Licence with every copy of the Work he/she distributes or
|
||||
communicates. The Licensee must cause any Derivative Work to carry prominent
|
||||
notices stating that the Work has been modified and the date of modification.
|
||||
|
||||
Copyleft clause: If the Licensee distributes or communicates copies of the
|
||||
Original Works or Derivative Works, this Distribution or Communication will be
|
||||
done under the terms of this Licence or of a later version of this Licence
|
||||
unless the Original Work is expressly distributed only under this version of the
|
||||
Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee
|
||||
(becoming Licensor) cannot offer or impose any additional terms or conditions on
|
||||
the Work or Derivative Work that alter or restrict the terms of the Licence.
|
||||
|
||||
Compatibility clause: If the Licensee Distributes or Communicates Derivative
|
||||
Works or copies thereof based upon both the Work and another work licensed under
|
||||
a Compatible Licence, this Distribution or Communication can be done under the
|
||||
terms of this Compatible Licence. For the sake of this clause, ‘Compatible
|
||||
Licence’ refers to the licences listed in the appendix attached to this Licence.
|
||||
Should the Licensee's obligations under the Compatible Licence conflict with
|
||||
his/her obligations under this Licence, the obligations of the Compatible
|
||||
Licence shall prevail.
|
||||
|
||||
Provision of Source Code: When distributing or communicating copies of the Work,
|
||||
the Licensee will provide a machine-readable copy of the Source Code or indicate
|
||||
a repository where this Source will be easily and freely available for as long
|
||||
as the Licensee continues to distribute or communicate the Work.
|
||||
|
||||
Legal Protection: This Licence does not grant permission to use the trade names,
|
||||
trademarks, service marks, or names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the copyright notice.
|
||||
|
||||
6. Chain of Authorship
|
||||
|
||||
The original Licensor warrants that the copyright in the Original Work granted
|
||||
hereunder is owned by him/her or licensed to him/her and that he/she has the
|
||||
power and authority to grant the Licence.
|
||||
|
||||
Each Contributor warrants that the copyright in the modifications he/she brings
|
||||
to the Work are owned by him/her or licensed to him/her and that he/she has the
|
||||
power and authority to grant the Licence.
|
||||
|
||||
Each time You accept the Licence, the original Licensor and subsequent
|
||||
Contributors grant You a licence to their contributions to the Work, under the
|
||||
terms of this Licence.
|
||||
|
||||
7. Disclaimer of Warranty
|
||||
|
||||
The Work is a work in progress, which is continuously improved by numerous
|
||||
Contributors. It is not a finished work and may therefore contain defects or
|
||||
‘bugs’ inherent to this type of development.
|
||||
|
||||
For the above reason, the Work is provided under the Licence on an ‘as is’ basis
|
||||
and without warranties of any kind concerning the Work, including without
|
||||
limitation merchantability, fitness for a particular purpose, absence of defects
|
||||
or errors, accuracy, non-infringement of intellectual property rights other than
|
||||
copyright as stated in Article 6 of this Licence.
|
||||
|
||||
This disclaimer of warranty is an essential part of the Licence and a condition
|
||||
for the grant of any rights to the Work.
|
||||
|
||||
8. Disclaimer of Liability
|
||||
|
||||
Except in the cases of wilful misconduct or damages directly caused to natural
|
||||
persons, the Licensor will in no event be liable for any direct or indirect,
|
||||
material or moral, damages of any kind, arising out of the Licence or of the use
|
||||
of the Work, including without limitation, damages for loss of goodwill, work
|
||||
stoppage, computer failure or malfunction, loss of data or any commercial
|
||||
damage, even if the Licensor has been advised of the possibility of such damage.
|
||||
However, the Licensor will be liable under statutory product liability laws as
|
||||
far such laws apply to the Work.
|
||||
|
||||
9. Additional agreements
|
||||
|
||||
While distributing the Work, You may choose to conclude an additional agreement,
|
||||
defining obligations or services consistent with this Licence. However, if
|
||||
accepting obligations, You may act only on your own behalf and on your sole
|
||||
responsibility, not on behalf of the original Licensor or any other Contributor,
|
||||
and only if You agree to indemnify, defend, and hold each Contributor harmless
|
||||
for any liability incurred by, or claims asserted against such Contributor by
|
||||
the fact You have accepted any warranty or additional liability.
|
||||
|
||||
10. Acceptance of the Licence
|
||||
|
||||
The provisions of this Licence can be accepted by clicking on an icon ‘I agree’
|
||||
placed under the bottom of a window displaying the text of this Licence or by
|
||||
affirming consent in any other similar way, in accordance with the rules of
|
||||
applicable law. Clicking on that icon indicates your clear and irrevocable
|
||||
acceptance of this Licence and all of its terms and conditions.
|
||||
|
||||
Similarly, you irrevocably accept this Licence and all of its terms and
|
||||
conditions by exercising any rights granted to You by Article 2 of this Licence,
|
||||
such as the use of the Work, the creation by You of a Derivative Work or the
|
||||
Distribution or Communication by You of the Work or copies thereof.
|
||||
|
||||
11. Information to the public
|
||||
|
||||
In case of any Distribution or Communication of the Work by means of electronic
|
||||
communication by You (for example, by offering to download the Work from a
|
||||
remote location) the distribution channel or media (for example, a website) must
|
||||
at least provide to the public the information requested by the applicable law
|
||||
regarding the Licensor, the Licence and the way it may be accessible, concluded,
|
||||
stored and reproduced by the Licensee.
|
||||
|
||||
12. Termination of the Licence
|
||||
|
||||
The Licence and the rights granted hereunder will terminate automatically upon
|
||||
any breach by the Licensee of the terms of the Licence.
|
||||
|
||||
Such a termination will not terminate the licences of any person who has
|
||||
received the Work from the Licensee under the Licence, provided such persons
|
||||
remain in full compliance with the Licence.
|
||||
|
||||
13. Miscellaneous
|
||||
|
||||
Without prejudice of Article 9 above, the Licence represents the complete
|
||||
agreement between the Parties as to the Work.
|
||||
|
||||
If any provision of the Licence is invalid or unenforceable under applicable
|
||||
law, this will not affect the validity or enforceability of the Licence as a
|
||||
whole. Such provision will be construed or reformed so as necessary to make it
|
||||
valid and enforceable.
|
||||
|
||||
The European Commission may publish other linguistic versions or new versions of
|
||||
this Licence or updated versions of the Appendix, so far this is required and
|
||||
reasonable, without reducing the scope of the rights granted by the Licence. New
|
||||
versions of the Licence will be published with a unique version number.
|
||||
|
||||
All linguistic versions of this Licence, approved by the European Commission,
|
||||
have identical value. Parties can take advantage of the linguistic version of
|
||||
their choice.
|
||||
|
||||
14. Jurisdiction
|
||||
|
||||
Without prejudice to specific agreement between parties,
|
||||
|
||||
- any litigation resulting from the interpretation of this License, arising
|
||||
between the European Union institutions, bodies, offices or agencies, as a
|
||||
Licensor, and any Licensee, will be subject to the jurisdiction of the Court
|
||||
of Justice of the European Union, as laid down in article 272 of the Treaty on
|
||||
the Functioning of the European Union,
|
||||
|
||||
- any litigation arising between other parties and resulting from the
|
||||
interpretation of this License, will be subject to the exclusive jurisdiction
|
||||
of the competent court where the Licensor resides or conducts its primary
|
||||
business.
|
||||
|
||||
15. Applicable Law
|
||||
|
||||
Without prejudice to specific agreement between parties,
|
||||
|
||||
- this Licence shall be governed by the law of the European Union Member State
|
||||
where the Licensor has his seat, resides or has his registered office,
|
||||
|
||||
- this licence shall be governed by Belgian law if the Licensor has no seat,
|
||||
residence or registered office inside a European Union Member State.
|
||||
|
||||
Appendix
|
||||
|
||||
‘Compatible Licences’ according to Article 5 EUPL are:
|
||||
|
||||
- GNU General Public License (GPL) v. 2, v. 3
|
||||
- GNU Affero General Public License (AGPL) v. 3
|
||||
- Open Software License (OSL) v. 2.1, v. 3.0
|
||||
- Eclipse Public License (EPL) v. 1.0
|
||||
- CeCILL v. 2.0, v. 2.1
|
||||
- Mozilla Public Licence (MPL) v. 2
|
||||
- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
|
||||
- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for
|
||||
works other than software
|
||||
- European Union Public Licence (EUPL) v. 1.1, v. 1.2
|
||||
- Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong
|
||||
Reciprocity (LiLiQ-R+).
|
||||
|
||||
The European Commission may update this Appendix to later versions of the above
|
||||
licences without producing a new version of the EUPL, as long as they provide
|
||||
the rights granted in Article 2 of this Licence and protect the covered Source
|
||||
Code from exclusive appropriation.
|
||||
|
||||
All other changes or additions to this Appendix require the production of a new
|
||||
EUPL version.
|
|
@ -0,0 +1,77 @@
|
|||
# Nexus Archive
|
||||
|
||||
The <q>Nexus Archive</q> website, based on Symfony framework.
|
||||
|
||||
## Licence
|
||||
|
||||
This project is licensed under [European Union Public Licence (EUPL)][EUPL].
|
||||
|
||||
For convenience an English text of the licence is included
|
||||
in [LICENSE.txt](./LICENSE.txt) file.
|
||||
|
||||
## Repositories
|
||||
|
||||
Source code is primarily hosted
|
||||
on [my private Git server](https://git.zerozero.pl/nexus-archive), but for
|
||||
convenience and redundancy it is also mirrored to a few popular code hosting
|
||||
portals:
|
||||
|
||||
- [Gitlab mirror](https://gitlab.com/krzysztof-sikorski/nexus-archive)
|
||||
- [GitHub mirror](https://github.com/krzysztof-sikorski/nexus-archive)
|
||||
- [Launchpad mirror](https://git.launchpad.net/nexus-archive)
|
||||
|
||||
## Installation and deployment
|
||||
|
||||
This is a standard Symfony-based web application, requiring only a standard
|
||||
software stack of:
|
||||
|
||||
- an http server (e.g. Nginx)
|
||||
- PHP binaries and some standard extensions (
|
||||
see [composer.json file](./composer.json) for details)
|
||||
- [Composer][Composer] tool (for fetching and installing third-party PHP
|
||||
libraries)
|
||||
- a relational database server supporting SQL language (e.g. PostgreSQL)
|
||||
|
||||
You can find some generic advice in Symfony documentation,
|
||||
in [installation][SymfonyInstallation]
|
||||
and [deployment][SymfonyDeployment] chapters.
|
||||
|
||||
The application was only tested on PostgreSQL, but it should theoretically work
|
||||
on any database engine that is supported by Doctrine library.
|
||||
Check [Doctrine documentation][DoctrineVendors] for details.
|
||||
|
||||
On Linux Mint (and probably also Ubuntu or Debian) you can use following
|
||||
commands to install required system packages:
|
||||
|
||||
```shell
|
||||
sudo apt-get install php-cli php-fpm postgresql # basic packages
|
||||
sudo apt-get install php-xml php-mbstring php-intl php-xml # required or recommended by Symfony
|
||||
sudo apt-get install php-pgsql # required by application design
|
||||
```
|
||||
|
||||
Remember to also configure periodic execution of following console commands
|
||||
(e.g. via cron jobs or systemd timers):
|
||||
|
||||
- `bin/console app:worker:parser` for parsing submitted data
|
||||
- `bin/console app:worker:prune-database` for pruning unwanted rows from db
|
||||
|
||||
## Development notes
|
||||
|
||||
- some classes are loaded from `var\cache` directory, so you have to
|
||||
execute `bin/console cache:warmup` to have them available for IDE
|
||||
autocompletion
|
||||
|
||||
[EUPL]:
|
||||
https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
|
||||
|
||||
[Composer]:
|
||||
https://getcomposer.org/
|
||||
|
||||
[SymfonyInstallation]:
|
||||
https://symfony.com/doc/current/setup.html
|
||||
|
||||
[SymfonyDeployment]:
|
||||
https://symfony.com/doc/current/deployment.html
|
||||
|
||||
[DoctrineVendors]:
|
||||
https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/introduction.html
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://nexus-archive.zerozero.pl/submit-json",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"requestStartedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"responseCompletedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"method": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"GET",
|
||||
"POST"
|
||||
]
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"formData": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"responseBody": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"requestStartedAt",
|
||||
"responseCompletedAt",
|
||||
"method",
|
||||
"url",
|
||||
"formData",
|
||||
"responseBody"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
a {
|
||||
@apply tw-text-blue-900;
|
||||
}
|
||||
|
||||
a[rel="external"]:after {
|
||||
content: ' ↗';
|
||||
}
|
||||
|
||||
button[type="submit"] {
|
||||
@apply tw-px-4 tw-py-2 tw-font-bold tw-text-white tw-bg-black tw-rounded-lg;
|
||||
}
|
||||
|
||||
body > * {
|
||||
@apply tw-mx-8 tw-my-4;
|
||||
}
|
||||
|
||||
header {
|
||||
@apply tw-flex;
|
||||
}
|
||||
|
||||
header a {
|
||||
@apply tw-text-white;
|
||||
}
|
||||
|
||||
header menu {
|
||||
@apply tw-flex
|
||||
tw-text-xl tw-font-bold tw-leading-loose
|
||||
tw-text-white tw-bg-black
|
||||
tw-border-solid tw-rounded-xl
|
||||
tw-divide-x-4 tw-divide-dotted tw-divide-white
|
||||
tw-shadow-lg;
|
||||
}
|
||||
|
||||
header menu > li {
|
||||
@apply tw-px-8 tw-py-2;
|
||||
}
|
||||
|
||||
main h1 {
|
||||
@apply tw-text-4xl tw-font-bold tw-leading-loose;
|
||||
}
|
||||
|
||||
table {
|
||||
@apply tw-my-4 tw-border-collapse;
|
||||
}
|
||||
|
||||
table th, table td {
|
||||
@apply tw-border-solid tw-border-2 tw-border-black tw-px-4 tw-py-2;
|
||||
}
|
||||
|
||||
section.leaderboard-grid {
|
||||
@apply tw-grid tw-gap-x-8 tw-gap-y-4
|
||||
tw-grid-cols-1 md:tw-grid-cols-2 xl:tw-grid-cols-3;
|
||||
}
|
||||
|
||||
section.leaderboard-grid caption {
|
||||
@apply tw-text-lg tw-font-bold;
|
||||
}
|
||||
|
||||
section.leaderboard-grid article.error {
|
||||
@apply tw-my-4 tw-text-red-500 tw-text-lg tw-font-bold;
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Kernel;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
|
||||
if (!is_file(dirname(__DIR__) . '/vendor/autoload_runtime.php')) {
|
||||
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
|
||||
}
|
||||
|
||||
require_once dirname(__DIR__) . '/vendor/autoload_runtime.php';
|
||||
|
||||
return function (array $context) {
|
||||
$kernel = new Kernel($context['APP_ENV'], (bool)$context['APP_DEBUG']);
|
||||
|
||||
return new Application($kernel);
|
||||
};
|
|
@ -0,0 +1,2 @@
|
|||
#!/usr/bin/env sh
|
||||
npx tailwindcss --input=assets/tailwindcss/input.css --output=public/css/combined.css --watch
|
|
@ -0,0 +1,2 @@
|
|||
#!/usr/bin/env sh
|
||||
npx tailwindcss --input=assets/tailwindcss/input.css --output=public/css/combined.css --minify
|
|
@ -0,0 +1,113 @@
|
|||
{
|
||||
"name": "krzysztof-sikorski/nexus-archive",
|
||||
"description": "The \"Nexus Archive\" website, based on Symfony framework.",
|
||||
"type": "project",
|
||||
"keywords": [
|
||||
"php",
|
||||
"symfony",
|
||||
"game",
|
||||
"pbbg",
|
||||
"nexus-clash"
|
||||
],
|
||||
"license": "EUPL-1.2",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Krzysztof Andrzej Sikorski",
|
||||
"role": "Main Developer"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"ext-ctype": "*",
|
||||
"ext-dom": "*",
|
||||
"ext-iconv": "*",
|
||||
"ext-intl": "*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-pdo": "*",
|
||||
"ext-pdo_pgsql": "*",
|
||||
"ext-simplexml": "*",
|
||||
"ext-xml": "*",
|
||||
"doctrine/doctrine-bundle": "^2.5",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.2",
|
||||
"doctrine/orm": "^2.10",
|
||||
"easycorp/easyadmin-bundle": "^4.0",
|
||||
"opis/json-schema": "^2.3",
|
||||
"symfony/console": "^6.0",
|
||||
"symfony/css-selector": "6.0.*",
|
||||
"symfony/dom-crawler": "6.0.*",
|
||||
"symfony/dotenv": "^6.0",
|
||||
"symfony/flex": "^v1.18",
|
||||
"symfony/form": "^6.0",
|
||||
"symfony/framework-bundle": "^6.0",
|
||||
"symfony/monolog-bundle": "^3.7",
|
||||
"symfony/password-hasher": "^6.0",
|
||||
"symfony/property-access": "6.0.*",
|
||||
"symfony/proxy-manager-bridge": "^6.0",
|
||||
"symfony/rate-limiter": "6.0.*",
|
||||
"symfony/runtime": "^6.0",
|
||||
"symfony/security-bundle": "^6.0",
|
||||
"symfony/serializer": "^6.0",
|
||||
"symfony/twig-bundle": "^6.0",
|
||||
"symfony/uid": "^6.0",
|
||||
"symfony/validator": "^6.0",
|
||||
"symfony/yaml": "^6.0",
|
||||
"twig/extra-bundle": "^3.3",
|
||||
"twig/twig": "^3.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"roave/security-advisories": "dev-latest",
|
||||
"symfony/debug-bundle": "^6.0",
|
||||
"symfony/stopwatch": "^6.0",
|
||||
"symfony/web-profiler-bundle": "^6.0"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/symfony": "*"
|
||||
},
|
||||
"replace": {
|
||||
"symfony/polyfill-ctype": "*",
|
||||
"symfony/polyfill-iconv": "*",
|
||||
"symfony/polyfill-php72": "*"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"App\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true,
|
||||
"config": {
|
||||
"preferred-install": {
|
||||
"*": "dist"
|
||||
},
|
||||
"optimize-autoloader": true,
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"composer/package-versions-deprecated": true,
|
||||
"symfony/flex": true,
|
||||
"symfony/runtime": true
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"auto-scripts": {
|
||||
"cache:clear": "symfony-cmd",
|
||||
"assets:install %PUBLIC_DIR%": "symfony-cmd"
|
||||
},
|
||||
"post-install-cmd": [
|
||||
"@auto-scripts"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@auto-scripts"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"symfony": {
|
||||
"allow-contrib": false,
|
||||
"require": "6.0.*"
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
||||
EasyCorp\Bundle\EasyAdminBundle\EasyAdminBundle::class => ['all' => true],
|
||||
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
|
||||
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
|
||||
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
||||
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
||||
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
|
||||
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
||||
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
|
||||
];
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Symfony\Config\DebugConfig;
|
||||
|
||||
return static function (DebugConfig $debugConfig) {
|
||||
// Forwards VarDumper Data clones to a centralized server
|
||||
// allowing to inspect dumps on CLI or in your browser.
|
||||
// See the "server:dump" command to start a new server.
|
||||
$debugConfig->dumpDestination(value: 'tcp://%env(VAR_DUMPER_SERVER)%');
|
||||
};
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Symfony\Config\FrameworkConfig;
|
||||
use Symfony\Config\WebProfilerConfig;
|
||||
|
||||
return static function (WebProfilerConfig $webProfilerConfig, FrameworkConfig $frameworkConfig) {
|
||||
$webProfilerConfig->toolbar(value: true);
|
||||
$webProfilerConfig->interceptRedirects(value: false);
|
||||
|
||||
$profilerConfig = $frameworkConfig->profiler();
|
||||
$profilerConfig->onlyExceptions(value: false);
|
||||
};
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Contract\Config\AppParameters;
|
||||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||||
use Symfony\Config\DoctrineConfig;
|
||||
use Symfony\Config\FrameworkConfig;
|
||||
|
||||
use function Symfony\Component\DependencyInjection\Loader\Configurator\env;
|
||||
|
||||
return static function (
|
||||
DoctrineConfig $doctrineConfig,
|
||||
FrameworkConfig $frameworkConfig,
|
||||
ContainerConfigurator $containerConfigurator
|
||||
) {
|
||||
$dbalConfig = $doctrineConfig->dbal();
|
||||
$dbalConfig->defaultConnection(value: AppParameters::DOCTRINE_DEFAULT_CONNECTION_NAME);
|
||||
|
||||
$defaultConnectionConfig = $dbalConfig->connection(name: AppParameters::DOCTRINE_DEFAULT_CONNECTION_NAME);
|
||||
$defaultConnectionConfig->url(value: env(name: 'DATABASE_URL')->resolve());
|
||||
|
||||
$ormConfig = $doctrineConfig->orm();
|
||||
$ormConfig->autoGenerateProxyClasses(value: true);
|
||||
|
||||
$entityManagerConfig = $ormConfig->entityManager(name: AppParameters::DOCTRINE_DEFAULT_CONNECTION_NAME);
|
||||
$entityManagerConfig->namingStrategy(value: 'doctrine.orm.naming_strategy.underscore_number_aware');
|
||||
$entityManagerConfig->autoMapping(value: true);
|
||||
|
||||
$mappingConfig = $entityManagerConfig->mapping(name: 'App');
|
||||
$mappingConfig->dir(value: '%kernel.project_dir%/src/Doctrine/Entity');
|
||||
$mappingConfig->prefix('App\Doctrine\Entity');
|
||||
$mappingConfig->alias(value: 'App');
|
||||
$mappingConfig->isBundle(value: false);
|
||||
|
||||
if ('prod' === $containerConfigurator->env()) {
|
||||
$ormConfig->autoGenerateProxyClasses(value: false);
|
||||
|
||||
$queryCacheDriverConfig = $entityManagerConfig->queryCacheDriver();
|
||||
$queryCacheDriverConfig->type(value: 'pool');
|
||||
$queryCacheDriverConfig->pool(AppParameters::CACHE_POOL_NAME_DOCTRINE_QUERY_CACHE);
|
||||
|
||||
$resultCacheDriverConfig = $entityManagerConfig->resultCacheDriver();
|
||||
$resultCacheDriverConfig->type(value: 'pool');
|
||||
$resultCacheDriverConfig->pool(AppParameters::CACHE_POOL_NAME_DOCTRINE_RESULT_CACHE);
|
||||
|
||||
$cacheConfig = $frameworkConfig->cache();
|
||||
$cacheConfig->pool(name: AppParameters::CACHE_POOL_NAME_DOCTRINE_QUERY_CACHE)->adapters(['cache.system']);
|
||||
$cacheConfig->pool(name: AppParameters::CACHE_POOL_NAME_DOCTRINE_RESULT_CACHE)->adapters(['cache.app']);
|
||||
}
|
||||
|
||||
if ('test' === $containerConfigurator->env()) {
|
||||
// "TEST_TOKEN" is typically set by ParaTest
|
||||
$defaultConnectionConfig->dbnameSuffix('_test' . env(name: 'TEST_TOKEN')->default(''));
|
||||
}
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Symfony\Config\DoctrineMigrationsConfig;
|
||||
|
||||
return static function (DoctrineMigrationsConfig $doctrineMigrationsConfig) {
|
||||
$doctrineMigrationsConfig->migrationsPath(
|
||||
namespace: 'DoctrineMigrations',
|
||||
value: '%kernel.project_dir%/migrations'
|
||||
);
|
||||
$doctrineMigrationsConfig->enableProfiler(value: '%kernel.debug%');
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||||
use Symfony\Config\FrameworkConfig;
|
||||
|
||||
return static function (FrameworkConfig $frameworkConfig, ContainerConfigurator $containerConfigurator) {
|
||||
$frameworkConfig->secret(value: '%env(APP_SECRET)%');
|
||||
$frameworkConfig->httpMethodOverride(value: false);
|
||||
|
||||
$sessionConfig = $frameworkConfig->session();
|
||||
$sessionConfig->storageFactoryId(value: 'session.storage.factory.native');
|
||||
$sessionConfig->handlerId(value: null);
|
||||
$sessionConfig->cookieSecure(value: 'auto');
|
||||
$sessionConfig->cookieSamesite(value: 'lax');
|
||||
|
||||
$phpErrorsConfig = $frameworkConfig->phpErrors();
|
||||
$phpErrorsConfig->log(value: true);
|
||||
|
||||
if ('test' === $containerConfigurator->env()) {
|
||||
$frameworkConfig->test(value: true);
|
||||
$sessionConfig->storageFactoryId(value: 'session.storage.factory.mock_file');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Symfony\Config\FrameworkConfig;
|
||||
|
||||
use function Symfony\Component\DependencyInjection\Loader\Configurator\env;
|
||||
|
||||
return static function (FrameworkConfig $frameworkConfig) {
|
||||
$lockConfig = $frameworkConfig->lock();
|
||||
$lockConfig->enabled(value: true);
|
||||
$lockConfig->resource(name: 'default', value: env(name: 'LOCK_DSN'));
|
||||
};
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Config\MonologConfig;
|
||||
|
||||
return static function (MonologConfig $monologConfig, ContainerConfigurator $containerConfigurator) {
|
||||
// As of Symfony 5.1, deprecations are logged in the dedicated "deprecation" channel when it exists
|
||||
$monologConfig->channels(value: ['deprecation']);
|
||||
|
||||
if ('dev' === $containerConfigurator->env()) {
|
||||
$deprecationHandlerConfig = $monologConfig->handler(name: 'deprecation');
|
||||
$deprecationHandlerConfig->type(value: 'stream');
|
||||
$deprecationHandlerConfig->path(value: '%kernel.logs_dir%/%kernel.environment%.deprecation.log');
|
||||
$deprecationHandlerConfig->channels()->elements(value: ['deprecation']);
|
||||
|
||||
$mainHandlerConfig = $monologConfig->handler(name: 'main');
|
||||
$mainHandlerConfig->type(value: 'stream');
|
||||
$mainHandlerConfig->path(value: '%kernel.logs_dir%/%kernel.environment%.log');
|
||||
$mainHandlerConfig->level(value: 'debug');
|
||||
$mainHandlerConfig->channels()->elements(value: ['!event']);
|
||||
|
||||
$consoleHandlerConfig = $monologConfig->handler(name: 'console');
|
||||
$consoleHandlerConfig->type(value: 'console');
|
||||
$consoleHandlerConfig->processPsr3Messages(value: true);
|
||||
$consoleHandlerConfig->channels()->elements(value: ['!event', '!doctrine', '!console']);
|
||||
}
|
||||
|
||||
if ('prod' === $containerConfigurator->env()) {
|
||||
$mainHandlerConfig = $monologConfig->handler(name: 'main');
|
||||
$mainHandlerConfig->type(value: 'fingers_crossed');
|
||||
$mainHandlerConfig->actionLevel(value: 'error');
|
||||
$mainHandlerConfig->handler(value: 'nested');
|
||||
$mainHandlerConfig->excludedHttpCode()->code(Response::HTTP_NOT_FOUND);
|
||||
$mainHandlerConfig->excludedHttpCode()->code(Response::HTTP_METHOD_NOT_ALLOWED);
|
||||
$mainHandlerConfig->bufferSize(value: 50); // How many messages should be saved? Prevent memory leaks
|
||||
|
||||
$nestedHandlerConfig = $monologConfig->handler(name: 'nested');
|
||||
$nestedHandlerConfig->type(value: 'stream');
|
||||
$nestedHandlerConfig->path(value: 'php://stderr');
|
||||
$nestedHandlerConfig->level(value: 'debug');
|
||||
$nestedHandlerConfig->formatter(value: 'monolog.formatter.json');
|
||||
|
||||
$consoleHandlerConfig = $monologConfig->handler(name: 'console');
|
||||
$consoleHandlerConfig->type(value: 'console');
|
||||
$consoleHandlerConfig->processPsr3Messages(value: false);
|
||||
$consoleHandlerConfig->channels()->elements(value: ['!event', '!doctrine']);
|
||||
}
|
||||
|
||||
if ('test' === $containerConfigurator->env()) {
|
||||
$mainHandlerConfig = $monologConfig->handler(name: 'main');
|
||||
$mainHandlerConfig->type(value: 'fingers_crossed');
|
||||
$mainHandlerConfig->actionLevel(value: 'error');
|
||||
$mainHandlerConfig->handler(value: 'nested');
|
||||
$mainHandlerConfig->excludedHttpCode()->code(Response::HTTP_NOT_FOUND);
|
||||
$mainHandlerConfig->excludedHttpCode()->code(Response::HTTP_METHOD_NOT_ALLOWED);
|
||||
$mainHandlerConfig->channels()->elements(value: ['!event']);
|
||||
|
||||
$nestedHandlerConfig = $monologConfig->handler(name: 'nested');
|
||||
$nestedHandlerConfig->type(value: 'stream');
|
||||
$nestedHandlerConfig->path(value: '%kernel.logs_dir%/%kernel.environment%.log');
|
||||
$nestedHandlerConfig->level(value: 'debug');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||||
use Symfony\Config\FrameworkConfig;
|
||||
|
||||
return static function (FrameworkConfig $frameworkConfig, ContainerConfigurator $containerConfigurator) {
|
||||
$routerConfig = $frameworkConfig->router();
|
||||
$routerConfig->utf8(value: true);
|
||||
|
||||
if ('prod' === $containerConfigurator->env()) {
|
||||
$routerConfig->strictRequirements(value: null);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Contract\Config\AppParameters;
|
||||
use App\Contract\Config\AppRoutes;
|
||||
use App\Contract\UserRoles;
|
||||
use App\Doctrine\Entity\User;
|
||||
use App\EasyAdmin\Controller\DashboardController;
|
||||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Config\SecurityConfig;
|
||||
|
||||
return static function (SecurityConfig $securityConfig, ContainerConfigurator $containerConfigurator) {
|
||||
$securityConfig->enableAuthenticatorManager(value: true);
|
||||
|
||||
$authPasswordHasherConfig = $securityConfig->passwordHasher(class: PasswordAuthenticatedUserInterface::class);
|
||||
$authPasswordHasherConfig->algorithm(value: 'auto');
|
||||
|
||||
$userPasswordHasherConfig = $securityConfig->passwordHasher(class: User::class);
|
||||
$userPasswordHasherConfig->algorithm(value: 'auto');
|
||||
|
||||
$securityConfig->passwordHasher(class: User::class)->algorithm(value: 'auto');
|
||||
|
||||
$entityConfig = $securityConfig->provider(name: AppParameters::SECURITY_USER_PROVIDER_NAME)->entity();
|
||||
$entityConfig->class(User::class);
|
||||
$entityConfig->property(value: AppParameters::SECURITY_USER_ENTITY_ID_FIELD);
|
||||
|
||||
$firewallConfig = $securityConfig->firewall(name: 'dev');
|
||||
$firewallConfig->pattern(value: '^/(_(profiler|wdt)|css|images|js)/');
|
||||
$firewallConfig->security(value: false);
|
||||
|
||||
$firewallConfig = $securityConfig->firewall(name: 'main');
|
||||
$firewallConfig->lazy(value: true);
|
||||
$firewallConfig->provider(value: AppParameters::SECURITY_USER_PROVIDER_NAME);
|
||||
|
||||
$formLoginConfig = $firewallConfig->formLogin();
|
||||
$formLoginConfig->loginPath(value: AppRoutes::LOGIN);
|
||||
$formLoginConfig->checkPath(value: AppRoutes::LOGIN);
|
||||
$formLoginConfig->enableCsrf(value: true);
|
||||
$formLoginConfig->defaultTargetPath(value: AppRoutes::HOME);
|
||||
$formLoginConfig->alwaysUseDefaultTargetPath(value: true);
|
||||
|
||||
$firewallConfig->loginThrottling(); // enable with default values
|
||||
|
||||
$firewallConfig->logout()->path(value: AppRoutes::LOGOUT);
|
||||
|
||||
$accessControlConfig = $securityConfig->accessControl();
|
||||
$accessControlConfig->path(value: DashboardController::ROUTE_SECURITY_REGEXP);
|
||||
$accessControlConfig->roles(value: [UserRoles::ROLE_ADMIN]);
|
||||
|
||||
if ('test' === $containerConfigurator->env()) {
|
||||
// By default, password hashers are resource intensive and take time. This is
|
||||
// important to generate secure password hashes. In tests however, secure hashes
|
||||
// are not important, waste resources and increase test times. The following
|
||||
// reduces the work factor to the lowest possible values.
|
||||
$authPasswordHasherConfig->algorithm(value: 'auto');
|
||||
$authPasswordHasherConfig->cost(value: 4); // Lowest possible value for bcrypt
|
||||
$authPasswordHasherConfig->timeCost(value: 3); // Lowest possible value for argon
|
||||
$authPasswordHasherConfig->memoryCost(value: 10); // Lowest possible value for argon
|
||||
}
|
||||
};
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Symfony\Config\FrameworkConfig;
|
||||
use Symfony\Config\WebProfilerConfig;
|
||||
|
||||
return static function (WebProfilerConfig $webProfilerConfig, FrameworkConfig $frameworkConfig) {
|
||||
$webProfilerConfig->toolbar(value: false);
|
||||
$webProfilerConfig->interceptRedirects(value: false);
|
||||
|
||||
$profilerConfig = $frameworkConfig->profiler();
|
||||
$profilerConfig->collect(value: false);
|
||||
};
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Contract\Config\AppParameters;
|
||||
use Symfony\Config\FrameworkConfig;
|
||||
|
||||
return static function (FrameworkConfig $frameworkConfig) {
|
||||
$frameworkConfig->defaultLocale(value: AppParameters::DEFAULT_LOCALE);
|
||||
|
||||
$translatorConfig = $frameworkConfig->translator();
|
||||
$translatorConfig->defaultPath(value: '%kernel.project_dir%/translations');
|
||||
$translatorConfig->fallbacks(value: [AppParameters::DEFAULT_LOCALE]);
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Service\MainMenuGenerator;
|
||||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||||
use Symfony\Config\TwigConfig;
|
||||
|
||||
use function Symfony\Component\DependencyInjection\Loader\Configurator\service;
|
||||
|
||||
return static function (TwigConfig $twigConfig, ContainerConfigurator $containerConfigurator) {
|
||||
$twigConfig->defaultPath(value: '%kernel.project_dir%/templates');
|
||||
|
||||
$mainMenuConfig = $twigConfig->global(key: 'mainMenuGenerator');
|
||||
$mainMenuConfig->value(value: service(serviceId: MainMenuGenerator::class));
|
||||
|
||||
if ('test' === $containerConfigurator->env()) {
|
||||
$twigConfig->strictVariables(value: true);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||||
use Symfony\Config\FrameworkConfig;
|
||||
|
||||
return static function (FrameworkConfig $frameworkConfig, ContainerConfigurator $containerConfigurator) {
|
||||
$validationConfig = $frameworkConfig->validation();
|
||||
$validationConfig->emailValidationMode(value: 'html5');
|
||||
|
||||
if ('test' === $containerConfigurator->env()) {
|
||||
$validationConfig->notCompromisedPassword()->enabled(value: false);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
if (file_exists(dirname(__DIR__) . '/var/cache/prod/App_KernelProdContainer.preload.php')) {
|
||||
require dirname(__DIR__) . '/var/cache/prod/App_KernelProdContainer.preload.php';
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Contract\Config\AppParameters;
|
||||
use App\Contract\Config\AppRoutes;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
|
||||
|
||||
return static function (RoutingConfigurator $routingConfigurator) {
|
||||
$routingConfigurator->import(
|
||||
resource: __DIR__ . '/../src/Controller/',
|
||||
type: AppParameters::CONFIGURATOR_IMPORT_TYPE_ANNOTATION,
|
||||
);
|
||||
|
||||
$routingConfigurator->import(
|
||||
resource: __DIR__ . '/../src/EasyAdmin/Controller/',
|
||||
type: AppParameters::CONFIGURATOR_IMPORT_TYPE_ANNOTATION,
|
||||
);
|
||||
|
||||
$routingConfigurator->import(
|
||||
resource: __DIR__ . '/../src/Kernel.php',
|
||||
type: AppParameters::CONFIGURATOR_IMPORT_TYPE_ANNOTATION,
|
||||
);
|
||||
|
||||
$routeConfigurator = $routingConfigurator->add(name: AppRoutes::LOGOUT, path: '/logout');
|
||||
$methods = [
|
||||
Request::METHOD_GET,
|
||||
Request::METHOD_POST,
|
||||
];
|
||||
$routeConfigurator->methods(methods: $methods);
|
||||
|
||||
if ('dev' === $routingConfigurator->env()) {
|
||||
$importConfigurator = $routingConfigurator->import(
|
||||
resource: '@FrameworkBundle/Resources/config/routing/errors.xml',
|
||||
);
|
||||
$importConfigurator->prefix(prefix: '/_error');
|
||||
|
||||
$importConfigurator = $routingConfigurator->import(
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml',
|
||||
);
|
||||
$importConfigurator->prefix(prefix: '/_wdt');
|
||||
|
||||
$importConfigurator = $routingConfigurator->import(
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml',
|
||||
);
|
||||
$importConfigurator->prefix(prefix: '/_profiler');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Command\WorkerParseCommand;
|
||||
use App\Contract\Config\AppParameters;
|
||||
use App\Contract\Config\AppTags;
|
||||
use App\Contract\Service\Parser\ParserInterface;
|
||||
use App\Service\ParserSelector;
|
||||
use App\Service\Serializer;
|
||||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
use function Symfony\Component\DependencyInjection\Loader\Configurator\env;
|
||||
use function Symfony\Component\DependencyInjection\Loader\Configurator\tagged_iterator;
|
||||
|
||||
return static function (ContainerConfigurator $containerConfigurator) {
|
||||
$containerConfigurator->import(
|
||||
resource: __DIR__ . '/packages/*.php',
|
||||
type: AppParameters::CONFIGURATOR_IMPORT_TYPE_GLOB,
|
||||
);
|
||||
|
||||
$envConfigDir = __DIR__ . '/packages/' . $containerConfigurator->env();
|
||||
if (is_dir($envConfigDir)) {
|
||||
$containerConfigurator->import(
|
||||
resource: $envConfigDir . '/*.php',
|
||||
type: AppParameters::CONFIGURATOR_IMPORT_TYPE_GLOB,
|
||||
);
|
||||
}
|
||||
|
||||
$servicesConfigurator = $containerConfigurator->services();
|
||||
|
||||
$defaultsConfigurator = $servicesConfigurator->defaults();
|
||||
$defaultsConfigurator->autowire(autowired: true);
|
||||
$defaultsConfigurator->autoconfigure(autoconfigured: true);
|
||||
|
||||
$instanceofConfigurator = $defaultsConfigurator->instanceof(fqcn: ParserInterface::class);
|
||||
$instanceofConfigurator->tag(name: AppTags::PARSER);
|
||||
|
||||
$prototypeConfigurator = $servicesConfigurator->load(
|
||||
namespace: 'App\\',
|
||||
resource: __DIR__ . '/../src/',
|
||||
);
|
||||
$excludes = [
|
||||
__DIR__ . '/../src/DependencyInjection/',
|
||||
__DIR__ . '/../src/Entity/',
|
||||
__DIR__ . '/../src/Kernel.php',
|
||||
];
|
||||
$prototypeConfigurator->exclude(excludes: $excludes);
|
||||
|
||||
$prototypeConfigurator = $servicesConfigurator->load(
|
||||
namespace: 'App\\Controller\\',
|
||||
resource: __DIR__ . '/../src/Controller/',
|
||||
);
|
||||
$prototypeConfigurator->tag(name: 'controller.service_arguments');
|
||||
|
||||
$servicesConfigurator->set(id: Serializer::class)->decorate(id: SerializerInterface::class);
|
||||
|
||||
$serviceConfigurator = $servicesConfigurator->set(id: ParserSelector::class);
|
||||
$serviceConfigurator->arg(key: '$parsers', value: tagged_iterator(tag: AppTags::PARSER));
|
||||
|
||||
$serviceConfigurator = $servicesConfigurator->set(id: WorkerParseCommand::class);
|
||||
$serviceConfigurator->arg(key: '$batchSize', value: env(name: 'APP_WORKER_PARSER_BATCH_SIZE')->int());
|
||||
$serviceConfigurator->arg(key: '$maxIterations', value: env(name: 'APP_WORKER_PARSER_MAX_ITERATIONS')->int());
|
||||
$serviceConfigurator->arg(key: '$maxDurationStr', value: env(name: 'APP_WORKER_PARSER_MAX_DURATION'));
|
||||
};
|
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version0001 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Create user, user_access_token, and nexus_raw_data tables';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// create user table
|
||||
$this->addSql(
|
||||
<<<'SQL'
|
||||
CREATE TABLE "user" (
|
||||
id UUID NOT NULL,
|
||||
created_at TIMESTAMP(0) WITH TIME ZONE NOT NULL,
|
||||
username VARCHAR(180) NOT NULL,
|
||||
roles JSON NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
enabled BOOLEAN NOT NULL,
|
||||
PRIMARY KEY(id)
|
||||
)
|
||||
SQL
|
||||
);
|
||||
$this->addSql('CREATE UNIQUE INDEX username_uniq ON "user" (username)');
|
||||
$this->addSql("COMMENT ON COLUMN \"user\".id IS '(DC2Type:uuid)'");
|
||||
$this->addSql("COMMENT ON COLUMN \"user\".created_at IS '(DC2Type:datetimetz_immutable)'");
|
||||
|
||||
// create user_access_token table
|
||||
$this->addSql(
|
||||
<<<'SQL'
|
||||
CREATE TABLE user_access_token (
|
||||
id UUID NOT NULL,
|
||||
owner_id UUID NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
created_at TIMESTAMP(0) WITH TIME ZONE NOT NULL,
|
||||
valid_until TIMESTAMP(0) WITH TIME ZONE DEFAULT NULL,
|
||||
PRIMARY KEY(id)
|
||||
)
|
||||
SQL
|
||||
);
|
||||
$this->addSql('CREATE INDEX IDX_366EA16A7E3C61F9 ON user_access_token (owner_id)');
|
||||
$this->addSql('CREATE UNIQUE INDEX value_uniq ON user_access_token (value)');
|
||||
$this->addSql("COMMENT ON COLUMN user_access_token.id IS '(DC2Type:uuid)'");
|
||||
$this->addSql("COMMENT ON COLUMN user_access_token.owner_id IS '(DC2Type:uuid)'");
|
||||
$this->addSql("COMMENT ON COLUMN user_access_token.created_at IS '(DC2Type:datetimetz_immutable)'");
|
||||
$this->addSql("COMMENT ON COLUMN user_access_token.valid_until IS '(DC2Type:datetimetz_immutable)'");
|
||||
|
||||
// create nexus_raw_data table
|
||||
$this->addSql(
|
||||
<<<'SQL'
|
||||
CREATE TABLE nexus_raw_data (
|
||||
id UUID NOT NULL,
|
||||
submitter_id UUID NOT NULL,
|
||||
submitted_at TIMESTAMP(0) WITH TIME ZONE NOT NULL,
|
||||
request_started_at TIMESTAMP(0) WITH TIME ZONE NOT NULL,
|
||||
response_completed_at TIMESTAMP(0) WITH TIME ZONE NOT NULL,
|
||||
method TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
form_data JSON DEFAULT NULL,
|
||||
response_body TEXT NOT NULL,
|
||||
PRIMARY KEY(id)
|
||||
)
|
||||
SQL
|
||||
);
|
||||
$this->addSql(
|
||||
'CREATE INDEX nexus_raw_data_sorting_idx ON nexus_raw_data (submitted_at, request_started_at, id)'
|
||||
);
|
||||
$this->addSql('CREATE INDEX nexus_raw_data_submitter_idx ON nexus_raw_data (submitter_id)');
|
||||
$this->addSql("COMMENT ON COLUMN nexus_raw_data.id IS '(DC2Type:uuid)'");
|
||||
$this->addSql("COMMENT ON COLUMN nexus_raw_data.submitter_id IS '(DC2Type:uuid)'");
|
||||
$this->addSql("COMMENT ON COLUMN nexus_raw_data.submitted_at IS '(DC2Type:datetimetz_immutable)'");
|
||||
$this->addSql("COMMENT ON COLUMN nexus_raw_data.request_started_at IS '(DC2Type:datetimetz_immutable)'");
|
||||
$this->addSql("COMMENT ON COLUMN nexus_raw_data.response_completed_at IS '(DC2Type:datetimetz_immutable)'");
|
||||
|
||||
// create foreign keys
|
||||
$this->addSql(
|
||||
<<<'SQL'
|
||||
ALTER TABLE nexus_raw_data ADD CONSTRAINT FK_7BE0EB04919E5513
|
||||
FOREIGN KEY (submitter_id) REFERENCES "user" (id)
|
||||
NOT DEFERRABLE INITIALLY IMMEDIATE
|
||||
SQL
|
||||
);
|
||||
$this->addSql(
|
||||
<<<'SQL'
|
||||
ALTER TABLE user_access_token ADD CONSTRAINT FK_366EA16A7E3C61F9
|
||||
FOREIGN KEY (owner_id) REFERENCES "user" (id)
|
||||
NOT DEFERRABLE INITIALLY IMMEDIATE
|
||||
SQL
|
||||
);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP TABLE nexus_raw_data');
|
||||
$this->addSql('DROP TABLE user_access_token');
|
||||
$this->addSql('DROP TABLE "user"');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version0002 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Make sure all tables have created_at and last_modified_at columns';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE "user" ADD last_modified_at TIMESTAMP(0) WITH TIME ZONE NOT NULL DEFAULT now()');
|
||||
$this->addSql('ALTER TABLE "user" ALTER last_modified_at DROP DEFAULT');
|
||||
$this->addSql("COMMENT ON COLUMN \"user\".last_modified_at IS '(DC2Type:datetimetz_immutable)'");
|
||||
|
||||
$this->addSql(
|
||||
'ALTER TABLE user_access_token ADD last_modified_at TIMESTAMP(0) WITH TIME ZONE NOT NULL DEFAULT now()'
|
||||
);
|
||||
$this->addSql('ALTER TABLE user_access_token ALTER last_modified_at DROP DEFAULT');
|
||||
$this->addSql("COMMENT ON COLUMN user_access_token.last_modified_at IS '(DC2Type:datetimetz_immutable)'");
|
||||
|
||||
$this->addSql('DROP INDEX nexus_raw_data_sorting_idx');
|
||||
$this->addSql('ALTER TABLE nexus_raw_data RENAME COLUMN submitted_at TO created_at');
|
||||
$this->addSql('CREATE INDEX nexus_raw_data_sorting_idx ON nexus_raw_data (created_at, request_started_at, id)');
|
||||
|
||||
$this->addSql(
|
||||
'ALTER TABLE nexus_raw_data ADD last_modified_at TIMESTAMP(0) WITH TIME ZONE NOT NULL DEFAULT now()'
|
||||
);
|
||||
$this->addSql('ALTER TABLE nexus_raw_data ALTER last_modified_at DROP DEFAULT');
|
||||
$this->addSql("COMMENT ON COLUMN nexus_raw_data.last_modified_at IS '(DC2Type:datetimetz_immutable)'");
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP INDEX nexus_raw_data_sorting_idx');
|
||||
$this->addSql('ALTER TABLE nexus_raw_data RENAME created_at TO submitted_at');
|
||||
$this->addSql(
|
||||
'CREATE INDEX nexus_raw_data_sorting_idx ON nexus_raw_data (submitted_at, request_started_at, id)'
|
||||
);
|
||||
|
||||
$this->addSql('ALTER TABLE nexus_raw_data DROP last_modified_at');
|
||||
$this->addSql('ALTER TABLE user_access_token DROP last_modified_at');
|
||||
$this->addSql('ALTER TABLE "user" DROP last_modified_at');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version0003 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Normalize names for indices enforced by Doctrine';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER INDEX username_uniq RENAME TO user_username_uniq');
|
||||
$this->addSql('ALTER INDEX idx_366ea16a7e3c61f9 RENAME TO user_access_token_owner_idx');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER INDEX user_access_token_owner_idx RENAME TO idx_366ea16a7e3c61f9');
|
||||
$this->addSql('ALTER INDEX user_username_uniq RENAME TO username_uniq');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version0004 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Create leaderboard tables';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql(
|
||||
<<<'SQL'
|
||||
CREATE TABLE nexus_leaderboard (
|
||||
id UUID NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
value_title TEXT NOT NULL,
|
||||
career BOOLEAN NOT NULL,
|
||||
created_at TIMESTAMP(0) WITH TIME ZONE NOT NULL,
|
||||
last_modified_at TIMESTAMP(0) WITH TIME ZONE NOT NULL,
|
||||
PRIMARY KEY(id)
|
||||
)
|
||||
SQL
|
||||
);
|
||||
$this->addSql('CREATE UNIQUE INDEX nexus_leaderboard_uniq ON nexus_leaderboard (title)');
|
||||
$this->addSql("COMMENT ON COLUMN nexus_leaderboard.id IS '(DC2Type:uuid)'");
|
||||
$this->addSql("COMMENT ON COLUMN nexus_leaderboard.created_at IS '(DC2Type:datetimetz_immutable)'");
|
||||
$this->addSql("COMMENT ON COLUMN nexus_leaderboard.last_modified_at IS '(DC2Type:datetimetz_immutable)'");
|
||||
|
||||
$this->addSql(
|
||||
<<<'SQL'
|
||||
CREATE TABLE nexus_leaderboard_entry (
|
||||
id UUID NOT NULL,
|
||||
leaderboard_id UUID NOT NULL,
|
||||
character_name TEXT NOT NULL,
|
||||
position INT NOT NULL,
|
||||
value INT NOT NULL,
|
||||
created_at TIMESTAMP(0) WITH TIME ZONE NOT NULL,
|
||||
last_modified_at TIMESTAMP(0) WITH TIME ZONE NOT NULL,
|
||||
PRIMARY KEY(id)
|
||||
)
|
||||
SQL
|
||||
);
|
||||
$this->addSql(
|
||||
'CREATE INDEX nexus_leaderboard_entry_leaderboard_idx ON nexus_leaderboard_entry (leaderboard_id)'
|
||||
);
|
||||
$this->addSql(
|
||||
'CREATE UNIQUE INDEX nexus_leaderboard_entry_uniq ON nexus_leaderboard_entry (position)'
|
||||
);
|
||||
$this->addSql("COMMENT ON COLUMN nexus_leaderboard_entry.id IS '(DC2Type:uuid)'");
|
||||
$this->addSql("COMMENT ON COLUMN nexus_leaderboard_entry.leaderboard_id IS '(DC2Type:uuid)'");
|
||||
$this->addSql("COMMENT ON COLUMN nexus_leaderboard_entry.created_at IS '(DC2Type:datetimetz_immutable)'");
|
||||
$this->addSql("COMMENT ON COLUMN nexus_leaderboard_entry.last_modified_at IS '(DC2Type:datetimetz_immutable)'");
|
||||
$this->addSql(
|
||||
<<<'SQL'
|
||||
ALTER TABLE nexus_leaderboard_entry ADD CONSTRAINT FK_33FD5D095CE067D8
|
||||
FOREIGN KEY (leaderboard_id) REFERENCES nexus_leaderboard (id)
|
||||
NOT DEFERRABLE INITIALLY IMMEDIATE
|
||||
SQL
|
||||
);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP TABLE nexus_leaderboard_entry');
|
||||
$this->addSql('DROP TABLE nexus_leaderboard');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version0005 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add parser-related columns to nexus_raw_data table';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql(
|
||||
<<<'SQL'
|
||||
ALTER TABLE nexus_raw_data
|
||||
ALTER form_data TYPE JSONB,
|
||||
ADD parsed_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL,
|
||||
ADD parser_errors JSONB DEFAULT NULL
|
||||
SQL
|
||||
);
|
||||
$this->addSql("COMMENT ON COLUMN nexus_raw_data.parsed_at IS '(DC2Type:datetime_immutable)'");
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE nexus_raw_data DROP parsed_at, DROP parser_errors, ALTER form_data TYPE JSON');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use App\Contract\Entity\Nexus\GamePeriodIdEnum;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version0006 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Create dictonary table for game periods';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql(
|
||||
<<<'SQL'
|
||||
CREATE TABLE nexus_game_period (
|
||||
id INT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
started_at TIMESTAMP(0) WITH TIME ZONE NOT NULL,
|
||||
completed_at TIMESTAMP(0) WITH TIME ZONE DEFAULT NULL,
|
||||
current BOOLEAN NOT NULL DEFAULT false,
|
||||
PRIMARY KEY(id))
|
||||
SQL
|
||||
);
|
||||
$this->addSql("COMMENT ON COLUMN nexus_game_period.started_at IS '(DC2Type:datetimetz_immutable)'");
|
||||
$this->addSql("COMMENT ON COLUMN nexus_game_period.completed_at IS '(DC2Type:datetimetz_immutable)'");
|
||||
|
||||
$sql = <<<'SQL'
|
||||
INSERT INTO nexus_game_period (id, name, started_at, completed_at, current)
|
||||
VALUES (:id, :name, :startedAt, :completedAt, false)
|
||||
SQL;
|
||||
$params = [
|
||||
'id' => GamePeriodIdEnum::BREATH_4,
|
||||
'name' => 'Breath 3.5 (also known as Breath 4)',
|
||||
'startedAt' => '2015-07-25 00:00:00 UTC',
|
||||
'completedAt' => '2021-11-24 00:00:00 UTC',
|
||||
];
|
||||
$this->addSql(sql: $sql, params: $params);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP TABLE nexus_game_period');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version0007 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Redesign leaderboard tables to support game periods and better match DTOs';
|
||||
}
|
||||
|
||||
private function checkLeaderboardCount(): void
|
||||
{
|
||||
$sql = 'SELECT COUNT(*) FROM nexus_leaderboard';
|
||||
$leaderboardCount = $this->connection->executeQuery(sql: $sql)->fetchOne();
|
||||
$this->abortIf(
|
||||
condition: $leaderboardCount > 0,
|
||||
message: 'This migration can only be executed on empty leaderboard tables!',
|
||||
);
|
||||
}
|
||||
|
||||
public function preUp(Schema $schema): void
|
||||
{
|
||||
$this->checkLeaderboardCount();
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// create table nexus_leaderboard_category
|
||||
$this->addSql(
|
||||
<<<'SQL'
|
||||
CREATE TABLE nexus_leaderboard_category (
|
||||
id UUID NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
score_label TEXT NOT NULL,
|
||||
career BOOLEAN NOT NULL,
|
||||
PRIMARY KEY(id)
|
||||
)
|
||||
SQL
|
||||
);
|
||||
$this->addSql(
|
||||
'CREATE UNIQUE INDEX nexus_leaderboard_category_uniq ON nexus_leaderboard_category (name, career)'
|
||||
);
|
||||
$this->addSql("COMMENT ON COLUMN nexus_leaderboard_category.id IS '(DC2Type:uuid)'");
|
||||
|
||||
// update nexus_leaderboard table
|
||||
$this->addSql('DROP INDEX nexus_leaderboard_uniq');
|
||||
$this->addSql("ALTER TABLE nexus_leaderboard ADD category_id UUID NOT NULL");
|
||||
$this->addSql('ALTER TABLE nexus_leaderboard ADD game_period_id INT NOT NULL');
|
||||
$this->addSql('ALTER TABLE nexus_leaderboard DROP title');
|
||||
$this->addSql('ALTER TABLE nexus_leaderboard DROP value_title');
|
||||
$this->addSql('ALTER TABLE nexus_leaderboard DROP career');
|
||||
$this->addSql("COMMENT ON COLUMN nexus_leaderboard.category_id IS '(DC2Type:uuid)'");
|
||||
$this->addSql(
|
||||
<<<'SQL'
|
||||
ALTER TABLE nexus_leaderboard ADD CONSTRAINT FK_2557F33F6140100F
|
||||
FOREIGN KEY (category_id) REFERENCES nexus_leaderboard_category (id)
|
||||
NOT DEFERRABLE INITIALLY IMMEDIATE
|
||||
SQL
|
||||
);
|
||||
$this->addSql(
|
||||
<<<'SQL'
|
||||
ALTER TABLE nexus_leaderboard ADD CONSTRAINT FK_2557F33F3E2DBBDC
|
||||
FOREIGN KEY (game_period_id) REFERENCES nexus_game_period (id)
|
||||
NOT DEFERRABLE INITIALLY IMMEDIATE
|
||||
SQL
|
||||
);
|
||||
$this->addSql('CREATE INDEX nexus_leaderboard_category_idx ON nexus_leaderboard (category_id)');
|
||||
$this->addSql('CREATE INDEX nexus_leaderboard_game_period_idx ON nexus_leaderboard (game_period_id)');
|
||||
$this->addSql('CREATE UNIQUE INDEX nexus_leaderboard_uniq ON nexus_leaderboard (category_id, game_period_id)');
|
||||
|
||||
// update nexus_leaderboard_entry table
|
||||
$this->addSql('DROP INDEX nexus_leaderboard_entry_uniq');
|
||||
$this->addSql('ALTER TABLE nexus_leaderboard_entry DROP CONSTRAINT nexus_leaderboard_entry_pkey');
|
||||
$this->addSql('ALTER TABLE nexus_leaderboard_entry DROP id');
|
||||
$this->addSql('ALTER TABLE nexus_leaderboard_entry DROP created_at');
|
||||
$this->addSql('ALTER TABLE nexus_leaderboard_entry DROP last_modified_at');
|
||||
$this->addSql('ALTER TABLE nexus_leaderboard_entry RENAME COLUMN value TO score');
|
||||
$this->addSql('ALTER TABLE nexus_leaderboard_entry ADD PRIMARY KEY (leaderboard_id, position)');
|
||||
}
|
||||
|
||||
public function preDown(Schema $schema): void
|
||||
{
|
||||
$this->checkLeaderboardCount();
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// update nexus_leaderboard_entry table
|
||||
$this->addSql('ALTER TABLE nexus_leaderboard_entry DROP CONSTRAINT nexus_leaderboard_entry_pkey');
|
||||
$this->addSql('ALTER TABLE nexus_leaderboard_entry RENAME COLUMN score TO value');
|
||||
$this->addSql('ALTER TABLE nexus_leaderboard_entry ADD id UUID NOT NULL');
|
||||
$this->addSql('ALTER TABLE nexus_leaderboard_entry ADD created_at TIMESTAMP(0) WITH TIME ZONE NOT NULL');
|
||||
$this->addSql('ALTER TABLE nexus_leaderboard_entry ADD last_modified_at TIMESTAMP(0) WITH TIME ZONE NOT NULL');
|
||||
$this->addSql("COMMENT ON COLUMN nexus_leaderboard_entry.id IS '(DC2Type:uuid)'");
|
||||
$this->addSql("COMMENT ON COLUMN nexus_leaderboard_entry.created_at IS '(DC2Type:datetimetz_immutable)'");
|
||||
$this->addSql("COMMENT ON COLUMN nexus_leaderboard_entry.last_modified_at IS '(DC2Type:datetimetz_immutable)'");
|
||||
$this->addSql('CREATE UNIQUE INDEX nexus_leaderboard_entry_uniq ON nexus_leaderboard_entry (position)');
|
||||
$this->addSql('ALTER TABLE nexus_leaderboard_entry ADD PRIMARY KEY (id)');
|
||||
|
||||
// update nexus_leaderboard table
|
||||
$this->addSql('ALTER TABLE nexus_leaderboard DROP category_id');
|
||||
$this->addSql('ALTER TABLE nexus_leaderboard DROP game_period_id');
|
||||
$this->addSql('ALTER TABLE nexus_leaderboard ADD title TEXT NOT NULL');
|
||||
$this->addSql('ALTER TABLE nexus_leaderboard ADD value_title TEXT NOT NULL');
|
||||
$this->addSql('ALTER TABLE nexus_leaderboard ADD career BOOLEAN NOT NULL');
|
||||
$this->addSql('CREATE UNIQUE INDEX nexus_leaderboard_uniq ON nexus_leaderboard (title)');
|
||||
|
||||
// drop table nexus_leaderboard_category
|
||||
$this->addSql('DROP TABLE nexus_leaderboard_category');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version0008 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Rename "nexus_raw_data" table to "page_view", minor redesign of table columns';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE nexus_raw_data RENAME TO page_view');
|
||||
$this->addSql('ALTER TABLE page_view RENAME COLUMN submitter_id TO owner_id');
|
||||
$this->addSql('ALTER TABLE page_view ALTER request_started_at DROP NOT NULL');
|
||||
$this->addSql('ALTER TABLE page_view ALTER response_completed_at DROP NOT NULL');
|
||||
$this->addSql('ALTER INDEX nexus_raw_data_sorting_idx RENAME TO page_view_sorting_idx');
|
||||
$this->addSql('ALTER INDEX nexus_raw_data_submitter_idx RENAME TO page_view_owner_idx');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER INDEX page_view_owner_idx RENAME TO nexus_raw_data_submitter_idx');
|
||||
$this->addSql('ALTER INDEX page_view_sorting_idx RENAME TO nexus_raw_data_sorting_idx');
|
||||
$this->addSql('ALTER TABLE page_view ALTER response_completed_at SET NOT NULL');
|
||||
$this->addSql('ALTER TABLE page_view ALTER request_started_at SET NOT NULL');
|
||||
$this->addSql('ALTER TABLE page_view RENAME COLUMN owner_id TO submitter_id');
|
||||
$this->addSql('ALTER TABLE page_view RENAME TO nexus_raw_data');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version0009 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Fix structure of leaderboard tables to match leaderboard DTOs';
|
||||
}
|
||||
|
||||
private function checkLeaderboardCount(): void
|
||||
{
|
||||
$sql = 'SELECT COUNT(*) FROM nexus_leaderboard_category';
|
||||
$categoryCount = $this->connection->executeQuery(sql: $sql)->fetchOne();
|
||||
|
||||
$sql = 'SELECT COUNT(*) FROM nexus_leaderboard';
|
||||
$leaderboardCount = $this->connection->executeQuery(sql: $sql)->fetchOne();
|
||||
|
||||
$sql = 'SELECT COUNT(*) FROM nexus_leaderboard_entry';
|
||||
$entryCount = $this->connection->executeQuery(sql: $sql)->fetchOne();
|
||||
|
||||
$this->abortIf(
|
||||
condition: ($categoryCount > 0) || ($leaderboardCount > 0) || ($entryCount > 0),
|
||||
message: 'This migration can only be executed on empty leaderboard tables!',
|
||||
);
|
||||
}
|
||||
|
||||
public function preUp(Schema $schema): void
|
||||
{
|
||||
$this->checkLeaderboardCount();
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP INDEX nexus_leaderboard_category_uniq');
|
||||
$this->addSql('ALTER TABLE nexus_leaderboard_category DROP career');
|
||||
$this->addSql('ALTER TABLE nexus_leaderboard_category ADD type TEXT NOT NULL');
|
||||
$this->addSql(
|
||||
'CREATE UNIQUE INDEX nexus_leaderboard_category_uniq ON nexus_leaderboard_category (name, type)'
|
||||
);
|
||||
$this->addSql(
|
||||
'CREATE UNIQUE INDEX nexus_leaderboard_entry_uniq ON nexus_leaderboard_entry (leaderboard_id, position)'
|
||||
);
|
||||
}
|
||||
|
||||
public function preDown(Schema $schema): void
|
||||
{
|
||||
$this->checkLeaderboardCount();
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP INDEX nexus_leaderboard_entry_uniq');
|
||||
$this->addSql('DROP INDEX nexus_leaderboard_category_uniq');
|
||||
$this->addSql('ALTER TABLE nexus_leaderboard_category DROP type');
|
||||
$this->addSql('ALTER TABLE nexus_leaderboard_category ADD career BOOLEAN NOT NULL');
|
||||
$this->addSql(
|
||||
'CREATE UNIQUE INDEX nexus_leaderboard_category_uniq ON nexus_leaderboard_category (name, career)'
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use App\Contract\Entity\Nexus\GamePeriodIdEnum;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
use function array_merge;
|
||||
|
||||
final class Version0010 extends AbstractMigration
|
||||
{
|
||||
private const TS_BREATH_5_LAUNCH = '2021-11-24 00:00:00 UTC';
|
||||
private const TS_BREATH_5_OUTER_PLANES = '2021-12-26 00:00:00 UTC';
|
||||
private const TS_BREATH_5_STRONGHOLDS = '2022-03-06 00:00:00 UTC';
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Insert rows for known game periods (up to B5 Stronghold launch)';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$sql = <<<'SQL'
|
||||
INSERT INTO nexus_game_period (id, name, started_at, completed_at, current)
|
||||
VALUES (:id, :name, :startedAt, :completedAt, :current)
|
||||
SQL;
|
||||
$rows = [
|
||||
GamePeriodIdEnum::BREATH_5_LAUNCH => [
|
||||
'name' => 'Breath 5 (early after launch)',
|
||||
'startedAt' => self::TS_BREATH_5_LAUNCH,
|
||||
'completedAt' => self::TS_BREATH_5_OUTER_PLANES,
|
||||
'current' => false,
|
||||
],
|
||||
GamePeriodIdEnum::BREATH_5_OUTER_PLANES => [
|
||||
'name' => 'Breath 5 (after opening of Outer Planes)',
|
||||
'startedAt' => self::TS_BREATH_5_OUTER_PLANES,
|
||||
'completedAt' => self::TS_BREATH_5_STRONGHOLDS,
|
||||
'current' => false,
|
||||
],
|
||||
GamePeriodIdEnum::BREATH_5_STRONGHOLDS => [
|
||||
'name' => 'Breath 5 (after launch of Strongholds)',
|
||||
'startedAt' => self::TS_BREATH_5_STRONGHOLDS,
|
||||
'completedAt' => null,
|
||||
'current' => true,
|
||||
],
|
||||
];
|
||||
$types = [
|
||||
'name' => Types::TEXT,
|
||||
'startedAt' => Types::TEXT,
|
||||
'completedAt' => Types::TEXT,
|
||||
'current' => Types::BOOLEAN,
|
||||
];
|
||||
foreach ($rows as $id => $row) {
|
||||
$params = array_merge(['id' => $id], $row);
|
||||
$this->addSql(sql: $sql, params: $params, types: $types);
|
||||
}
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql(
|
||||
sql: 'DELETE FROM nexus_game_period WHERE id != :idBreath4',
|
||||
params: ['idBreath4' => GamePeriodIdEnum::BREATH_4]
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,823 @@
|
|||
{
|
||||
"name": "nexus-archive",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"tailwindcss": "^3.2.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "2.0.5",
|
||||
"run-parallel": "^1.1.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.stat": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
||||
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.walk": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
||||
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@nodelib/fs.scandir": "2.1.5",
|
||||
"fastq": "^1.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/forms": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.3.tgz",
|
||||
"integrity": "sha512-y5mb86JUoiUgBjY/o6FJSFZSEttfb3Q5gllE4xoKjAAD+vBrnIhE4dViwUuow3va8mpH4s9jyUbUbrRGoRdc2Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mini-svg-data-uri": "^1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "7.4.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
||||
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-node": {
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz",
|
||||
"integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"acorn": "^7.0.0",
|
||||
"acorn-walk": "^7.0.0",
|
||||
"xtend": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-walk": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz",
|
||||
"integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/anymatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/arg": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
||||
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/camelcase-css": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
|
||||
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar/node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/cssesc": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"cssesc": "bin/cssesc"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/defined": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz",
|
||||
"integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/detective": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz",
|
||||
"integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"acorn-node": "^1.8.2",
|
||||
"defined": "^1.0.0",
|
||||
"minimist": "^1.2.6"
|
||||
},
|
||||
"bin": {
|
||||
"detective": "bin/detective.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/didyoumean": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/dlv": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
|
||||
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.2.12",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
|
||||
"integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "^2.0.2",
|
||||
"@nodelib/fs.walk": "^1.2.3",
|
||||
"glob-parent": "^5.1.2",
|
||||
"merge2": "^1.3.0",
|
||||
"micromatch": "^4.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-glob/node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fastq": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
|
||||
"integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/has": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-core-module": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
|
||||
"integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has": "^1.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lilconfig": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
|
||||
"integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
|
||||
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"braces": "^3.0.2",
|
||||
"picomatch": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mini-svg-data-uri": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
|
||||
"integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"mini-svg-data-uri": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
|
||||
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-hash": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
||||
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/path-parse": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pify": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
||||
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.21",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz",
|
||||
"integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.4",
|
||||
"picocolors": "^1.0.0",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-import": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz",
|
||||
"integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"postcss-value-parser": "^4.0.0",
|
||||
"read-cache": "^1.0.0",
|
||||
"resolve": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-js": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
|
||||
"integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"camelcase-css": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12 || ^14 || >= 16"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.21"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-load-config": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz",
|
||||
"integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lilconfig": "^2.0.5",
|
||||
"yaml": "^1.10.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": ">=8.0.9",
|
||||
"ts-node": ">=9.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"postcss": {
|
||||
"optional": true
|
||||
},
|
||||
"ts-node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-nested": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz",
|
||||
"integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"postcss-selector-parser": "^6.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.2.14"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-selector-parser": {
|
||||
"version": "6.0.11",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz",
|
||||
"integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-value-parser": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/quick-lru": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
|
||||
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"pify": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
|
||||
"integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-core-module": "^2.9.0",
|
||||
"path-parse": "^1.0.7",
|
||||
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"resolve": "bin/resolve"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/reusify": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
||||
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"iojs": ">=1.0.0",
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/run-parallel": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
|
||||
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-preserve-symlinks-flag": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.7.tgz",
|
||||
"integrity": "sha512-B6DLqJzc21x7wntlH/GsZwEXTBttVSl1FtCzC8WP4oBc/NKef7kaax5jeihkkCEWc831/5NDJ9gRNDK6NEioQQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"arg": "^5.0.2",
|
||||
"chokidar": "^3.5.3",
|
||||
"color-name": "^1.1.4",
|
||||
"detective": "^5.2.1",
|
||||
"didyoumean": "^1.2.2",
|
||||
"dlv": "^1.1.3",
|
||||
"fast-glob": "^3.2.12",
|
||||
"glob-parent": "^6.0.2",
|
||||
"is-glob": "^4.0.3",
|
||||
"lilconfig": "^2.0.6",
|
||||
"micromatch": "^4.0.5",
|
||||
"normalize-path": "^3.0.0",
|
||||
"object-hash": "^3.0.0",
|
||||
"picocolors": "^1.0.0",
|
||||
"postcss": "^8.0.9",
|
||||
"postcss-import": "^14.1.0",
|
||||
"postcss-js": "^4.0.0",
|
||||
"postcss-load-config": "^3.1.4",
|
||||
"postcss-nested": "6.0.0",
|
||||
"postcss-selector-parser": "^6.0.11",
|
||||
"postcss-value-parser": "^4.2.0",
|
||||
"quick-lru": "^5.1.1",
|
||||
"resolve": "^1.22.1"
|
||||
},
|
||||
"bin": {
|
||||
"tailwind": "lib/cli.js",
|
||||
"tailwindcss": "lib/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.13.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.0.9"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
|
||||
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"tailwindcss": "^3.2.7"
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Kernel;
|
||||
|
||||
require_once dirname(__DIR__) . '/vendor/autoload_runtime.php';
|
||||
|
||||
return function (array $context) {
|
||||
return new Kernel($context['APP_ENV'], (bool)$context['APP_DEBUG']);
|
||||
};
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Contract\Config\AppSerializationGroups;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\Serializer\Encoder\JsonEncoder;
|
||||
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
abstract class BaseCommand extends Command
|
||||
{
|
||||
public function __construct(protected SerializerInterface $serializer)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function createSymfonyStyle(InputInterface $input, OutputInterface $output): SymfonyStyle
|
||||
{
|
||||
return new SymfonyStyle(input: $input, output: $output);
|
||||
}
|
||||
|
||||
protected function getQuestionHelper(): QuestionHelper
|
||||
{
|
||||
return $this->getHelper(name: 'question');
|
||||
}
|
||||
|
||||
protected function displayValue(
|
||||
SymfonyStyle $io,
|
||||
string $label,
|
||||
mixed $value,
|
||||
?string $serializationGroup = null
|
||||
): void {
|
||||
$context = [];
|
||||
if (null !== $serializationGroup) {
|
||||
$context[ObjectNormalizer::GROUPS] = [
|
||||
AppSerializationGroups::DEFAULT,
|
||||
$serializationGroup,
|
||||
];
|
||||
}
|
||||
$serializedValue = $this->serializer->serialize(data: $value, format: JsonEncoder::FORMAT, context: $context);
|
||||
$io->info(message: sprintf('%s: %s', $label, $serializedValue));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Contract\Entity\Nexus\GamePeriodIdEnum;
|
||||
use App\Doctrine\Entity\Nexus\GamePeriod;
|
||||
use App\Doctrine\Entity\Nexus\Leaderboard;
|
||||
use App\Doctrine\Entity\Nexus\LeaderboardEntry;
|
||||
use App\Service\Repository\Nexus\GamePeriodRepository;
|
||||
use App\Service\Repository\Nexus\LeaderboardRepository;
|
||||
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\Question\Question;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
use function array_key_exists;
|
||||
use function intval;
|
||||
use function is_numeric;
|
||||
use function max;
|
||||
use function mb_convert_case;
|
||||
use function mb_strlen;
|
||||
use function printf;
|
||||
use function sprintf;
|
||||
|
||||
use const MB_CASE_TITLE;
|
||||
use const PHP_EOL;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:export:leaderboards',
|
||||
description: 'Export leaderboards into a file',
|
||||
)]
|
||||
final class ExportLeaderboardsCommand extends BaseCommand
|
||||
{
|
||||
private const OPTION_NAME_GAME_PERIOD_ID = 'period';
|
||||
private const TABLE_HEADER_CHARACTER = 'Character';
|
||||
private const ENCODING_UTF8 = 'UTF-8';
|
||||
|
||||
private const BREATH_4_NAME_REPLACEMENTS = [
|
||||
"I\u{00c3}\u{00af}\u{00c2}\u{00bf}\u{00c2}\u{00bd}unn" => "I\u{00f0}unn",
|
||||
"J\u{00c3}\u{00af}\u{00c2}\u{00bf}\u{00c2}\u{00bd}stein Bever" => "J\u{00f8}stein Bever",
|
||||
"Mockfj\u{00c3}\u{00af}\u{00c2}\u{00bf}\u{00c2}\u{00bd}rdsvapnet" => "Mockfj\u{00e4}rdsvapnet",
|
||||
];
|
||||
|
||||
private ?GamePeriod $gamePeriod = null;
|
||||
|
||||
public function __construct(
|
||||
private GamePeriodRepository $gamePeriodRepository,
|
||||
private LeaderboardRepository $leaderboardRepository,
|
||||
SerializerInterface $serializer,
|
||||
) {
|
||||
parent::__construct(serializer: $serializer);
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addOption(
|
||||
name: self::OPTION_NAME_GAME_PERIOD_ID,
|
||||
mode: InputOption::VALUE_REQUIRED,
|
||||
description: 'Game period ID',
|
||||
);
|
||||
}
|
||||
|
||||
protected function interact(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$io = $this->createSymfonyStyle(input: $input, output: $output);
|
||||
$helper = $this->getQuestionHelper();
|
||||
$question = new Question(question: 'Input game period ID:');
|
||||
|
||||
$inputStr = $input->getOption(name: self::OPTION_NAME_GAME_PERIOD_ID);
|
||||
$gamePeriodId = is_numeric($inputStr) ? intval($inputStr) : null;
|
||||
|
||||
while (true) {
|
||||
if (null === $gamePeriodId) {
|
||||
do {
|
||||
$inputStr = $helper->ask(input: $input, output: $output, question: $question);
|
||||
$gamePeriodId = is_numeric($inputStr) ? intval($inputStr) : null;
|
||||
} while (null === $gamePeriodId);
|
||||
}
|
||||
$this->gamePeriod = $this->gamePeriodRepository->findById(id: $gamePeriodId);
|
||||
if (null === $this->gamePeriod) {
|
||||
$io->error(message: sprintf('Game period with id=%s does not exists!', $inputStr));
|
||||
$gamePeriodId = null;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = $this->createSymfonyStyle(input: $input, output: $output);
|
||||
|
||||
$this->displayValue(io: $io, label: 'Selected game period', value: $this->gamePeriod->getName());
|
||||
|
||||
$leaderboards = $this->leaderboardRepository->findByGamePeriod(gamePeriod: $this->gamePeriod);
|
||||
|
||||
/** @var Leaderboard $leaderboard */
|
||||
foreach ($leaderboards as $leaderboard) {
|
||||
echo PHP_EOL, PHP_EOL, PHP_EOL;
|
||||
$category = $leaderboard->getCategory();
|
||||
printf(
|
||||
'[b]%s (%s)[/b]',
|
||||
$category->getName(),
|
||||
mb_convert_case(string: $category->getType(), mode: MB_CASE_TITLE, encoding: self::ENCODING_UTF8),
|
||||
);
|
||||
echo PHP_EOL, '[code]', PHP_EOL;
|
||||
$headerCharacterLength = mb_strlen(string: self::TABLE_HEADER_CHARACTER, encoding: self::ENCODING_UTF8);
|
||||
$characterColumnWidth = $headerCharacterLength;
|
||||
$entries = [];
|
||||
/** @var LeaderboardEntry $entry */
|
||||
foreach ($leaderboard->getEntries() as $entry) {
|
||||
$characterName = $entry->getCharacterName();
|
||||
if (
|
||||
GamePeriodIdEnum::BREATH_4 === $this->gamePeriod->getId()
|
||||
&& array_key_exists(key: $characterName, array: self::BREATH_4_NAME_REPLACEMENTS)
|
||||
) {
|
||||
$characterName = self::BREATH_4_NAME_REPLACEMENTS[$characterName];
|
||||
}
|
||||
$characterNameLength = mb_strlen(string: $characterName, encoding: self::ENCODING_UTF8);
|
||||
$characterColumnWidth = max($characterColumnWidth, $characterNameLength + 4);
|
||||
$entries[] = [
|
||||
'position' => $entry->getPosition(),
|
||||
'characterName' => $characterName,
|
||||
'characterNameLength' => $characterNameLength,
|
||||
'score' => $entry->getScore(),
|
||||
];
|
||||
}
|
||||
$padding = str_repeat(string: ' ', times: $characterColumnWidth - $headerCharacterLength);
|
||||
printf('%s%s %s', self::TABLE_HEADER_CHARACTER, $padding, $category->getScoreLabel());
|
||||
echo PHP_EOL;
|
||||
/** @var LeaderboardEntry $entry */
|
||||
foreach ($entries as $entry) {
|
||||
$characterStr = sprintf('%d) %s', $entry['position'], $entry['characterName']);
|
||||
$characterStrLength = mb_strlen(string: $characterStr, encoding: self::ENCODING_UTF8);
|
||||
$padding = str_repeat(string: ' ', times: $characterColumnWidth - $characterStrLength);
|
||||
printf('%s%s %s', $characterStr, $padding, $entry['score']);
|
||||
echo PHP_EOL;
|
||||
}
|
||||
echo '[/code]', PHP_EOL;
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Contract\Config\AppSerializationGroups;
|
||||
use App\Doctrine\Entity\User;
|
||||
use App\Service\Repository\UserAccessTokenRepository;
|
||||
use App\Service\Repository\UserRepository;
|
||||
use DateInterval;
|
||||
use Exception;
|
||||
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\Question\Question;
|
||||
use Symfony\Component\Serializer\Encoder\JsonEncoder;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:user:create-access-token',
|
||||
description: 'Creates a new user access token',
|
||||
)]
|
||||
final class UserCreateAccessTokenCommand extends BaseCommand
|
||||
{
|
||||
private const ARGUMENT_NAME_OWNER = 'owner';
|
||||
private const ARGUMENT_NAME_DURATION = 'duration';
|
||||
private const DEFAULT_DURATION = '1 month';
|
||||
|
||||
private ?User $owner = null;
|
||||
private ?string $durationStr = null;
|
||||
private ?DateInterval $duration = null;
|
||||
|
||||
public function __construct(
|
||||
private UserRepository $userRepository,
|
||||
private UserAccessTokenRepository $userAccessTokenRepository,
|
||||
SerializerInterface $serializer,
|
||||
) {
|
||||
parent::__construct(serializer: $serializer);
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addOption(
|
||||
name: self::ARGUMENT_NAME_OWNER,
|
||||
shortcut: null,
|
||||
mode: InputOption::VALUE_REQUIRED,
|
||||
description: 'Owner\'s username',
|
||||
default: null
|
||||
);
|
||||
|
||||
$this->addOption(
|
||||
name: self::ARGUMENT_NAME_DURATION,
|
||||
shortcut: null,
|
||||
mode: InputOption::VALUE_REQUIRED,
|
||||
description: 'Token duration (how long it is valid)',
|
||||
default: self::DEFAULT_DURATION
|
||||
);
|
||||
}
|
||||
|
||||
protected function interact(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$io = $this->createSymfonyStyle(input: $input, output: $output);
|
||||
|
||||
$helper = $this->getQuestionHelper();
|
||||
$question = new Question(question: 'Owner\'s username?', default: null);
|
||||
|
||||
$ownerUsername = $input->getOption(name: self::ARGUMENT_NAME_OWNER);
|
||||
while (true) {
|
||||
while (null === $ownerUsername) {
|
||||
$ownerUsername = $helper->ask(input: $input, output: $output, question: $question);
|
||||
}
|
||||
$this->owner = $this->userRepository->findByUsername(username: $ownerUsername);
|
||||
if (null !== $this->owner) {
|
||||
break;
|
||||
} else {
|
||||
$io->error(message: sprintf('User with username=%s does not exist!', $ownerUsername));
|
||||
$ownerUsername = null;
|
||||
}
|
||||
}
|
||||
|
||||
$questionText = sprintf('Token duration (default: %s)?', self::DEFAULT_DURATION);
|
||||
$question = new Question(question: $questionText, default: self::DEFAULT_DURATION);
|
||||
|
||||
$this->durationStr = $input->getOption(name: self::ARGUMENT_NAME_DURATION);
|
||||
while (true) {
|
||||
if (null === $this->durationStr) {
|
||||
$this->durationStr = $helper->ask(input: $input, output: $output, question: $question);
|
||||
}
|
||||
try {
|
||||
$duration = DateInterval::createFromDateString(datetime: $this->durationStr);
|
||||
} catch (Exception $e) {
|
||||
$duration = null;
|
||||
}
|
||||
if (false !== $duration instanceof DateInterval) {
|
||||
$this->duration = $duration;
|
||||
break;
|
||||
} else {
|
||||
$io->error(message: sprintf('Invalid duration: %s', $this->durationStr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = $this->createSymfonyStyle(input: $input, output: $output);
|
||||
|
||||
$this->displayValue(io: $io, label: 'Selected owner', value: $this->owner);
|
||||
|
||||
$parsedDuration = $this->serializer->serialize(data: $this->duration, format: JsonEncoder::FORMAT);
|
||||
$message = sprintf('Selected duration: %s (parsed as: %s)', $this->durationStr, $parsedDuration);
|
||||
$io->info(message: $message);
|
||||
|
||||
$token = $this->userAccessTokenRepository->create(owner: $this->owner, duration: $this->duration);
|
||||
|
||||
$this->displayValue(
|
||||
io: $io,
|
||||
label: 'Created token',
|
||||
value: $token,
|
||||
serializationGroup: AppSerializationGroups::ENTITY_USER_ACCESS_TOKEN
|
||||
);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,145 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Contract\Config\AppParameters;
|
||||
use App\Contract\UserRoles;
|
||||
use App\Service\Repository\UserRepository;
|
||||
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\Question\Question;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:user:create',
|
||||
description: 'Creates a new user',
|
||||
)]
|
||||
final class UserCreateCommand extends BaseCommand
|
||||
{
|
||||
private const ARGUMENT_NAME_USERNAME = 'username';
|
||||
private const ARGUMENT_NAME_ROLE = 'role';
|
||||
|
||||
private ?string $username = null;
|
||||
private ?string $plaintextPassword = null;
|
||||
private array $roles = [];
|
||||
|
||||
public function __construct(
|
||||
private UserRepository $userRepository,
|
||||
SerializerInterface $serializer,
|
||||
) {
|
||||
parent::__construct(serializer: $serializer);
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addOption(
|
||||
name: self::ARGUMENT_NAME_USERNAME,
|
||||
shortcut: null,
|
||||
mode: InputOption::VALUE_REQUIRED,
|
||||
description: 'Username',
|
||||
default: null
|
||||
);
|
||||
$this->addOption(
|
||||
name: self::ARGUMENT_NAME_ROLE,
|
||||
shortcut: null,
|
||||
mode: InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
||||
description: sprintf('Additional roles (%s is always given)', AppParameters::SECURITY_DEFAULT_ROLE),
|
||||
default: []
|
||||
);
|
||||
}
|
||||
|
||||
protected function interact(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$io = $this->createSymfonyStyle(input: $input, output: $output);
|
||||
|
||||
$this->username = $input->getOption(name: self::ARGUMENT_NAME_USERNAME);
|
||||
|
||||
while (true) {
|
||||
if (null === $this->username) {
|
||||
$this->askForUsername(input: $input, output: $output);
|
||||
}
|
||||
if (null !== $this->userRepository->findByUsername(username: $this->username)) {
|
||||
$io->error(message: sprintf('User with username=%s already exists!', $this->username));
|
||||
$this->username = null;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->askForPassword(input: $input, output: $output);
|
||||
|
||||
$this->roles = $input->getOption(name: self::ARGUMENT_NAME_ROLE);
|
||||
$this->addRole(role: AppParameters::SECURITY_DEFAULT_ROLE);
|
||||
$this->askForAdditionalRoles(input: $input, output: $output);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = $this->createSymfonyStyle(input: $input, output: $output);
|
||||
|
||||
$this->displayValue(io: $io, label: 'Selected username', value: $this->username);
|
||||
$this->displayValue(io: $io, label: 'Selected password', value: $this->plaintextPassword);
|
||||
$this->displayValue(io: $io, label: 'Selected roles', value: $this->roles);
|
||||
|
||||
$user = $this->userRepository->create(
|
||||
username: $this->username,
|
||||
plaintextPassword: $this->plaintextPassword,
|
||||
roles: $this->roles
|
||||
);
|
||||
|
||||
$this->displayValue(io: $io, label: 'Created user', value: $user);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function askForUsername(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$helper = $this->getQuestionHelper();
|
||||
$question = new Question(question: 'Username?');
|
||||
do {
|
||||
$this->username = $helper->ask(input: $input, output: $output, question: $question);
|
||||
} while (null === $this->username);
|
||||
}
|
||||
|
||||
private function askForPassword(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$helper = $this->getQuestionHelper();
|
||||
$question = new Question(question: 'Password?');
|
||||
do {
|
||||
$this->plaintextPassword = $helper->ask(input: $input, output: $output, question: $question);
|
||||
} while (null === $this->plaintextPassword);
|
||||
}
|
||||
|
||||
private function addRole(string $role): void
|
||||
{
|
||||
$this->roles[] = $role;
|
||||
$this->roles = UserRoles::normalize(roles: $this->roles);
|
||||
}
|
||||
|
||||
private function askForAdditionalRoles(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$helper = $this->getQuestionHelper();
|
||||
$question = new Question(question: 'Additional role? (empty to finish adding)');
|
||||
|
||||
$io = $this->createSymfonyStyle(input: $input, output: $output);
|
||||
while (true) {
|
||||
$this->displayValue(io: $io, label: 'Selected roles', value: $this->roles);
|
||||
$role = $helper->ask(input: $input, output: $output, question: $question);
|
||||
if (null === $role) {
|
||||
break;
|
||||
}
|
||||
if (UserRoles::isValidRole(role: $role)) {
|
||||
$this->addRole(role: $role);
|
||||
} else {
|
||||
$io->error(message: sprintf('Invalid role name: %s', $role));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Contract\Service\ClockInterface;
|
||||
use App\Service\PageViewProcessor;
|
||||
use DateInterval;
|
||||
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 Throwable;
|
||||
|
||||
use function intval;
|
||||
use function is_numeric;
|
||||
use function sprintf;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:worker:parse',
|
||||
description: 'Background worker to parse submitted raw data',
|
||||
)]
|
||||
final class WorkerParseCommand extends Command
|
||||
{
|
||||
private const OPTION_NAME_BATCH_SIZE = 'batch-size';
|
||||
private const OPTION_NAME_MAX_ITERATIONS = 'max-iterations';
|
||||
private const OPTION_NAME_MAX_DURATION = 'max-duration';
|
||||
|
||||
public function __construct(
|
||||
private ClockInterface $clock,
|
||||
private PageViewProcessor $pageViewProcessor,
|
||||
private int $batchSize,
|
||||
private int $maxIterations,
|
||||
private string $maxDurationStr,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setHelp(
|
||||
help: <<<'HELP'
|
||||
Gradually parses batches of submitted raw data, starting from the oldest unparsed records.
|
||||
Processing stops after enough rows are processed or too much time elapses.
|
||||
|
||||
This command is intended to be executed periodically as a background process,
|
||||
for example by cron job or systemd timer.
|
||||
|
||||
The option "max-duration" only accepts a strict set of relative date formats,
|
||||
see <https://www.php.net/manual/en/datetime.formats.relative.php> for details.
|
||||
HELP
|
||||
);
|
||||
|
||||
$this->addOption(
|
||||
name: self::OPTION_NAME_BATCH_SIZE,
|
||||
mode: InputOption::VALUE_OPTIONAL,
|
||||
description: 'Number of records processed in single iteration',
|
||||
default: $this->batchSize
|
||||
);
|
||||
|
||||
$this->addOption(
|
||||
name: self::OPTION_NAME_MAX_ITERATIONS,
|
||||
mode: InputOption::VALUE_OPTIONAL,
|
||||
description: 'Maximum number of iterations',
|
||||
default: $this->maxIterations
|
||||
);
|
||||
|
||||
$this->addOption(
|
||||
name: self::OPTION_NAME_MAX_DURATION,
|
||||
mode: InputOption::VALUE_OPTIONAL,
|
||||
description: 'Maximum duration of execution',
|
||||
default: $this->maxDurationStr
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$this->setBatchSize(input: $input);
|
||||
$this->setMaxBatches(input: $input);
|
||||
$this->setMaxDurationStr(input: $input);
|
||||
|
||||
try {
|
||||
$maxDuration = DateInterval::createFromDateString(datetime: $this->maxDurationStr);
|
||||
} catch (Throwable) {
|
||||
$io = new SymfonyStyle(input: $input, output: $output);
|
||||
$io->error(sprintf('Invalid maximum duration: %s', $this->maxDurationStr));
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$endDateTime = (clone($this->clock->getCurrentDateTime()))->add(interval: $maxDuration);
|
||||
|
||||
for ($iteration = 0; $iteration < $this->maxIterations; $iteration++) {
|
||||
$this->pageViewProcessor->process(batchSize: $this->batchSize);
|
||||
if ($this->clock->getCurrentDateTime() >= $endDateTime) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function setBatchSize(InputInterface $input): void
|
||||
{
|
||||
if ($input->hasOption(name: self::OPTION_NAME_BATCH_SIZE)) {
|
||||
$valueStr = $input->getOption(name: self::OPTION_NAME_BATCH_SIZE);
|
||||
if (is_numeric(value: $valueStr)) {
|
||||
$this->batchSize = intval(value: $valueStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function setMaxBatches(InputInterface $input): void
|
||||
{
|
||||
if ($input->hasOption(name: self::OPTION_NAME_MAX_ITERATIONS)) {
|
||||
$valueStr = $input->getOption(name: self::OPTION_NAME_MAX_ITERATIONS);
|
||||
if (is_numeric(value: $valueStr)) {
|
||||
$this->maxIterations = intval(value: $valueStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function setMaxDurationStr(InputInterface $input): void
|
||||
{
|
||||
if ($input->hasOption(name: self::OPTION_NAME_MAX_DURATION)) {
|
||||
$this->maxDurationStr = $input->getOption(name: self::OPTION_NAME_MAX_DURATION);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Service\Repository\PageViewRepository;
|
||||
use App\Service\Repository\UserAccessTokenRepository;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:worker:prune-database',
|
||||
description: 'Background worker to prune unwanted content from database',
|
||||
)]
|
||||
final class WorkerPruneDatabaseCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private UserAccessTokenRepository $userAccessTokenRepository,
|
||||
private PageViewRepository $pageViewRepository,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$this->userAccessTokenRepository->prune();
|
||||
$this->pageViewRepository->prune();
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Contract\Config;
|
||||
|
||||
use App\Contract\UserRoles;
|
||||
use DateTimeInterface;
|
||||
use Symfony\Component\Serializer\Encoder\JsonEncode;
|
||||
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
||||
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
|
||||
|
||||
use const JSON_PRETTY_PRINT;
|
||||
|
||||
final class AppParameters
|
||||
{
|
||||
// doctrine parameters
|
||||
public const DOCTRINE_DEFAULT_CONNECTION_NAME = 'default';
|
||||
public const CACHE_POOL_NAME_DOCTRINE_QUERY_CACHE = 'doctrine.system_cache_pool';
|
||||
public const CACHE_POOL_NAME_DOCTRINE_RESULT_CACHE = 'doctrine.result_cache_pool';
|
||||
|
||||
// framework parameters
|
||||
public const DEFAULT_LOCALE = 'en';
|
||||
public const SERIALIZER_DEFAULT_CONTEXT = [
|
||||
ObjectNormalizer::GROUPS => [AppSerializationGroups::DEFAULT],
|
||||
DateTimeNormalizer::FORMAT_KEY => DateTimeInterface::ISO8601,
|
||||
JsonEncode::OPTIONS => JSON_PRETTY_PRINT,
|
||||
];
|
||||
|
||||
// security parameters
|
||||
public const SECURITY_USER_PROVIDER_NAME = 'app_user_provider';
|
||||
public const SECURITY_USER_ENTITY_ID_FIELD = 'username';
|
||||
public const SECURITY_DEFAULT_ROLE = UserRoles::ROLE_USER;
|
||||
|
||||
// value literals that are not defined as constants in package files
|
||||
public const CONFIGURATOR_IMPORT_TYPE_ANNOTATION = 'annotation';
|
||||
public const CONFIGURATOR_IMPORT_TYPE_GLOB = 'glob';
|
||||
public const DOCTRINE_COLUMN_TYPE_UUID = 'uuid';
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Contract\Config;
|
||||
|
||||
// TODO convert into native enum when PHP 8.1 is released
|
||||
final class AppRoutes
|
||||
{
|
||||
// public routes
|
||||
public const HOME = 'app_home';
|
||||
public const FAVICON_ICO = 'app_favicon_ico';
|
||||
public const LEADERBOARDS = 'app_leaderboards';
|
||||
public const ABOUT = 'app_about';
|
||||
public const SUBMIT_JSON = 'app_submit_json';
|
||||
|
||||
// undocumented routes
|
||||
public const LOGIN = 'app_login';
|
||||
public const LOGOUT = 'app_logout';
|
||||
public const EASYADMIN = 'app_easyadmin';
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Contract\Config;
|
||||
|
||||
// TODO: convert into native enum when 8.1 is available in production
|
||||
final class AppSerializationGroups
|
||||
{
|
||||
public const DEFAULT = 'app.default';
|
||||
public const ENTITY_USER = 'app.entity.user';
|
||||
public const ENTITY_USER_ACCESS_TOKEN = 'app.entity.user_access_token';
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Contract\Config;
|
||||
|
||||
// TODO convert to native enums when PHP 8.1 is available on production
|
||||
final class AppTags
|
||||
{
|
||||
public const PARSER = 'app.parser';
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Contract\Doctrine\Entity;
|
||||
|
||||
use DateTimeInterface;
|
||||
|
||||
interface DatedEntityInterface
|
||||
{
|
||||
public function getCreatedAt(): ?DateTimeInterface;
|
||||
|
||||
public function setCreatedAt(DateTimeInterface $createdAt): void;
|
||||
|
||||
public function getLastModifiedAt(): ?DateTimeInterface;
|
||||
|
||||
public function setLastModifiedAt(DateTimeInterface $lastModifiedAt): void;
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Contract\Doctrine\Entity;
|
||||
|
||||
use App\Contract\Entity\Nexus\GamePeriodInterface;
|
||||
|
||||
interface GamePeriodReferenceInterface
|
||||
{
|
||||
public function getGamePeriod(): ?GamePeriodInterface;
|
||||
|
||||
public function setGamePeriod(GamePeriodInterface $gamePeriod): void;
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Contract\Doctrine\Entity;
|
||||
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
interface UuidPrimaryKeyInterface
|
||||
{
|
||||
public function getId(): Uuid;
|
||||
|
||||
public function setId(Uuid $id): void;
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Contract\Entity;
|
||||
|
||||
// TODO convert into native enum when PHP 8.1 is available
|
||||
final class LeaderboardTypes
|
||||
{
|
||||
public const BREATH = 'breath';
|
||||
public const CAREER = 'career';
|
||||
|
||||
public static function cases(): array
|
||||
{
|
||||
return [
|
||||
self::BREATH,
|
||||
self::CAREER,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Contract\Entity\Nexus;
|
||||
|
||||
// TODO convert into native enum when PHP 8.1 is available on production
|
||||
class GamePeriodIdEnum
|
||||
{
|
||||
public const BREATH_4 = 1;
|
||||
public const BREATH_5_LAUNCH = 2;
|
||||
public const BREATH_5_OUTER_PLANES = 3;
|
||||
public const BREATH_5_STRONGHOLDS = 4;
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Contract\Entity\Nexus;
|
||||
|
||||
use DateTimeInterface;
|
||||
|
||||
/**
|
||||
* Significant date period, for example a Breath or a part of Breath.
|
||||
*/
|
||||
interface GamePeriodInterface
|
||||
{
|
||||
public function getId(): ?int;
|
||||
|
||||
public function setId(?int $value): void;
|
||||
|
||||
public function getName(): ?string;
|
||||
|
||||
public function setName(string $name): void;
|
||||
|
||||
public function getStartedAt(): DateTimeInterface;
|
||||
|
||||
public function setStartedAt(DateTimeInterface $value): void;
|
||||
|
||||
public function getCompletedAt(): DateTimeInterface;
|
||||
|
||||
public function setCompletedAt(?DateTimeInterface $value): void;
|
||||
|
||||
public function isCurrent(): bool;
|
||||
|
||||
public function setCurrent(bool $value): void;
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Contract\Entity\Nexus\Leaderboard;
|
||||
|
||||
interface EntryInterface
|
||||
{
|
||||
public function getCharacterName(): ?string;
|
||||
|
||||
public function setCharacterName(string $characterName): void;
|
||||
|
||||
public function getScore(): ?int;
|
||||
|
||||
public function setScore(int $value): void;
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Contract\Entity\Nexus\Leaderboard;
|
||||
|
||||
use ArrayAccess;
|
||||
use Countable;
|
||||
use InvalidArgumentException;
|
||||
use Iterator;
|
||||
|
||||
/**
|
||||
* Sorted collection of leaderboard entries.
|
||||
*/
|
||||
interface EntryListInterface extends Iterator, ArrayAccess, Countable
|
||||
{
|
||||
/** Iterator interface: Return the current element */
|
||||
public function current(): ?EntryInterface;
|
||||
|
||||
/** Iterator interface: Return the key of the current element */
|
||||
public function key(): ?int;
|
||||
|
||||
/** Iterator interface: Move forward to next element */
|
||||
public function next(): void;
|
||||
|
||||
/** Iterator interface: Rewind the Iterator to the first element */
|
||||
public function rewind(): void;
|
||||
|
||||
/** Iterator interface: Checks if current position is valid */
|
||||
public function valid(): bool;
|
||||
|
||||
/** ArrayAccess interface: Whether an offset exists */
|
||||
public function offsetExists(mixed $offset): bool;
|
||||
|
||||
/** ArrayAccess interface: Offset to retrieve */
|
||||
public function offsetGet(mixed $offset): ?EntryInterface;
|
||||
|
||||
/**
|
||||
* ArrayAccess interface: Assign a value to the specified offset
|
||||
*
|
||||
* @throws InvalidArgumentException when $offset ins not an integer
|
||||
* @throws InvalidArgumentException when $value is not an instance of EntryInterface
|
||||
*/
|
||||
public function offsetSet(mixed $offset, mixed $value): void;
|
||||
|
||||
/** ArrayAccess interface: Unset an offset */
|
||||
public function offsetUnset(mixed $offset): void;
|
||||
|
||||
/** Countable interface: Count elements of an object */
|
||||
public function count(): int;
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Contract\Entity\Nexus;
|
||||
|
||||
use App\Contract\Entity\Nexus\Leaderboard\EntryListInterface;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Leaderboard table (header and a list of entries).
|
||||
*/
|
||||
interface LeaderboardInterface
|
||||
{
|
||||
public function getName(): ?string;
|
||||
|
||||
public function setName(string $name): void;
|
||||
|
||||
public function getType(): ?string;
|
||||
|
||||
/** @throws InvalidArgumentException when $type is not valid value */
|
||||
public function setType(string $type): void;
|
||||
|
||||
public function getScoreLabel(): ?string;
|
||||
|
||||
public function setScoreLabel(string $scoreLabel): void;
|
||||
|
||||
public function getEntries(): EntryListInterface;
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Contract;
|
||||
|
||||
// TODO: convert into native enum when PHP 8.1 is available on production
|
||||
final class PageViewSubmissionResultStatus
|
||||
{
|
||||
public const SUCCESS = 'success';
|
||||
public const ERROR_JSON_DECODE = 'json-decode';
|
||||
public const ERROR_JSON_SCHEMA = 'json-schema';
|
||||
public const ERROR_ACCESS_TOKEN = 'access-token';
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Contract\Service;
|
||||
|
||||
use DateTimeInterface;
|
||||
use DateTimeZone;
|
||||
|
||||
interface ClockInterface
|
||||
{
|
||||
public function getUtcTimeZone(): DateTimeZone;
|
||||
|
||||
public function getCurrentDateTime(): DateTimeInterface;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Contract\Service\Parser;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class ParserError extends RuntimeException
|
||||
{
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Contract\Service\Parser;
|
||||
|
||||
use App\Doctrine\Entity\PageView;
|
||||
|
||||
/**
|
||||
* Parser for submitted raw data
|
||||
*/
|
||||
interface ParserInterface
|
||||
{
|
||||
/**
|
||||
* Check if can parse given object
|
||||
*/
|
||||
public function supports(PageView $pageView): bool;
|
||||
|
||||
/**
|
||||
* Parse given object
|
||||
*/
|
||||
public function parse(PageView $pageView): ParserResultInterface;
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Contract\Service\Parser;
|
||||
|
||||
use App\Contract\Entity\Nexus\GamePeriodInterface;
|
||||
use App\Contract\Entity\Nexus\LeaderboardInterface;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Result of parsing raw data
|
||||
*/
|
||||
interface ParserResultInterface
|
||||
{
|
||||
public function hasErrors(): bool;
|
||||
|
||||
public function getErrors(): ?array;
|
||||
|
||||
public function addError(string|Throwable $error): void;
|
||||
|
||||
public function getGamePeriod(): ?GamePeriodInterface;
|
||||
|
||||
public function setGamePeriod(GamePeriodInterface $gamePeriod): void;
|
||||
|
||||
public function getLeaderboard(): ?LeaderboardInterface;
|
||||
|
||||
public function setLeaderboard(LeaderboardInterface $leaderboard): void;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Contract\Service\Parser;
|
||||
|
||||
interface ParserResultProcessorInterface
|
||||
{
|
||||
/**
|
||||
* Persist parser result to database
|
||||
*/
|
||||
public function persist(ParserResultInterface $parserResult): void;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Contract\Service\Parser;
|
||||
|
||||
use App\Doctrine\Entity\PageView;
|
||||
|
||||
interface ParserSelectorInterface
|
||||
{
|
||||
/**
|
||||
* Find parser that can handle given object
|
||||
*/
|
||||
public function findParser(PageView $pageView): ?ParserInterface;
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Contract;
|
||||
|
||||
use function array_filter;
|
||||
use function array_values;
|
||||
use function in_array;
|
||||
|
||||
// TODO convert into native enum when PHP 8.1 is released
|
||||
final class UserRoles
|
||||
{
|
||||
public const ROLE_USER = 'ROLE_USER';
|
||||
public const ROLE_ADMIN = 'ROLE_ADMIN';
|
||||
|
||||
private const ALL_ROLES = [
|
||||
self::ROLE_USER,
|
||||
self::ROLE_ADMIN,
|
||||
];
|
||||
|
||||
public static function isValidRole(string $role): bool
|
||||
{
|
||||
return in_array(needle: $role, haystack: self::ALL_ROLES, strict: true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $roles
|
||||
* @return string[]
|
||||
*/
|
||||
public static function normalize(array $roles): array
|
||||
{
|
||||
$callback = static function (string $role) use ($roles) {
|
||||
return in_array($role, $roles, true);
|
||||
};
|
||||
return array_values(array: array_filter(array: self::ALL_ROLES, callback: $callback));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Contract\Config\AppRoutes;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Twig\Environment;
|
||||
|
||||
final class AboutController
|
||||
{
|
||||
public function __construct(private Environment $twigEnvironment)
|
||||
{
|
||||
}
|
||||
|
||||
#[Route(path: '/about', name: AppRoutes::ABOUT, methods: [Request::METHOD_GET])]
|
||||
public function index(): Response
|
||||
{
|
||||
$responseBody = $this->twigEnvironment->render(name: 'about/index.html.twig');
|
||||
|
||||
return new Response(content: $responseBody);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Contract\Config\AppRoutes;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
final class FaviconController
|
||||
{
|
||||
#[Route(path: '/favicon.ico', name: AppRoutes::FAVICON_ICO, methods: [Request::METHOD_GET])]
|
||||
public function favicon(): Response
|
||||
{
|
||||
return new Response(content: null, status: Response::HTTP_GONE, headers: []);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Contract\Config\AppRoutes;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Twig\Environment;
|
||||
|
||||
final class HomeController
|
||||
{
|
||||
public function __construct(private Environment $twigEnvironment)
|
||||
{
|
||||
}
|
||||
|
||||
#[Route(path: '/', name: AppRoutes::HOME, methods: [Request::METHOD_GET])]
|
||||
public function index(): Response
|
||||
{
|
||||
$content = $this->twigEnvironment->render(name: 'home/index.html.twig');
|
||||
|
||||
return new Response(content: $content);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Contract\Config\AppRoutes;
|
||||
use App\Service\Repository\Nexus\GamePeriodRepository;
|
||||
use App\Service\Repository\Nexus\LeaderboardRepository;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Twig\Environment;
|
||||
|
||||
use function array_key_exists;
|
||||
use function intval;
|
||||
use function sprintf;
|
||||
|
||||
final class LeaderboardController
|
||||
{
|
||||
public function __construct(
|
||||
private Environment $twigEnvironment,
|
||||
private GamePeriodRepository $gamePeriodRepository,
|
||||
private LeaderboardRepository $leaderboardRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route(path: '/leaderboards', name: AppRoutes::LEADERBOARDS, methods: [Request::METHOD_GET])]
|
||||
public function index(
|
||||
Request $request
|
||||
): Response {
|
||||
$gamePeriods = $this->gamePeriodRepository->findAll();
|
||||
|
||||
$defaultGamePeriodId = null;
|
||||
$optionsGamePeriods = [];
|
||||
foreach ($gamePeriods as $gamePeriod) {
|
||||
$id = $gamePeriod->getId();
|
||||
$optionsGamePeriods[$id] = $gamePeriod;
|
||||
if ($gamePeriod->isCurrent()) {
|
||||
$defaultGamePeriodId = $id;
|
||||
}
|
||||
}
|
||||
|
||||
$selectedGamePeriodStr = $request->get(key: 'gamePeriod');
|
||||
if (null !== $selectedGamePeriodStr) {
|
||||
$selectedGamePeriodId = intval(value: $selectedGamePeriodStr);
|
||||
if (false === array_key_exists(key: $selectedGamePeriodId, array: $optionsGamePeriods)) {
|
||||
throw new NotFoundHttpException(
|
||||
message: sprintf('Invalid ID of game period: %d', $selectedGamePeriodId)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$selectedGamePeriodId = $defaultGamePeriodId;
|
||||
}
|
||||
|
||||
$selectedGamePeriod = $optionsGamePeriods[$selectedGamePeriodId];
|
||||
$leaderboards = $this->leaderboardRepository->findByGamePeriod(gamePeriod: $selectedGamePeriod);
|
||||
|
||||
$context = [
|
||||
'optionsGamePeriods' => $optionsGamePeriods,
|
||||
'selectedGamePeriodId' => $selectedGamePeriodId,
|
||||
'leaderboards' => $leaderboards,
|
||||
];
|
||||
$responseBody = $this->twigEnvironment->render(name: 'leaderboards/index.html.twig', context: $context);
|
||||
|
||||
return new Response(content: $responseBody);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Contract\Config\AppRoutes;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||
use Twig\Environment;
|
||||
|
||||
final class LoginController
|
||||
{
|
||||
public function __construct(private Environment $twigEnvironment)
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/login', name: AppRoutes::LOGIN)]
|
||||
public function index(
|
||||
AuthenticationUtils $authenticationUtils
|
||||
): Response {
|
||||
$context = [
|
||||
'error' => $authenticationUtils->getLastAuthenticationError(),
|
||||
'last_username' => $authenticationUtils->getLastUsername(),
|
||||
];
|
||||
$content = $this->twigEnvironment->render(name: 'login/index.html.twig', context: $context);
|
||||
|
||||
return new Response(content: $content);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Contract\Config\AppRoutes;
|
||||
use App\Contract\PageViewSubmissionResultStatus;
|
||||
use App\Service\PageViewSubmissionHandler;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Serializer\Encoder\JsonEncoder;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use Twig\Environment;
|
||||
|
||||
final class SubmitJsonController
|
||||
{
|
||||
public function __construct(
|
||||
private Environment $twigEnvironment,
|
||||
private PageViewSubmissionHandler $pageViewSubmissionHandler,
|
||||
private SerializerInterface $serializer,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route(path: '/submit-json', name: AppRoutes::SUBMIT_JSON, methods: [Request::METHOD_GET, Request::METHOD_POST])]
|
||||
public function json(
|
||||
Request $request
|
||||
): Response {
|
||||
if ($request->isMethod(method: Request::METHOD_POST)) {
|
||||
$userAccessTokenValue = $request->request->get(key: 'userAccessToken');
|
||||
$jsonData = $request->request->get(key: 'jsonData');
|
||||
|
||||
$submissionResult = $this->pageViewSubmissionHandler->handle(
|
||||
userAccessTokenValue: $userAccessTokenValue,
|
||||
jsonData: $jsonData,
|
||||
);
|
||||
|
||||
$responseStatus = match ($submissionResult->getStatus()) {
|
||||
PageViewSubmissionResultStatus::SUCCESS => Response::HTTP_CREATED,
|
||||
PageViewSubmissionResultStatus::ERROR_ACCESS_TOKEN => Response::HTTP_UNAUTHORIZED,
|
||||
default => Response::HTTP_BAD_REQUEST,
|
||||
};
|
||||
|
||||
return $this->createJsonResponse(data: $submissionResult, status: $responseStatus);
|
||||
}
|
||||
|
||||
$content = $this->twigEnvironment->render(name: 'submit-json/index.html.twig');
|
||||
|
||||
return new Response(content: $content);
|
||||
}
|
||||
|
||||
private function createJsonResponse(mixed $data, int $status): Response
|
||||
{
|
||||
$serializedData = $this->serializer->serialize(data: $data, format: JsonEncoder::FORMAT);
|
||||
return new JsonResponse(data: $serializedData, status: $status, json: true);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\DTO\Nexus;
|
||||
|
||||
use App\Contract\Entity\LeaderboardTypes;
|
||||
use App\Contract\Entity\Nexus\Leaderboard\EntryListInterface;
|
||||
use App\Contract\Entity\Nexus\LeaderboardInterface;
|
||||
use App\DTO\Nexus\Leaderboard\EntryList;
|
||||
use InvalidArgumentException;
|
||||
|
||||
use function in_array;
|
||||
use function sprintf;
|
||||
|
||||
class Leaderboard implements LeaderboardInterface
|
||||
{
|
||||
private EntryListInterface $entries;
|
||||
|
||||
public function __construct(
|
||||
private ?string $name = null,
|
||||
private ?string $type = null,
|
||||
private ?string $scoreLabel = null,
|
||||
) {
|
||||
$this->entries = new EntryList();
|
||||
}
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): void
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
public function getType(): ?string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function setType(string $type): void
|
||||
{
|
||||
if (false === in_array(needle: $type, haystack: LeaderboardTypes::cases(), strict: true)) {
|
||||
throw new InvalidArgumentException(message: sprintf('Value "%s" is not a valid type', $type));
|
||||
}
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
public function getScoreLabel(): ?string
|
||||
{
|
||||
return $this->scoreLabel;
|
||||
}
|
||||
|
||||
public function setScoreLabel(string $scoreLabel): void
|
||||
{
|
||||
$this->scoreLabel = $scoreLabel;
|
||||
}
|
||||
|
||||
public function getEntries(): EntryListInterface
|
||||
{
|
||||
return $this->entries;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\DTO\Nexus\Leaderboard;
|
||||
|
||||
use App\Contract\Entity\Nexus\Leaderboard\EntryInterface;
|
||||
|
||||
class Entry implements EntryInterface
|
||||
{
|
||||
private ?string $characterName = null;
|
||||
private ?int $score = null;
|
||||
|
||||
public function getCharacterName(): ?string
|
||||
{
|
||||
return $this->characterName;
|
||||
}
|
||||
|
||||
public function setCharacterName(string $characterName): void
|
||||
{
|
||||
$this->characterName = $characterName;
|
||||
}
|
||||
|
||||
public function getScore(): ?int
|
||||
{
|
||||
return $this->score;
|
||||
}
|
||||
|
||||
public function setScore(int $value): void
|
||||
{
|
||||
$this->score = $value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\DTO\Nexus\Leaderboard;
|
||||
|
||||
use App\Contract\Entity\Nexus\Leaderboard\EntryInterface;
|
||||
use App\Contract\Entity\Nexus\Leaderboard\EntryListInterface;
|
||||
use InvalidArgumentException;
|
||||
|
||||
use function array_key_exists;
|
||||
use function array_keys;
|
||||
use function count;
|
||||
use function is_int;
|
||||
|
||||
final class EntryList implements EntryListInterface
|
||||
{
|
||||
/** @var int current position of iterator cursor */
|
||||
private int $iteratorCursor = 0;
|
||||
|
||||
/** @var array array of filled positions */
|
||||
private array $positionList = [];
|
||||
|
||||
/** @var array<int, EntryInterface> position=>entry mapping */
|
||||
private array $entryDict = [];
|
||||
|
||||
public function current(): ?EntryInterface
|
||||
{
|
||||
$position = $this->key();
|
||||
return $this->offsetGet(offset: $position);
|
||||
}
|
||||
|
||||
public function key(): ?int
|
||||
{
|
||||
if ($this->valid()) {
|
||||
return $position = $this->positionList[$this->iteratorCursor];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function next(): void
|
||||
{
|
||||
++$this->iteratorCursor;
|
||||
}
|
||||
|
||||
public function rewind(): void
|
||||
{
|
||||
$this->positionList = array_keys(array: $this->entryDict);
|
||||
$this->iteratorCursor = 0;
|
||||
}
|
||||
|
||||
public function valid(): bool
|
||||
{
|
||||
if (array_key_exists(key: $this->iteratorCursor, array: $this->positionList)) {
|
||||
$position = $this->positionList[$this->iteratorCursor];
|
||||
return $this->offsetExists(offset: $position);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->entryDict);
|
||||
}
|
||||
|
||||
public function offsetExists(mixed $offset): bool
|
||||
{
|
||||
if (false === is_int($offset)) {
|
||||
return false; // unsupported offset type
|
||||
}
|
||||
return array_key_exists(key: $offset, array: $this->entryDict);
|
||||
}
|
||||
|
||||
public function offsetGet(mixed $offset): ?EntryInterface
|
||||
{
|
||||
if ($this->offsetExists(offset: $offset)) {
|
||||
return $this->entryDict[$offset];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function offsetSet(mixed $offset, mixed $value): void
|
||||
{
|
||||
if (false === is_int(value: $offset)) {
|
||||
throw new InvalidArgumentException(message: 'Offset is not an integer');
|
||||
}
|
||||
if (false === $value instanceof EntryInterface) {
|
||||
throw new InvalidArgumentException(message: 'Value is not an instance of EntryInterface');
|
||||
}
|
||||
$this->entryDict[$offset] = $value;
|
||||
}
|
||||
|
||||
public function offsetUnset(mixed $offset): void
|
||||
{
|
||||
if ($this->offsetExists(offset: $offset)) {
|
||||
unset($this->entryDict[$offset]);
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue