Compare commits
290 Commits
Release_23
...
master
Author | SHA1 | Date | |
---|---|---|---|
6e1ee839ba | |||
cac57d7f71 | |||
cde14ee779 | |||
dfcdd8dca7 | |||
a5286bf62f | |||
ec4d3806ce | |||
0c2334502d | |||
6341072b0b | |||
1dbd813bef | |||
14d83d24b4 | |||
ba02b6d8cb | |||
9b8fcaad9a | |||
6bd6ede442 | |||
65dac4640a | |||
7bd12050f6 | |||
5041258de0 | |||
47928d2d2b | |||
55cf2afde3 | |||
ed2b1c23ae | |||
6ef45b8b6a | |||
b4c359e81d | |||
495a2fe910 | |||
37ba0ec172 | |||
9ffde6bccb | |||
0ce1c4f28a | |||
0d1e4a3d1c | |||
cbf62d1c4a | |||
5eeac18b4c | |||
c4dce94f5e | |||
147b7690ac | |||
bdfa46b838 | |||
27fe883f59 | |||
1718dfca9e | |||
6870d08c21 | |||
eba674bfbc | |||
832170b1e1 | |||
8dfea0993b | |||
6146641eed | |||
7cb406cc49 | |||
2d2cf9c6f0 | |||
ca069d0795 | |||
17d9437a50 | |||
aed4548c7b | |||
0d6d152b82 | |||
7beec510a1 | |||
22b849a1c8 | |||
492aa9fcef | |||
b6da70e015 | |||
afead7fd61 | |||
69acbf708b | |||
08ebfae030 | |||
a597839379 | |||
e0fe6a8622 | |||
5ebce653dc | |||
61eb2404a2 | |||
e199dfccc7 | |||
1d0a2b09d5 | |||
1886257b63 | |||
eda1415e86 | |||
4e9c7f8c3c | |||
eebf4aff00 | |||
6dae33507f | |||
96d5aa2219 | |||
6c326b89e2 | |||
6c821d41b4 | |||
ade02f4736 | |||
bb5b510315 | |||
1267cb3d95 | |||
c7641b85e7 | |||
ee4b8e9d65 | |||
58d4f982e9 | |||
ad06cfaa58 | |||
aa2e0e9b4a | |||
d1c926541a | |||
9929e00a3f | |||
bf415a2a46 | |||
c0bfa5b334 | |||
e122248a97 | |||
d2bb1d4936 | |||
b8a901b041 | |||
ede5c24a5d | |||
0ae7040804 | |||
f370ccba88 | |||
a128168e7f | |||
3e9c25f6fe | |||
352311a82e | |||
c97d80a724 | |||
532dbfd8d6 | |||
49f3a371e5 | |||
2bcaf13ee3 | |||
2fc5812eba | |||
63ca6e7557 | |||
224cdc0eda | |||
42da9eb778 | |||
d1dee27191 | |||
726b1818d0 | |||
75f0ae7668 | |||
b43ed72040 | |||
5dc4350b20 | |||
829588150a | |||
54d901e45e | |||
5e1692efdb | |||
32733668ff | |||
3034217ff8 | |||
c664b0a4bf | |||
f397354c1e | |||
e3992d9d55 | |||
0f7c9aa02f | |||
b531589c52 | |||
dc824a0f70 | |||
1a06326589 | |||
9780aa62cb | |||
cc3cfc15c8 | |||
a877484d56 | |||
da5c2121ba | |||
73c7d8434e | |||
3f7817fd1f | |||
361671a362 | |||
b2babcf30f | |||
ea579614b3 | |||
d8786e74d5 | |||
9e448e62c6 | |||
7f1177cc55 | |||
a941f5a1cd | |||
ba5dfdbe4d | |||
5557f00e57 | |||
baddc17b33 | |||
8d495c9f37 | |||
cc8f1e1ace | |||
97e04d785c | |||
0c102da1d7 | |||
7f882f2e71 | |||
cb76cbb9b1 | |||
1f32387f63 | |||
be93c75fe7 | |||
38d18873b8 | |||
5053d37388 | |||
c00be9a49d | |||
881280fa3e | |||
40eb8b5335 | |||
789e9e199f | |||
be521a42ee | |||
65424c2944 | |||
490e48a87a | |||
b958a2f5b3 | |||
2da0ccb09b | |||
584fcaba39 | |||
b5dc96f98b | |||
4dbbb1321c | |||
0a7cfdbb8e | |||
751c1895ca | |||
3fd43b6e41 | |||
ae3554c365 | |||
5526a55145 | |||
3e6514a2e5 | |||
8566641459 | |||
14f7a5b9b5 | |||
6fb951a01e | |||
c7c2df89c5 | |||
67114211ec | |||
f2d99c4f0f | |||
2ed01df331 | |||
f89bed25da | |||
d707c20ab6 | |||
49d72f1d48 | |||
eabf17cb77 | |||
7b45069904 | |||
49df206ddc | |||
e0ea18a6d0 | |||
20a850f011 | |||
157f530ad5 | |||
0b261b6272 | |||
882b326aca | |||
49fe85dcde | |||
a021aff92c | |||
6abe93e3b5 | |||
7e84e6b4ba | |||
76ed977375 | |||
fea403fe5a | |||
1d96fd0cb9 | |||
7437e19de5 | |||
47567c9b75 | |||
ef16e70235 | |||
91d064947e | |||
9a6e95673e | |||
d24c3bb1cb | |||
2ad637e55f | |||
ac09342c9d | |||
3a1054e1df | |||
0f02c4b5be | |||
be0a15d02d | |||
12065b0525 | |||
0fc21cc461 | |||
7d63de4403 | |||
c520c256ee | |||
2f9b97d1a4 | |||
ad18c7cb25 | |||
a2fb166b55 | |||
f0a7b34212 | |||
dccb34971b | |||
36f767ea9b | |||
c0739eeddf | |||
cac30c9203 | |||
89b0ce768b | |||
656ce219ef | |||
fd30911dd3 | |||
b4cfa7aab2 | |||
617ae903ae | |||
a7997429b7 | |||
4d5e982370 | |||
46ea01efe1 | |||
4161abe098 | |||
8777d931a4 | |||
d2ef9bb18a | |||
3ec7fc11fa | |||
9fd8453f63 | |||
a7d3942d1f | |||
41933ec510 | |||
e98bb28faf | |||
34eeb10c27 | |||
70e13f70c0 | |||
a89182b64f | |||
45a22c2dd4 | |||
c6d33753c8 | |||
b32092f1cd | |||
b5cb59d3ee | |||
0b8d7b9cda | |||
4f4cc43e90 | |||
b066ef22b4 | |||
08767d1d59 | |||
46404a9d7b | |||
8a4c5403cb | |||
15a58e8ace | |||
d763c4344c | |||
4dc08dffc9 | |||
87b811f716 | |||
eb4ebb9582 | |||
6af7813e3d | |||
31ff9b287d | |||
c9a06e5ada | |||
4f007765f4 | |||
97780eb079 | |||
e487a59816 | |||
a7790319eb | |||
e143d05801 | |||
7c0ebe6668 | |||
6eb54a6f11 | |||
ed137b38de | |||
db757de71c | |||
71aed9dcec | |||
b809542083 | |||
749b93e3af | |||
df3bf89079 | |||
de346c0c6e | |||
6df63373ab | |||
a0fe77fe66 | |||
151112bd2a | |||
366abf61b3 | |||
367e78cbf8 | |||
c056e0bdfc | |||
12890293e0 | |||
845f1fe262 | |||
bc9f1a1d1f | |||
84df948012 | |||
1200489186 | |||
74d0b24f5f | |||
cbe6d79ab5 | |||
72618c6c66 | |||
b6018a0715 | |||
35b7db81b2 | |||
89c7d3b0ea | |||
13b62c8c02 | |||
364d55a4b2 | |||
e4dc8ace04 | |||
af8ecc748f | |||
6d11be728e | |||
453940a5ef | |||
d45b790122 | |||
862daea29c | |||
006ffea7c5 | |||
03d71727e8 | |||
b2584dca31 | |||
545e5b39ac | |||
67619926ca | |||
e13281af5f | |||
36d5635812 | |||
ef6561ac19 | |||
f2aa55bc3e | |||
48017bf46f | |||
8edb7ff37f |
@ -19,3 +19,6 @@ GOOGLE_OAUTH_CLIENT_SECRET=your_google_oauth_client_secret
|
||||
GOOGLE_ANALITICS_ID=your_google_analytics_id
|
||||
RECAPTCHA_SITEKEY=your_recaptcha_sitekey
|
||||
RECAPTCHA_SECRET=your_recaptcha_secret
|
||||
JWT_RSA_PRIVATE_KEY=jwt-rsa256-private.pem
|
||||
JWT_RSA_PUBLIC_KEY=jwt-rsa256-public.pem
|
||||
JWT_KEY_KID=1
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@
|
||||
installed
|
||||
vendor
|
||||
node_modules
|
||||
*.pem
|
||||
|
65
Jenkinsfile
vendored
65
Jenkinsfile
vendored
@ -13,8 +13,9 @@ pipeline {
|
||||
}
|
||||
agent {
|
||||
dockerfile {
|
||||
filename 'docker/Dockerfile-test'
|
||||
filename 'docker/Dockerfile'
|
||||
dir '.'
|
||||
additionalBuildArgs '--target rvr_base'
|
||||
reuseNode true
|
||||
}
|
||||
}
|
||||
@ -26,8 +27,9 @@ pipeline {
|
||||
stage('Unit Testing') {
|
||||
agent {
|
||||
dockerfile {
|
||||
filename 'docker/Dockerfile-test'
|
||||
filename 'docker/Dockerfile'
|
||||
dir '.'
|
||||
additionalBuildArgs '--target rvr_base'
|
||||
reuseNode true
|
||||
}
|
||||
}
|
||||
@ -35,7 +37,7 @@ pipeline {
|
||||
sh 'vendor/bin/phpunit --log-junit unit_test_results.xml --testdox tests'
|
||||
}
|
||||
post {
|
||||
success {
|
||||
always {
|
||||
archiveArtifacts 'unit_test_results.xml'
|
||||
}
|
||||
}
|
||||
@ -44,8 +46,9 @@ pipeline {
|
||||
stage('Static Code Analysis') {
|
||||
agent {
|
||||
dockerfile {
|
||||
filename 'docker/Dockerfile-test'
|
||||
filename 'docker/Dockerfile'
|
||||
dir '.'
|
||||
additionalBuildArgs '--target rvr_base'
|
||||
reuseNode true
|
||||
}
|
||||
}
|
||||
@ -53,10 +56,62 @@ pipeline {
|
||||
sh 'php vendor/bin/phpstan analyse -c phpstan.neon --error-format=prettyJson > static_code_analysis_results.json'
|
||||
}
|
||||
post {
|
||||
success {
|
||||
always {
|
||||
archiveArtifacts 'static_code_analysis_results.json'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Prepare Docker release') {
|
||||
environment {
|
||||
COMPOSER_HOME="${WORKSPACE}/.composer"
|
||||
npm_config_cache="${WORKSPACE}/.npm"
|
||||
}
|
||||
agent {
|
||||
dockerfile {
|
||||
filename 'docker/Dockerfile'
|
||||
dir '.'
|
||||
additionalBuildArgs '--target rvr_base'
|
||||
reuseNode true
|
||||
}
|
||||
}
|
||||
steps {
|
||||
script {
|
||||
sh script: 'git clean -ffdx', label: 'Clean repository'
|
||||
env.VERSION = sh(script: 'git describe --tags --always --match "Release_*" HEAD', returnStdout: true).trim()
|
||||
sh script: 'docker/scripts/release.sh', label: 'Release script'
|
||||
sh script: "rm -rf ${env.COMPOSER_HOME} ${env.npm_config_cache}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Release Docker image') {
|
||||
steps {
|
||||
script {
|
||||
withDockerRegistry([credentialsId: 'gitea-system-user', url: 'https://git.esoko.eu/']) {
|
||||
sh script: 'docker buildx create --use --bootstrap --platform=linux/arm64,linux/amd64 --name multi-platform-builder'
|
||||
sh script: """docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
-f docker/Dockerfile \
|
||||
--target rvr_release \
|
||||
-t git.esoko.eu/esoko/rvr:${env.VERSION} \
|
||||
--push \
|
||||
.""",
|
||||
label: 'Build Docker image'
|
||||
|
||||
if (env.BRANCH_NAME == 'master') {
|
||||
if (env.VERSION ==~ '.*-\\d+-g[a-f0-9]{7}') {
|
||||
env.FIXED_VERSION = 'dev'
|
||||
} else {
|
||||
env.FIXED_VERSION = 'stable'
|
||||
}
|
||||
sh script: """docker buildx imagetools create \
|
||||
-t git.esoko.eu/esoko/rvr:${env.FIXED_VERSION} \
|
||||
git.esoko.eu/esoko/rvr:${env.VERSION}"""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
118
README.md
118
README.md
@ -2,81 +2,81 @@
|
||||
|
||||
[](https://ci.esoko.eu/job/rvr-nextgen/job/master/)
|
||||
|
||||
This is the RVR Application project. This is a game about guessing where you are based on a street view panorama - inspired by existing applications.
|
||||
This is the RVR Application project.
|
||||
|
||||
## Installation
|
||||
|
||||
### Clone the Git repository
|
||||
### Set environment variables
|
||||
|
||||
The first step is obviously cloning the repository to your machine:
|
||||
The `.env` file contains several environment variables that are needed by the application to work properly. These should be configured for your environment. Check `.env.example` for reference.
|
||||
|
||||
**Important: `DEV` should NOT be set for production! See section Development if you want to use the application in development mode.**
|
||||
|
||||
### Docker Compose
|
||||
|
||||
Create a `docker-compose.yml` file. The example code below assumes that `.env` is placed in the same folder.
|
||||
|
||||
```yml
|
||||
version: '3'
|
||||
services:
|
||||
app:
|
||||
image: git.esoko.eu/esoko/rvr:latest
|
||||
depends_on:
|
||||
mariadb:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- 80:80
|
||||
volumes:
|
||||
- .env:/var/www/rvr/.env
|
||||
mariadb:
|
||||
image: mariadb:10.3
|
||||
volumes:
|
||||
- mysql:/var/lib/mysql
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: 'root'
|
||||
MYSQL_DATABASE: 'rvr'
|
||||
MYSQL_USER: 'rvr'
|
||||
MYSQL_PASSWORD: 'rvr'
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "mysqladmin -u $$MYSQL_USER -p$$MYSQL_PASSWORD ping -h localhost || exit 1"]
|
||||
start_period: 5s
|
||||
start_interval: 1s
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
volumes:
|
||||
mysql:
|
||||
|
||||
```
|
||||
git clone https://git.esoko.eu/esoko/rvr-nextgen.git
|
||||
```
|
||||
|
||||
All the commands listed here should be executed from the repository root.
|
||||
|
||||
### Setup Docker stack (recommended)
|
||||
|
||||
The easiest way to build up a fully working application with web server and database is to use Docker Compose with the included `docker-compose.yml`.
|
||||
|
||||
All you have to do is executing the following command:
|
||||
|
||||
```
|
||||
Execute the following command:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Attach shell to the container of `rvr-nextgen-app`:
|
||||
|
||||
```
|
||||
docker exec -it rvr-nextgen-app bash
|
||||
```
|
||||
|
||||
All of the following commands should be executed there.
|
||||
|
||||
### Manual setup (alternative)
|
||||
|
||||
If you don't use the Docker stack you need to install your environment manually. Check `docker-compose.yml` and `docker/Dockerfile` to see the system requirements.
|
||||
|
||||
### Initialize project
|
||||
|
||||
This command installes all of the Composer requirements and creates a copy of the example `.env` file.
|
||||
|
||||
```
|
||||
composer create-project
|
||||
```
|
||||
|
||||
### Set environment variables
|
||||
|
||||
The `.env` file contains several environment variables that are needed by the application to work properly. These should be configured for your environment.
|
||||
|
||||
One very important variable is `DEV`. This indicates that the application operates in development (staging) and not in production mode.
|
||||
|
||||
**Hint:** If you install the application in the Docker stack for development (staging) environment, only the variables for external dependencies (API keys, map attribution, etc.) should be adapted. All other variables (for DB connection, static root, mailing, multiplayer, etc.) are fine with the default value.
|
||||
|
||||
### (Production only) Create cron job
|
||||
|
||||
To maintain database (delete inactive users, old sessions etc.), the command `db:maintain` should be regularly executed. It is recommended to create a cron job that runs every hour:
|
||||
|
||||
```
|
||||
0 * * * * /path/to/your/installation/rvr db:maintain >>/var/log/cron-rvr.log 2>&1
|
||||
```
|
||||
|
||||
### Finalize installation
|
||||
|
||||
After you followed the above steps, execute the following command:
|
||||
|
||||
```
|
||||
scripts/install.sh
|
||||
```
|
||||
|
||||
**And you are done!** The application is ready to use and develop. In development mode an administrative user is also created by the installation script, email is **rvr@rvr.dev**, password is **123456**. In production mode you should create the first administrative user with the following command:
|
||||
**And you are done!** The application is ready to use. You can create the first administrative user with the following command after attaching to the `app` container:
|
||||
|
||||
```
|
||||
./rvr user:add EMAIL PASSWORD admin
|
||||
```
|
||||
|
||||
If you installed it in the Docker stack, you can reach it on http://localhost. The mails that are sent by the application can be found on http://localhost:8080/. If needed, the database server can be directly reached on localhost:3306.
|
||||
## Development
|
||||
|
||||
### Set environment variables
|
||||
|
||||
`.env.example` should be copied to `.env` into the repo root. Only the variables for external dependencies (API keys, map attribution, etc.) should be adapted. All other variables (for DB connection, static root, mailing, multiplayer, etc.) are fine with the default value. **`DEV=1` should be set for development!**
|
||||
|
||||
### Docker Compose
|
||||
|
||||
Execute the following command from the repo root:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
**And you are done!** You can reach the application on http://localhost. The mails that are sent by the application can be found on http://localhost:8080. If needed, the database server can be directly reached on localhost:3306, or you can use Adminer web interface on http://localhost:9090
|
||||
|
||||
You might have to attach to the `app` container, e.g. for creating users, `composer update`, etc.
|
||||
|
||||
---
|
||||
|
||||
|
10
app.php
10
app.php
@ -15,9 +15,13 @@ $dotenv->load();
|
||||
class Container
|
||||
{
|
||||
static SokoWeb\Interfaces\Database\IConnection $dbConnection;
|
||||
static SokoWeb\Routing\RouteCollection $routeCollection;
|
||||
static SokoWeb\Interfaces\Session\ISessionHandler $sessionHandler;
|
||||
static SokoWeb\Interfaces\Request\IRequest $request;
|
||||
static SokoWeb\Interfaces\Database\IAuditLogger $auditLogger;
|
||||
static SokoWeb\Interfaces\PersistentData\IPersistentDataManager $persistentDataManager;
|
||||
static ?SokoWeb\Interfaces\Routing\IRouteCollection $routeCollection = null;
|
||||
static ?SokoWeb\Interfaces\Session\ISessionHandler $sessionHandler = null;
|
||||
static ?SokoWeb\Interfaces\Request\IRequest $request = null;
|
||||
}
|
||||
|
||||
Container::$dbConnection = new SokoWeb\Database\Mysql\Connection($_ENV['DB_HOST'], $_ENV['DB_USER'], $_ENV['DB_PASSWORD'], $_ENV['DB_NAME']);
|
||||
Container::$auditLogger = new RVR\Database\AuditLogger(Container::$dbConnection, 'audit_log');
|
||||
Container::$persistentDataManager = new SokoWeb\PersistentData\PersistentDataManager(Container::$dbConnection, Container::$auditLogger);
|
||||
|
@ -10,10 +10,11 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"esoko/soko-web": "0.1"
|
||||
"esoko/soko-web": "0.15",
|
||||
"firebase/php-jwt": "^6.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.6",
|
||||
"phpunit/phpunit": "^10.3",
|
||||
"phpstan/phpstan": "^1.10"
|
||||
},
|
||||
"autoload": {
|
||||
|
1068
composer.lock
generated
1068
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
use RVR\PersistentData\Model\Community;
|
||||
use RVR\PersistentData\Model\Currency;
|
||||
use SokoWeb\Database\Query\Select;
|
||||
|
||||
$select = new Select(Container::$dbConnection);
|
||||
$communities = Container::$persistentDataManager->selectMultipleFromDb($select, Community::class);
|
||||
|
||||
foreach ($communities as $community) {
|
||||
$mainCurrency = new Currency();
|
||||
$mainCurrency->setCommunity($community);
|
||||
$mainCurrency->setCode($community->getCurrency());
|
||||
$mainCurrency->setRoundDigits(0);
|
||||
Container::$persistentDataManager->saveToDb($mainCurrency);
|
||||
|
||||
$community->setMainCurrency($mainCurrency);
|
||||
Container::$persistentDataManager->saveToDb($community);
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
use RVR\PersistentData\Model\Community;
|
||||
use SokoWeb\Database\Query\Select;
|
||||
|
||||
$select = new Select(Container::$dbConnection);
|
||||
$communities = Container::$persistentDataManager->selectMultipleFromDb($select, Community::class);
|
||||
|
||||
foreach ($communities as $community) {
|
||||
$community->generateSlug();
|
||||
Container::$persistentDataManager->saveToDb($community);
|
||||
}
|
10
database/migrations/structure/20230408_1129_oauth.sql
Normal file
10
database/migrations/structure/20230408_1129_oauth.sql
Normal file
@ -0,0 +1,10 @@
|
||||
CREATE TABLE `oauth_tokens` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`nonce` varchar(255) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
`user_id` int(10) unsigned DEFAULT NULL,
|
||||
`code` varchar(255) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
`created` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
`expires` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `code` (`code`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
3
database/migrations/structure/20230409_0101_username.sql
Normal file
3
database/migrations/structure/20230409_0101_username.sql
Normal file
@ -0,0 +1,3 @@
|
||||
ALTER TABLE `users`
|
||||
ADD `username` varchar(100) DEFAULT NULL,
|
||||
ADD UNIQUE `username` (`username`);
|
@ -0,0 +1,5 @@
|
||||
ALTER TABLE `users`
|
||||
ADD `full_name` varchar(255) NOT NULL DEFAULT '',
|
||||
ADD `nickname` varchar(255) NOT NULL DEFAULT '',
|
||||
ADD `phone` varchar(255) NOT NULL DEFAULT '',
|
||||
ADD `id_number` varchar(255) NOT NULL DEFAULT '';
|
@ -0,0 +1,4 @@
|
||||
ALTER TABLE `oauth_tokens`
|
||||
ADD `scope` varchar(255) NOT NULL DEFAULT '',
|
||||
ADD `access_token` varchar(255) CHARACTER SET ascii COLLATE ascii_bin DEFAULT NULL,
|
||||
ADD UNIQUE `access_token` (`access_token`);
|
@ -0,0 +1,10 @@
|
||||
CREATE TABLE `oauth_clients` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`client_id` varchar(16) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
`client_secret` varchar(40) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
`redirect_uris` text NOT NULL,
|
||||
`preapproved` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`created` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `client_id` (`client_id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
@ -0,0 +1,2 @@
|
||||
ALTER TABLE `oauth_tokens`
|
||||
ADD `audience` varchar(255) NOT NULL DEFAULT '';
|
@ -0,0 +1,2 @@
|
||||
ALTER TABLE `oauth_tokens`
|
||||
DROP `audience`;
|
@ -0,0 +1,2 @@
|
||||
ALTER TABLE `oauth_clients`
|
||||
MODIFY `client_id` varchar(255) CHARACTER SET ascii COLLATE ascii_bin NOT NULL;
|
20
database/migrations/structure/20230412_1955_communities.sql
Normal file
20
database/migrations/structure/20230412_1955_communities.sql
Normal file
@ -0,0 +1,20 @@
|
||||
CREATE TABLE `communities` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`currency` varchar(3) NOT NULL,
|
||||
`created` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
||||
|
||||
CREATE TABLE `community_members` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`community_id` int(10) unsigned NOT NULL,
|
||||
`user_id` int(10) unsigned DEFAULT NULL,
|
||||
`owner` tinyint(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `unique_user_for_community` (`community_id`, `user_id`),
|
||||
KEY `community_id` (`community_id`),
|
||||
KEY `user_id` (`user_id`),
|
||||
CONSTRAINT `community_members_community_id` FOREIGN KEY (`community_id`) REFERENCES `communities` (`id`),
|
||||
CONSTRAINT `community_members_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
12
database/migrations/structure/20230417_0158_audit_log.sql
Normal file
12
database/migrations/structure/20230417_0158_audit_log.sql
Normal file
@ -0,0 +1,12 @@
|
||||
CREATE TABLE `audit_log` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`local_table` varchar(255) NOT NULL,
|
||||
`local_id` int(10) unsigned NOT NULL,
|
||||
`type` enum('insert','update','delete') NOT NULL,
|
||||
`date` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
`modifier_id` int(10) unsigned NULL,
|
||||
`column` varchar(255) NULL,
|
||||
`old` text NULL,
|
||||
`new` text NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
10
database/migrations/structure/20230421_2234_currencies.sql
Normal file
10
database/migrations/structure/20230421_2234_currencies.sql
Normal file
@ -0,0 +1,10 @@
|
||||
CREATE TABLE `currencies` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`community_id` int(10) unsigned NOT NULL,
|
||||
`code` varchar(3) NOT NULL,
|
||||
`round_digits` tinyint(1) unsigned NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `unique_currency_for_community` (`community_id`, `code`),
|
||||
KEY `community_id` (`community_id`),
|
||||
CONSTRAINT `currencies_community_id` FOREIGN KEY (`community_id`) REFERENCES `communities` (`id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
@ -0,0 +1,10 @@
|
||||
CREATE TABLE `currency_exchange_rates` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`currency_id` int(10) unsigned NOT NULL,
|
||||
`exchange_rate` decimal(19,9) unsigned NOT NULL,
|
||||
`valid_from` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `currency_id` (`currency_id`),
|
||||
INDEX `valid_from` (`valid_from`),
|
||||
CONSTRAINT `currency_exchange_rates_currency_id` FOREIGN KEY (`currency_id`) REFERENCES `currencies` (`id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
@ -0,0 +1,4 @@
|
||||
ALTER TABLE `communities`
|
||||
ADD `main_currency_id` int(10) unsigned DEFAULT NULL,
|
||||
ADD KEY `main_currency_id` (`main_currency_id`),
|
||||
ADD CONSTRAINT `communities_main_currency_id` FOREIGN KEY (`main_currency_id`) REFERENCES `currencies` (`id`);
|
19
database/migrations/structure/20230428_2150_transactions.sql
Normal file
19
database/migrations/structure/20230428_2150_transactions.sql
Normal file
@ -0,0 +1,19 @@
|
||||
CREATE TABLE `transactions` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`community_id` int(10) unsigned NOT NULL,
|
||||
`currency_id` int(10) unsigned NOT NULL,
|
||||
`payer_user_id` int(10) unsigned NOT NULL,
|
||||
`payee_user_id` int(10) unsigned NULL,
|
||||
`description` varchar(255) NOT NULL,
|
||||
`sum` decimal(19,9) NOT NULL,
|
||||
`time` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `community_id` (`community_id`),
|
||||
KEY `currency_id` (`currency_id`),
|
||||
KEY `payer_user_id` (`payer_user_id`),
|
||||
KEY `payee_user_id` (`payee_user_id`),
|
||||
CONSTRAINT `transactions_community_id` FOREIGN KEY (`community_id`) REFERENCES `communities` (`id`),
|
||||
CONSTRAINT `transactions_currency_id` FOREIGN KEY (`currency_id`) REFERENCES `currencies` (`id`),
|
||||
CONSTRAINT `transactions_payer_user_id` FOREIGN KEY (`payer_user_id`) REFERENCES `users` (`id`),
|
||||
CONSTRAINT `transactions_payee_user_id` FOREIGN KEY (`payee_user_id`) REFERENCES `users` (`id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
@ -0,0 +1,3 @@
|
||||
ALTER TABLE `communities`
|
||||
ADD `slug` varchar(255) CHARACTER SET ascii COLLATE ascii_bin DEFAULT NULL AFTER `id`,
|
||||
ADD UNIQUE `slug` (`slug`);
|
13
database/migrations/structure/20230526_1551_events.sql
Normal file
13
database/migrations/structure/20230526_1551_events.sql
Normal file
@ -0,0 +1,13 @@
|
||||
CREATE TABLE `events` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`slug` varchar(255) CHARACTER SET ascii COLLATE ascii_bin DEFAULT NULL,
|
||||
`community_id` int(10) unsigned NOT NULL,
|
||||
`start` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
`end` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
`title` varchar(255) NOT NULL,
|
||||
`description` text NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `community_id` (`community_id`),
|
||||
UNIQUE KEY `slug` (`slug`),
|
||||
CONSTRAINT `events_community_id` FOREIGN KEY (`community_id`) REFERENCES `communities` (`id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
@ -0,0 +1,4 @@
|
||||
ALTER TABLE `transactions`
|
||||
ADD `event_id` int(10) unsigned NULL AFTER `community_id`,
|
||||
ADD KEY `event_id` (`event_id`),
|
||||
ADD CONSTRAINT `transactions_event_id` FOREIGN KEY (`event_id`) REFERENCES `events` (`id`);
|
@ -0,0 +1,11 @@
|
||||
CREATE TABLE `transaction_payees` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`transaction_id` int(10) unsigned NOT NULL,
|
||||
`user_id` int(10) unsigned DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `unique_user_for_transaction` (`transaction_id`, `user_id`),
|
||||
KEY `transaction_id` (`transaction_id`),
|
||||
KEY `user_id` (`user_id`),
|
||||
CONSTRAINT `transaction_payees_transaction_id` FOREIGN KEY (`transaction_id`) REFERENCES `transactions` (`id`),
|
||||
CONSTRAINT `transaction_payees_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
@ -0,0 +1,26 @@
|
||||
CREATE TABLE `oauth_sessions` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`client_id` varchar(255) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
`scope` varchar(255) NOT NULL DEFAULT '',
|
||||
`nonce` varchar(255) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
`user_id` int(10) unsigned DEFAULT NULL,
|
||||
`code` varchar(255) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
`code_challenge` varchar(128) NULL,
|
||||
`code_challenge_method` enum('plain', 'S256') NULL,
|
||||
`token_claimed` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`created` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
`expires` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `code` (`code`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
ALTER TABLE `oauth_tokens`
|
||||
ADD `session_id` int(10) unsigned NULL,
|
||||
ADD CONSTRAINT `oauth_tokens_session_id` FOREIGN KEY (`session_id`) REFERENCES `oauth_sessions` (`id`),
|
||||
DROP INDEX `code`,
|
||||
DROP INDEX `access_token`,
|
||||
DROP `scope`,
|
||||
DROP `nonce`,
|
||||
DROP `user_id`,
|
||||
DROP `code`,
|
||||
DROP `access_token`;
|
@ -2,12 +2,17 @@ version: '3'
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: ./docker
|
||||
dockerfile: Dockerfile-app
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile
|
||||
target: rvr_dev
|
||||
depends_on:
|
||||
mariadb:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- 80:80
|
||||
volumes:
|
||||
- .:/var/www/rvr
|
||||
working_dir: /var/www/rvr
|
||||
mariadb:
|
||||
image: mariadb:10.3
|
||||
ports:
|
||||
@ -19,6 +24,19 @@ services:
|
||||
MYSQL_DATABASE: 'rvr'
|
||||
MYSQL_USER: 'rvr'
|
||||
MYSQL_PASSWORD: 'rvr'
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "mysqladmin -u $$MYSQL_USER -p$$MYSQL_PASSWORD ping -h localhost || exit 1"]
|
||||
start_period: 5s
|
||||
start_interval: 1s
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
adminer:
|
||||
image: adminer:4.8.1-standalone
|
||||
ports:
|
||||
- 9090:8080
|
||||
environment:
|
||||
- ADMINER_DEFAULT_SERVER=mariadb
|
||||
mail:
|
||||
image: marcopas/docker-mailslurper:latest
|
||||
ports:
|
||||
|
44
docker/Dockerfile
Normal file
44
docker/Dockerfile
Normal file
@ -0,0 +1,44 @@
|
||||
FROM ubuntu:22.04 AS rvr_base
|
||||
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
|
||||
RUN apt update --fix-missing && apt install -y sudo curl git unzip mariadb-client nginx \
|
||||
php-apcu php8.1-cli php8.1-curl php8.1-fpm php8.1-mbstring php8.1-mysql php8.1-zip php8.1-xml
|
||||
|
||||
RUN mkdir -p /run/php
|
||||
COPY docker/configs/nginx.conf /etc/nginx/sites-available/default
|
||||
|
||||
COPY docker/scripts/install-composer.sh install-composer.sh
|
||||
RUN ./install-composer.sh
|
||||
|
||||
COPY docker/scripts/install-nodejs.sh install-nodejs.sh
|
||||
RUN ./install-nodejs.sh
|
||||
RUN npm install -g uglify-js clean-css-cli svgo yarn
|
||||
|
||||
|
||||
FROM rvr_base AS rvr_dev
|
||||
|
||||
RUN apt update --fix-missing && apt install -y php-xdebug
|
||||
|
||||
RUN echo "xdebug.remote_enable = 1" >> /etc/php/8.1/mods-available/xdebug.ini &&\
|
||||
echo "xdebug.remote_autostart = 1" >> /etc/php/8.1/mods-available/xdebug.ini &&\
|
||||
echo "xdebug.remote_connect_back = 1" >> /etc/php/8.1/mods-available/xdebug.ini
|
||||
|
||||
EXPOSE 80
|
||||
EXPOSE 5000
|
||||
EXPOSE 8090
|
||||
EXPOSE 9229
|
||||
ENTRYPOINT docker/scripts/entry-point-dev.sh
|
||||
|
||||
|
||||
FROM rvr_base AS rvr_release
|
||||
|
||||
RUN apt update --fix-missing && apt install -y cron
|
||||
|
||||
WORKDIR /var/www/rvr
|
||||
COPY ./ /var/www/rvr
|
||||
RUN rm -rf /var/www/rvr/.git
|
||||
|
||||
EXPOSE 80
|
||||
EXPOSE 8090
|
||||
ENTRYPOINT docker/scripts/entry-point.sh
|
@ -1,30 +0,0 @@
|
||||
FROM ubuntu:focal
|
||||
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
|
||||
# Install Nginx, PHP and further necessary packages
|
||||
RUN apt update --fix-missing
|
||||
RUN apt install -y curl git unzip mariadb-client nginx \
|
||||
php-apcu php-xdebug php7.4-cli php7.4-curl php7.4-fpm php7.4-mbstring php7.4-mysql php7.4-zip php7.4-xml
|
||||
|
||||
# Configure Nginx with PHP
|
||||
RUN mkdir -p /run/php
|
||||
COPY configs/nginx.conf /etc/nginx/sites-available/default
|
||||
RUN echo "xdebug.remote_enable = 1" >> /etc/php/7.4/mods-available/xdebug.ini
|
||||
RUN echo "xdebug.remote_autostart = 1" >> /etc/php/7.4/mods-available/xdebug.ini
|
||||
RUN echo "xdebug.remote_connect_back = 1" >> /etc/php/7.4/mods-available/xdebug.ini
|
||||
|
||||
# Install Composer
|
||||
COPY scripts/install-composer.sh install-composer.sh
|
||||
RUN ./install-composer.sh
|
||||
|
||||
# Install Node.js and required packages
|
||||
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash -
|
||||
RUN apt install -y nodejs
|
||||
RUN npm install -g uglify-js clean-css-cli svgo yarn
|
||||
|
||||
EXPOSE 80
|
||||
VOLUME /var/www/rvr
|
||||
WORKDIR /var/www/rvr
|
||||
|
||||
ENTRYPOINT /usr/sbin/php-fpm7.4 -F & /usr/sbin/nginx -g 'daemon off;'
|
@ -1,6 +0,0 @@
|
||||
FROM ubuntu:focal
|
||||
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
|
||||
RUN apt update && apt install -y curl git unzip php7.4-cli php7.4-mbstring php7.4-xml
|
||||
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
|
@ -1,3 +1,9 @@
|
||||
map $http_x_forwarded_proto $forwarded_scheme {
|
||||
default $scheme;
|
||||
http http;
|
||||
https https;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
@ -14,7 +20,8 @@ server {
|
||||
|
||||
location ~ \.php$ {
|
||||
include snippets/fastcgi-php.conf;
|
||||
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
|
||||
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
|
||||
fastcgi_param REQUEST_SCHEME $forwarded_scheme;
|
||||
}
|
||||
|
||||
location ~ /\.ht {
|
||||
|
1
docker/scripts/cron
Normal file
1
docker/scripts/cron
Normal file
@ -0,0 +1 @@
|
||||
0 * * * * /var/www/rvr/rvr db:maintain
|
36
docker/scripts/entry-point-dev.sh
Executable file
36
docker/scripts/entry-point-dev.sh
Executable file
@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "Installing Composer packages..."
|
||||
if [ -f .env ]; then
|
||||
composer install
|
||||
else
|
||||
composer create-project
|
||||
fi
|
||||
|
||||
echo "Installing Yarn packages..."
|
||||
(cd public/static && yarn install)
|
||||
|
||||
echo "Migrating DB..."
|
||||
./rvr db:migrate
|
||||
|
||||
echo "Set runner user based on owner of .env..."
|
||||
if ! getent group rvr; then
|
||||
USER_GID=$(stat -c "%g" .env)
|
||||
groupadd --gid $USER_GID rvr
|
||||
fi
|
||||
if ! id -u rvr; then
|
||||
USER_UID=$(stat -c "%u" .env)
|
||||
useradd --uid $USER_UID --gid $USER_GID rvr
|
||||
fi
|
||||
sed -i -e "s/^user = .*$/user = rvr/g" -e "s/^group = .*$/group = rvr/g" /etc/php/8.1/fpm/pool.d/www.conf
|
||||
|
||||
set +e
|
||||
|
||||
/usr/sbin/php-fpm8.1 -F &
|
||||
/usr/sbin/nginx -g 'daemon off;' &
|
||||
|
||||
wait -n
|
||||
|
||||
exit $?
|
31
docker/scripts/entry-point.sh
Executable file
31
docker/scripts/entry-point.sh
Executable file
@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "Migrating DB..."
|
||||
./rvr db:migrate
|
||||
|
||||
echo "Installing crontab..."
|
||||
/usr/bin/crontab docker/scripts/cron
|
||||
|
||||
echo "Set runner user based on owner of .env..."
|
||||
if ! getent group rvr; then
|
||||
USER_GID=$(stat -c "%g" .env)
|
||||
groupadd --gid $USER_GID rvr
|
||||
fi
|
||||
if ! id -u rvr; then
|
||||
USER_UID=$(stat -c "%u" .env)
|
||||
useradd --uid $USER_UID --gid $USER_GID rvr
|
||||
fi
|
||||
chown -R rvr:rvr cache
|
||||
sed -i -e "s/^user = .*$/user = rvr/g" -e "s/^group = .*$/group = rvr/g" /etc/php/8.1/fpm/pool.d/www.conf
|
||||
|
||||
set +e
|
||||
|
||||
/usr/sbin/cron -f &
|
||||
/usr/sbin/php-fpm8.1 -F &
|
||||
/usr/sbin/nginx -g 'daemon off;' &
|
||||
|
||||
wait -n
|
||||
|
||||
exit $?
|
14
docker/scripts/install-nodejs.sh
Executable file
14
docker/scripts/install-nodejs.sh
Executable file
@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
apt update
|
||||
apt install -y ca-certificates curl gnupg
|
||||
mkdir -p /etc/apt/keyrings
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
|
||||
|
||||
NODE_MAJOR=18
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
|
||||
|
||||
apt update
|
||||
apt install -y nodejs
|
27
docker/scripts/release.sh
Executable file
27
docker/scripts/release.sh
Executable file
@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "Installing Composer packages..."
|
||||
composer create-project --no-dev
|
||||
|
||||
echo "Installing Yarn packages..."
|
||||
(cd public/static && yarn install)
|
||||
|
||||
echo "Updating version info..."
|
||||
VERSION=$(git describe --tags --always --match "Release_*" HEAD)
|
||||
REVISION=$(git rev-parse --short HEAD)
|
||||
REVISION_DATE=$(git show -s --format=%aI HEAD)
|
||||
sed -i -E "s/const VERSION = '(.*)';/const VERSION = '${VERSION}';/" app.php
|
||||
sed -i -E "s/const REVISION = '(.*)';/const REVISION = '${REVISION}';/" app.php
|
||||
sed -i -E "s/const REVISION_DATE = '(.*)';/const REVISION_DATE = '${REVISION_DATE}';/" app.php
|
||||
|
||||
echo "Minifying JS, CSS and SVG files..."
|
||||
find public/static/js -type f -iname '*.js' -exec uglifyjs {} -c -m -o {} \;
|
||||
find public/static/css -type f -iname '*.css' -exec cleancss {} -o {} \;
|
||||
find public/static/img -type f -iname '*.svg' -exec svgo {} -o {} \;
|
||||
|
||||
echo "Linking view files..."
|
||||
./rvr view:link
|
||||
|
||||
rm .env
|
@ -1,9 +0,0 @@
|
||||
Hi,
|
||||
<br><br>
|
||||
You recently signed up on {{APP_NAME}} with this Google account ({{EMAIL}}).
|
||||
<br><br>
|
||||
Have fun on {{APP_NAME}}!
|
||||
<br><br>
|
||||
Regards,<br>
|
||||
{{APP_NAME}}<br>
|
||||
<a href="{{BASE_URL}}" title="{{APP_NAME}}">{{BASE_URL}}</a>
|
@ -1,18 +0,0 @@
|
||||
Hi,
|
||||
<br><br>
|
||||
You recently signed up on {{APP_NAME}} with this email address ({{EMAIL}}).
|
||||
To activate your account, please click on the following link:<br>
|
||||
<a href="{{ACTIVATE_LINK}}" title="Account activation">{{ACTIVATE_LINK}}</a>
|
||||
<br><br>
|
||||
You can activate your account until {{ACTIVATABLE_UNTIL}}.
|
||||
If you don't activate your account, your email address will be permanently deleted after this point of time.
|
||||
<br><br>
|
||||
If you did not sign up on {{APP_NAME}} or changed your mind, no further action is required.
|
||||
However if you want to immediately delete your email address, please click on the following link:<br>
|
||||
<a href="{{CANCEL_LINK}}" title="Sign up cancellation">{{CANCEL_LINK}}</a>
|
||||
<br><br>
|
||||
Have fun on {{APP_NAME}}!
|
||||
<br><br>
|
||||
Regards,<br>
|
||||
{{APP_NAME}}<br>
|
||||
<a href="{{BASE_URL}}" title="{{APP_NAME}}">{{BASE_URL}}</a>
|
@ -1,58 +1,3 @@
|
||||
<?php
|
||||
|
||||
use SokoWeb\Interfaces\Response\IRedirect;
|
||||
use SokoWeb\Response\Redirect;
|
||||
|
||||
require '../web.php';
|
||||
|
||||
$method = strtolower($_SERVER['REQUEST_METHOD']);
|
||||
$url = substr($_SERVER['REQUEST_URI'], strlen('/'));
|
||||
if (($pos = strpos($url, '?')) !== false) {
|
||||
$url = substr($url, 0, $pos);
|
||||
}
|
||||
$url = rawurldecode($url);
|
||||
|
||||
$match = Container::$routeCollection->match($method, $url == '' ? [] : explode('/', $url));
|
||||
|
||||
if ($match !== null) {
|
||||
list($route, $params) = $match;
|
||||
|
||||
Container::$request->setParsedRouteParams($params);
|
||||
|
||||
$handler = $route->getHandler();
|
||||
$controller = new $handler[0](Container::$request);
|
||||
|
||||
if ($controller instanceof SokoWeb\Interfaces\Authorization\ISecured) {
|
||||
$authorized = $controller->authorize();
|
||||
} else {
|
||||
$authorized = true;
|
||||
}
|
||||
|
||||
if (!$authorized) {
|
||||
Container::$request->session()->set('redirect_after_login', $url);
|
||||
$response = new Redirect(Container::$routeCollection->getRoute('login')->generateLink(), IRedirect::TEMPORARY);
|
||||
header('Location: ' . $response->getUrl(), true, $response->getHttpCode());
|
||||
return;
|
||||
}
|
||||
|
||||
if ($method === 'post' && Container::$request->post('anti_csrf_token') !== Container::$request->session()->get('anti_csrf_token')) {
|
||||
$content = new SokoWeb\Response\JsonContent(['error' => 'no_valid_anti_csrf_token']);
|
||||
header('Content-Type: text/html; charset=UTF-8', true, 403);
|
||||
$content->render();
|
||||
return;
|
||||
}
|
||||
|
||||
$response = call_user_func([$controller, $handler[1]]);
|
||||
if ($response instanceof SokoWeb\Interfaces\Response\IContent) {
|
||||
header('Content-Type: ' . $response->getContentType() . '; charset=UTF-8');
|
||||
$response->render();
|
||||
return;
|
||||
} elseif ($response instanceof SokoWeb\Interfaces\Response\IRedirect) {
|
||||
header('Location: ' . $response->getUrl(), true, $response->getHttpCode());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$content = new SokoWeb\Response\HtmlContent('error/404');
|
||||
header('Content-Type: text/html; charset=UTF-8', true, 404);
|
||||
$content->render();
|
||||
|
@ -31,6 +31,10 @@ main {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
::placeholder, select > option[value=""], .gray {
|
||||
color: #8e8e8e;
|
||||
}
|
||||
|
||||
p, h1, h2, h3, input, textarea, select, button, a, table, label {
|
||||
font-family: 'Oxygen', sans-serif;
|
||||
}
|
||||
@ -63,7 +67,7 @@ p, h2, h3 {
|
||||
line-height: 150%;
|
||||
}
|
||||
|
||||
p {
|
||||
p, th, td {
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
}
|
||||
@ -88,18 +92,39 @@ sub {
|
||||
}
|
||||
|
||||
hr {
|
||||
border: solid #bbbbbb 1px;
|
||||
border: solid #bbbbcc 1px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
p.small, span.small {
|
||||
p.small, span.small, td.small {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
p.big, span.big, td.big {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: #a80908;
|
||||
}
|
||||
|
||||
.green {
|
||||
color: #008000;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: 'Oxygen Mono', mono;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.justify {
|
||||
text-align: justify;
|
||||
}
|
||||
@ -109,7 +134,7 @@ p.small, span.small {
|
||||
}
|
||||
|
||||
.marginLeft {
|
||||
margin-left: 10px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.marginBottom {
|
||||
@ -117,7 +142,7 @@ p.small, span.small {
|
||||
}
|
||||
|
||||
.marginRight {
|
||||
margin-right: 10px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.center {
|
||||
@ -146,6 +171,12 @@ a:hover, a:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a.block {
|
||||
color: initial;
|
||||
font-weight: initial;
|
||||
text-decoration: initial;
|
||||
}
|
||||
|
||||
button, a.button {
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
@ -211,6 +242,7 @@ button.noRightRadius, a.button.noRightRadius {
|
||||
|
||||
button.gray, a.button.gray {
|
||||
background-color: #808080;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
button.gray:enabled:hover, button.gray:enabled:focus, a.button.gray:hover, a.button.gray:focus {
|
||||
@ -219,6 +251,7 @@ button.gray:enabled:hover, button.gray:enabled:focus, a.button.gray:hover, a.but
|
||||
|
||||
button.red, a.button.red {
|
||||
background-color: #aa5e5e;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
button.red:enabled:hover, button.red:enabled:focus, a.button.red:hover, a.button.red:focus {
|
||||
@ -226,11 +259,11 @@ button.red:enabled:hover, button.red:enabled:focus, a.button.red:hover, a.button
|
||||
}
|
||||
|
||||
button.yellow, a.button.yellow {
|
||||
background-color: #e8a349;
|
||||
background-color: #daa520;
|
||||
}
|
||||
|
||||
button.yellow:enabled:hover, button.yellow:enabled:focus, a.button.yellow:hover, a.button.yellow:focus {
|
||||
background-color: #c37713;
|
||||
background-color: #b8860b;
|
||||
}
|
||||
|
||||
button.green, a.button.green {
|
||||
@ -368,16 +401,20 @@ header>p {
|
||||
}
|
||||
|
||||
header>p>span {
|
||||
padding-left: 6px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
header>p>span>a:link, header>p>span>a:visited {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
header>p>span>a:hover, header>p>span>a:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
header>p>span:not(:last-child) {
|
||||
border-right: solid white 1px;
|
||||
padding-right: 6px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
main {
|
||||
@ -385,13 +422,6 @@ main {
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
main.full {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: calc(100% - 40px);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
footer {
|
||||
background-color: #869ab9;
|
||||
padding: 3px 6px;
|
||||
@ -411,20 +441,6 @@ div.buttonContainer>button {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#cookiesNotice {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
margin: 20px;
|
||||
background-color: #eeeeee;
|
||||
border: solid #888888 1px;
|
||||
border-radius: 3px;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#loading {
|
||||
position: fixed;
|
||||
width: 64px;
|
||||
@ -438,51 +454,106 @@ div.buttonContainer>button {
|
||||
}
|
||||
|
||||
div.box {
|
||||
width: 576px;
|
||||
background-color: #eeeeee;
|
||||
background-color: #eeeef4;
|
||||
border-radius: 3px;
|
||||
margin: 10px auto;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.circleControl {
|
||||
position: absolute;
|
||||
width: 60px;
|
||||
bottom: 20px;
|
||||
right: 10px;
|
||||
div.compactBox {
|
||||
width: 576px;
|
||||
}
|
||||
|
||||
.circleControl .controlItem {
|
||||
position: relative;
|
||||
height: 60px;
|
||||
margin-top: 10px;
|
||||
opacity: 70%;
|
||||
cursor: pointer;
|
||||
div.transaction {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
}
|
||||
|
||||
.circleControl .controlItem:hover {
|
||||
opacity: 100%;
|
||||
div.gridContainer {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
grid-gap: 10px;
|
||||
}
|
||||
|
||||
.circleControl .controlItem div {
|
||||
position: absolute;
|
||||
div.gridContainer > div {
|
||||
background-color: #eeeef4;
|
||||
border-radius: 3px;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
table.fullWidth {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.circleControl .controlBackground {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 50%;
|
||||
table th {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.circleControl .controlIcon {
|
||||
width: 75%;
|
||||
height: 75%;
|
||||
margin: auto;
|
||||
margin-top: 50%;
|
||||
transform: translateY(-50%);
|
||||
table th, table td {
|
||||
padding: 3px 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table th:not(:first-child), table td:not(:first-child) {
|
||||
padding-left: 3px;
|
||||
}
|
||||
|
||||
table th:not(:last-child), table td:not(:last-child) {
|
||||
padding-right: 3px;
|
||||
}
|
||||
|
||||
p.paginateContainer {
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
p.paginateContainer > * {
|
||||
font-size: initial;
|
||||
background-color: #5e77aa;
|
||||
border: solid #5e77aa 1px;
|
||||
color: #ffffff;
|
||||
font-weight: 700;
|
||||
padding: 3px 6px;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
height: 25px;
|
||||
line-height: 25px;
|
||||
width: 25px;
|
||||
}
|
||||
|
||||
p.paginateContainer > a:hover, p.paginateContainer > .selected {
|
||||
background-color: #3b5998;
|
||||
border: solid #29457f 1px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
p.paginateContainer > *:not(:last-child) {
|
||||
border-right: solid #869ab9 1px;
|
||||
}
|
||||
|
||||
p.formLabel {
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
span.label {
|
||||
border-radius: 3px;
|
||||
background-color: #555555;
|
||||
color: #ffffff;
|
||||
padding: 2px 4px;
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 424px) {
|
||||
div.gridContainer {
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 599px) {
|
||||
@ -493,6 +564,7 @@ div.box {
|
||||
margin-top: 4px;
|
||||
}
|
||||
button, a.button {
|
||||
margin: 3px 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
@ -508,15 +580,9 @@ div.box {
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
div.box {
|
||||
div.compactBox {
|
||||
width: initial;
|
||||
}
|
||||
.circleControl {
|
||||
width: 45px;
|
||||
}
|
||||
.circleControl .controlItem {
|
||||
height: 45px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 600px) {
|
||||
@ -546,12 +612,6 @@ div.box {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.circleControl {
|
||||
width: 45px;
|
||||
}
|
||||
.circleControl .controlItem {
|
||||
height: 45px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-height: 400px) and (max-height: 499px) {
|
||||
|
@ -3,7 +3,7 @@ var Account = {
|
||||
countdown: null,
|
||||
|
||||
openGoogleAuthenticate: function () {
|
||||
window.open('/account/googleAuthenticate', 'googleAuthenticate', 'height=600,width=600')
|
||||
window.open(googleAuthenticateUrl, 'googleAuthenticate', 'height=600,width=600')
|
||||
},
|
||||
|
||||
authenticatedWithGoogleCallback: function (authenticatedWithGoogleUntil) {
|
||||
@ -59,11 +59,10 @@ var Account = {
|
||||
};
|
||||
|
||||
(function () {
|
||||
document.getElementById('authenticateWithGoogleButton').onclick = function () {
|
||||
Account.openGoogleAuthenticate();
|
||||
};
|
||||
|
||||
document.getElementsByTagName('form')[0].onreset = function () {
|
||||
Account.resetGoogleAuthentication();
|
||||
};
|
||||
var authenticateWithGoogleButton = document.getElementById('authenticateWithGoogleButton');
|
||||
if (authenticateWithGoogleButton) {
|
||||
authenticateWithGoogleButton.onclick = function () {
|
||||
Account.openGoogleAuthenticate();
|
||||
};
|
||||
}
|
||||
})();
|
||||
|
30
public/static/js/communities/community_members.js
Normal file
30
public/static/js/communities/community_members.js
Normal file
@ -0,0 +1,30 @@
|
||||
(function () {
|
||||
const element = document.getElementById('newMember').elements['user_id'];
|
||||
const select = new TomSelect(element, {
|
||||
valueField: 'value',
|
||||
labelField: 'label',
|
||||
searchField: 'label',
|
||||
loadThrottle: 300,
|
||||
load: function (query, callback) {
|
||||
var self = this;
|
||||
RVR.httpRequest('GET', searchUserUrl.replace('QUERY', encodeURIComponent(query)), function () {
|
||||
self.clearOptions();
|
||||
callback(this.response.results);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
select.on('change', function (value) {
|
||||
this.clearOptions();
|
||||
});
|
||||
|
||||
select.on('blur', function (value) {
|
||||
this.clearOptions();
|
||||
});
|
||||
|
||||
select.on('type', function (value) {
|
||||
if (value === '') {
|
||||
this.clearOptions();
|
||||
}
|
||||
});
|
||||
})();
|
30
public/static/js/communities/transaction.js
Normal file
30
public/static/js/communities/transaction.js
Normal file
@ -0,0 +1,30 @@
|
||||
(function () {
|
||||
const element = document.getElementById('transactionForm').elements['event_id'];
|
||||
const select = new TomSelect(element, {
|
||||
valueField: 'value',
|
||||
labelField: 'label',
|
||||
searchField: 'label',
|
||||
loadThrottle: 300,
|
||||
load: function (query, callback) {
|
||||
var self = this;
|
||||
RVR.httpRequest('GET', searchEventUrl.replace('QUERY', encodeURIComponent(query)), function () {
|
||||
self.clearOptions();
|
||||
callback(this.response.results);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
select.on('change', function (value) {
|
||||
this.clearOptions();
|
||||
});
|
||||
|
||||
select.on('blur', function (value) {
|
||||
this.clearOptions();
|
||||
});
|
||||
|
||||
select.on('type', function (value) {
|
||||
if (value === '') {
|
||||
this.clearOptions();
|
||||
}
|
||||
});
|
||||
})();
|
@ -1,6 +1,5 @@
|
||||
var RVR = {
|
||||
isSecure: window.location.protocol === 'https:',
|
||||
cookiesAgreed: false,
|
||||
sessionAvailableHooks: {},
|
||||
|
||||
initGoogleAnalitics: function () {
|
||||
@ -50,7 +49,7 @@ var RVR = {
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
|
||||
var formData = new FormData(form);
|
||||
var formError = form.getElementsByClassName('formError')[0];
|
||||
var formError = document.getElementsByClassName('formError')[0];
|
||||
var pageLeaveOnSuccess = form.dataset.redirectOnSuccess || form.dataset.reloadOnSuccess;
|
||||
|
||||
RVR.httpRequest('POST', form.action, function () {
|
||||
@ -65,6 +64,9 @@ var RVR = {
|
||||
|
||||
formError.style.display = 'block';
|
||||
formError.innerHTML = this.response.error.errorText;
|
||||
if (typeof grecaptcha !== 'undefined') {
|
||||
grecaptcha.reset();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
@ -89,6 +91,28 @@ var RVR = {
|
||||
}
|
||||
},
|
||||
|
||||
setOnclickForFormConfirmation: function (button) {
|
||||
button.onclick = function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var self = this;
|
||||
|
||||
RVR.showModalWithContent('Confirmation', this.dataset.confirmation, [
|
||||
{
|
||||
type: 'button',
|
||||
html: this.dataset.confirmationButton ? this.dataset.confirmationButton : this.innerHTML,
|
||||
classNames: ['red'],
|
||||
onclick: function() {
|
||||
var event = new Event('submit', {'bubbles': true, 'cancelable': true});
|
||||
self.form.dispatchEvent(event);
|
||||
|
||||
RVR.hideModal();
|
||||
}
|
||||
}
|
||||
]);
|
||||
};
|
||||
},
|
||||
|
||||
showModal: function (id) {
|
||||
document.getElementById(id).style.visibility = 'visible';
|
||||
document.getElementById('cover').style.visibility = 'visible';
|
||||
@ -126,7 +150,11 @@ var RVR = {
|
||||
|
||||
button.classList.add('marginTop');
|
||||
button.classList.add('marginRight');
|
||||
button.textContent = extraButton.text;
|
||||
if (typeof extraButton.html !== 'undefined') {
|
||||
button.innerHTML = extraButton.html;
|
||||
} else {
|
||||
button.textContent = extraButton.text;
|
||||
}
|
||||
|
||||
if (extraButton.type === 'a') {
|
||||
button.href = extraButton.href;
|
||||
@ -159,12 +187,23 @@ var RVR = {
|
||||
document.getElementById('cover').style.visibility = 'hidden';
|
||||
},
|
||||
|
||||
observeInput: function (input, buttonToToggle) {
|
||||
if (input.defaultValue !== input.value) {
|
||||
buttonToToggle.disabled = false;
|
||||
} else {
|
||||
buttonToToggle.disabled = true;
|
||||
observeInput: function (form, observedInputs) {
|
||||
var anyChanged = false;
|
||||
|
||||
for (var i = 0; i < observedInputs.length; i++) {
|
||||
var input = form.elements[observedInputs[i]];
|
||||
if (input.type === 'checkbox') {
|
||||
if (input.defaultChecked !== input.checked) {
|
||||
anyChanged = true;
|
||||
}
|
||||
} else {
|
||||
if (input.defaultValue !== input.value) {
|
||||
anyChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
form.elements['submit_button'].disabled = !anyChanged;
|
||||
},
|
||||
|
||||
observeInputsInForm: function (form, observedInputs) {
|
||||
@ -175,20 +214,28 @@ var RVR = {
|
||||
case 'INPUT':
|
||||
case 'TEXTAREA':
|
||||
input.oninput = function () {
|
||||
RVR.observeInput(this, form.elements.submit);
|
||||
RVR.observeInput(form, observedInputs);
|
||||
};
|
||||
break;
|
||||
case 'SELECT':
|
||||
input.onchange = function () {
|
||||
RVR.observeInput(this, form.elements.submit);
|
||||
RVR.observeInput(form, observedInputs);
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
form.onreset = function () {
|
||||
form.elements.submit.disabled = true;
|
||||
form.elements['submit_button'].disabled = true;
|
||||
}
|
||||
},
|
||||
|
||||
debounce: function(func, timeout = 300) {
|
||||
let timer;
|
||||
return (...args) => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => { func.apply(this, args); }, timeout);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@ -216,6 +263,10 @@ var RVR = {
|
||||
if (form.dataset.observeInputs) {
|
||||
RVR.observeInputsInForm(form, form.dataset.observeInputs.split(','));
|
||||
}
|
||||
|
||||
if (form.elements['submit_button'] && form.elements['submit_button'].dataset.confirmation) {
|
||||
RVR.setOnclickForFormConfirmation(form.elements['submit_button']);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('cover').onclick = function () {
|
||||
|
43
public/static/package-lock.json
generated
Normal file
43
public/static/package-lock.json
generated
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"requires": true,
|
||||
"lockfileVersion": 1,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.0.tgz",
|
||||
"integrity": "sha512-0NyytTlPJwB/BF5LtRV8rrABDbe3TdTXqNB3PdZ+UUUZAEIrdOJdmABqKjt4AXwIoJNaRVVZEXxpNrqvE1GAYQ=="
|
||||
},
|
||||
"@orchidjs/sifter": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@orchidjs/sifter/-/sifter-1.0.3.tgz",
|
||||
"integrity": "sha512-zCZbwKegHytfsPm8Amcfh7v/4vHqTAaOu6xFswBYcn8nznBOuseu6COB2ON7ez0tFV0mKL0nRNnCiZZA+lU9/g==",
|
||||
"requires": {
|
||||
"@orchidjs/unicode-variants": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"@orchidjs/unicode-variants": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@orchidjs/unicode-variants/-/unicode-variants-1.0.4.tgz",
|
||||
"integrity": "sha512-NvVBRnZNE+dugiXERFsET1JlKZfM5lJDEpSMilKW4bToYJ7pxf0Zne78xyXB2ny2c2aHfJ6WLnz1AaTNHAmQeQ=="
|
||||
},
|
||||
"leaflet": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.3.tgz",
|
||||
"integrity": "sha512-iB2cR9vAkDOu5l3HAay2obcUHZ7xwUBBjph8+PGtmW/2lYhbLizWtG7nTeYht36WfOslixQF9D/uSIzhZgGMfQ=="
|
||||
},
|
||||
"leaflet.markercluster": {
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz",
|
||||
"integrity": "sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA=="
|
||||
},
|
||||
"tom-select": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tom-select/-/tom-select-2.2.2.tgz",
|
||||
"integrity": "sha512-igGah1yY6yhrnN2h/Ky8I5muw/nE/YQxIsEZoYu5qaA4bsRibvKto3s8QZZosKpOd0uO8fNYhRfAwgHB4IAYew==",
|
||||
"requires": {
|
||||
"@orchidjs/sifter": "^1.0.3",
|
||||
"@orchidjs/unicode-variants": "^1.0.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.4.0",
|
||||
"leaflet": "^1.6.0",
|
||||
"leaflet.markercluster": "^1.4.1"
|
||||
"leaflet.markercluster": "^1.4.1",
|
||||
"tom-select": "^2.2.2"
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,23 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@fortawesome/fontawesome-free@^6.4.0":
|
||||
version "6.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.1.tgz#160a48730d533ec77578ed0141661a8f0150a71d"
|
||||
integrity sha512-ALIk/MOh5gYe1TG/ieS5mVUsk7VUIJTJKPMK9rFFqOgfp0Q3d5QiBXbcOMwUvs37fyZVCz46YjOE6IFeOAXCHA==
|
||||
|
||||
"@orchidjs/sifter@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@orchidjs/sifter/-/sifter-1.1.0.tgz#b36154ad0cda4898305d1ac44f318b41048a0438"
|
||||
integrity sha512-mYwHCfr736cIWWdhhSZvDbf90AKt2xyrJspKFC3qyIJG1LtrJeJunYEqCGG4Aq2ijENbc4WkOjszcvNaIAS/pQ==
|
||||
dependencies:
|
||||
"@orchidjs/unicode-variants" "^1.1.2"
|
||||
|
||||
"@orchidjs/unicode-variants@^1.1.2":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@orchidjs/unicode-variants/-/unicode-variants-1.1.2.tgz#1fd71791a67fdd1591ebe0dcaadd3964537a824e"
|
||||
integrity sha512-5DobW1CHgnBROOEpFlEXytED5OosEWESFvg/VYmH0143oXcijYTprRYJTs+55HzGM4IqxiLFSuqEzu9mPNwVsA==
|
||||
|
||||
leaflet.markercluster@^1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/leaflet.markercluster/-/leaflet.markercluster-1.4.1.tgz#b53f2c4f2ca7306ddab1dbb6f1861d5e8aa6c5e5"
|
||||
@ -11,3 +28,11 @@ leaflet@^1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.6.0.tgz#aecbb044b949ec29469eeb31c77a88e2f448f308"
|
||||
integrity sha512-CPkhyqWUKZKFJ6K8umN5/D2wrJ2+/8UIpXppY7QDnUZW5bZL5+SEI2J7GBpwh4LIupOKqbNSQXgqmrEJopHVNQ==
|
||||
|
||||
tom-select@^2.2.2:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/tom-select/-/tom-select-2.4.1.tgz#6a0b6df8af3df7b09b22dd965eb75ce4d1c547bc"
|
||||
integrity sha512-adI8H8+wk8RRzHYLQ3bXSk2Q+FAq/kzAATrcWlJ2fbIrEzb0VkwaXzKHTAlBwSJrhqbPJvhV/0eypFkED/nAug==
|
||||
dependencies:
|
||||
"@orchidjs/sifter" "^1.1.0"
|
||||
"@orchidjs/unicode-variants" "^1.1.2"
|
||||
|
3
rvr
3
rvr
@ -9,5 +9,8 @@ $app->add(new RVR\Cli\MigrateDatabaseCommand());
|
||||
$app->add(new RVR\Cli\AddUserCommand());
|
||||
$app->add(new RVR\Cli\LinkViewCommand());
|
||||
$app->add(new RVR\Cli\MaintainDatabaseCommand());
|
||||
$app->add(new RVR\Cli\AddOAuthClientCommand());
|
||||
$app->add(new RVR\Cli\AddOAuthRedirectUriCommand());
|
||||
$app->add(new RVR\Cli\RemoveOAuthRedirectUriCommand());
|
||||
|
||||
$app->run();
|
||||
|
@ -1,162 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
# Usage: ./deploy-to-multiple-worktrees.py REPO_PATH WORKTREE_DEVELOPMENT_PATH WORKTREE_PRODUCTION_PATH
|
||||
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
import re
|
||||
|
||||
WORKTREE_REGEX = r"^worktree (.*)\nHEAD ([a-f0-9]*)\n(?:branch refs\/heads\/(.*)|detached)$"
|
||||
|
||||
if len(sys.argv) < 4:
|
||||
print("Usage: ./deploy-to-multiple-worktrees.py REPO_PATH WORKTREE_DEVELOPMENT_PATH WORKTREE_PRODUCTION_PATH")
|
||||
exit(1)
|
||||
|
||||
REPO = os.path.abspath(sys.argv[1])
|
||||
WORKTREE_DEVELOPMENT = os.path.abspath(sys.argv[2])
|
||||
WORKTREE_PRODUCTION = os.path.abspath(sys.argv[3])
|
||||
|
||||
class Worktree:
|
||||
def __init__(self, path, branch, revision, version):
|
||||
self.path = path
|
||||
self.branch = branch
|
||||
self.revision = revision
|
||||
self.version = version
|
||||
self.newRevision = None
|
||||
self.newVersion = None
|
||||
|
||||
def getDataForWorktrees():
|
||||
ret = subprocess.check_output(["git", "worktree", "list", "--porcelain"], cwd=REPO).decode().strip()
|
||||
blocks = ret.split("\n\n")
|
||||
|
||||
worktrees = []
|
||||
|
||||
for block in blocks:
|
||||
matches = re.search(WORKTREE_REGEX, block)
|
||||
|
||||
if matches:
|
||||
path = matches.group(1)
|
||||
revision = matches.group(2)
|
||||
branch = matches.group(3)
|
||||
version = getVersion(revision)
|
||||
|
||||
worktrees.append(Worktree(path, branch, revision, version))
|
||||
|
||||
return worktrees
|
||||
|
||||
def findWorktree(path):
|
||||
for worktree in worktrees:
|
||||
if worktree.path == path:
|
||||
return worktree
|
||||
|
||||
return None
|
||||
|
||||
def getVersion(branch):
|
||||
return subprocess.check_output(["git", "describe", "--tags", "--always", "--match", "Release_*", branch], cwd=REPO).decode().strip()
|
||||
|
||||
def getRevisionForRef(ref):
|
||||
return subprocess.check_output(["git", "rev-list", "-1", ref], cwd=REPO).decode().strip()
|
||||
|
||||
def getLatestReleaseTag():
|
||||
process = subprocess.Popen(["git", "for-each-ref", "refs/tags/Release*", "--sort=-creatordate", "--format=%(refname:short)"], stdout=subprocess.PIPE, cwd=REPO)
|
||||
|
||||
for line in process.stdout:
|
||||
tag = line.decode().rstrip()
|
||||
|
||||
if isTagVerified(tag):
|
||||
return tag
|
||||
|
||||
print(f"[WARNING] Tag '{tag}' is not verified, skipping.")
|
||||
|
||||
raise Exception("No verified 'Release*' tag found!")
|
||||
|
||||
def isTagVerified(tag):
|
||||
process = subprocess.run(["git", "tag", "--verify", tag], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=REPO)
|
||||
|
||||
return process.returncode == 0
|
||||
|
||||
def updateRepoFromRemote():
|
||||
subprocess.call(["git", "fetch", "origin", "--prune", "--prune-tags"], cwd=REPO)
|
||||
|
||||
def checkoutWorktree(worktreePath, ref):
|
||||
subprocess.call(["git", "checkout", "-f", ref], cwd=worktreePath)
|
||||
|
||||
def cleanWorktree(worktreePath):
|
||||
subprocess.call(["git", "clean", "-f", "-d"], cwd=worktreePath)
|
||||
|
||||
def updateAppInWorktree(worktreePath):
|
||||
subprocess.call([worktreePath + "/scripts/update.sh"], cwd=worktreePath)
|
||||
|
||||
def updateAppVersionInWorktree(worktreePath):
|
||||
subprocess.call([worktreePath + "/scripts/update-version.sh"], cwd=worktreePath)
|
||||
|
||||
worktrees = getDataForWorktrees()
|
||||
|
||||
updateRepoFromRemote()
|
||||
|
||||
print("Repo is updated from origin")
|
||||
|
||||
print("----------------------------------------------")
|
||||
print("----------------------------------------------")
|
||||
|
||||
developmentWorktree = findWorktree(WORKTREE_DEVELOPMENT)
|
||||
|
||||
developmentWorktree.newRevision = getRevisionForRef(developmentWorktree.branch)
|
||||
developmentWorktree.newVersion = getVersion(developmentWorktree.revision)
|
||||
|
||||
print("DEVELOPMENT (" + developmentWorktree.path + ") is on branch " + developmentWorktree.branch)
|
||||
print(developmentWorktree.revision + " = " + developmentWorktree.branch + " (" + developmentWorktree.version + ")")
|
||||
print(developmentWorktree.newRevision + " = origin/" + developmentWorktree.branch + " (" + developmentWorktree.newVersion + ")")
|
||||
|
||||
if developmentWorktree.revision != developmentWorktree.newRevision:
|
||||
print("-> DEVELOPMENT (" + developmentWorktree.path + ") will be UPDATED")
|
||||
print("----------------------------------------------")
|
||||
|
||||
checkoutWorktree(developmentWorktree.path, developmentWorktree.branch)
|
||||
cleanWorktree(developmentWorktree.path)
|
||||
|
||||
print(developmentWorktree.path + " is checked out to " + developmentWorktree.branch + " and cleaned")
|
||||
|
||||
updateAppInWorktree(developmentWorktree.path)
|
||||
updateAppVersionInWorktree(developmentWorktree.path)
|
||||
|
||||
print("RVR is updated in " + developmentWorktree.path)
|
||||
elif developmentWorktree.version != developmentWorktree.newVersion:
|
||||
print("-> DEVELOPMENT " + developmentWorktree.path + "'s version info will be UPDATED")
|
||||
|
||||
updateAppVersionInWorktree(developmentWorktree.path)
|
||||
|
||||
print("RVR version is updated in " + developmentWorktree.path)
|
||||
else:
|
||||
print("-> DEVELOPMENT (" + developmentWorktree.path + ") WON'T be updated")
|
||||
|
||||
print("----------------------------------------------")
|
||||
print("----------------------------------------------")
|
||||
|
||||
productionWorktree = findWorktree(WORKTREE_PRODUCTION)
|
||||
|
||||
productionWorktree.newVersion = getLatestReleaseTag()
|
||||
productionWorktree.newRevision = getRevisionForRef(productionWorktree.newVersion)
|
||||
|
||||
print("PRODUCTION (" + productionWorktree.path + ")")
|
||||
print(productionWorktree.revision + " = " + productionWorktree.version)
|
||||
print(productionWorktree.newRevision + " = " + productionWorktree.newVersion)
|
||||
|
||||
if productionWorktree.revision != productionWorktree.newRevision:
|
||||
print("-> PRODUCTION (" + productionWorktree.path + ") will be UPDATED")
|
||||
|
||||
checkoutWorktree(productionWorktree.path, productionWorktree.newRevision)
|
||||
cleanWorktree(productionWorktree.path)
|
||||
|
||||
print(productionWorktree.path + " is checked out to " + productionWorktree.newRevision + " and cleaned")
|
||||
|
||||
updateAppInWorktree(productionWorktree.path)
|
||||
updateAppVersionInWorktree(productionWorktree.path)
|
||||
|
||||
print("RVR is updated in " + productionWorktree.path)
|
||||
else:
|
||||
print("-> PRODUCTION (" + productionWorktree.path + ") WON'T be updated")
|
||||
|
||||
print("----------------------------------------------")
|
||||
print("----------------------------------------------")
|
@ -7,7 +7,7 @@ if [[ "${BRANCH_NAME}" =~ $BRANCH_PATTERN ]]; then
|
||||
TICKET_ID=$(echo $BRANCH_NAME | sed -E "s@$BRANCH_PATTERN@\\2@")
|
||||
|
||||
COMMIT_MESSAGE=$(head -n 1 $1)
|
||||
COMMIT_MESSAGE_REGEX="^$TICKET_ID .*"
|
||||
COMMIT_MESSAGE_REGEX="^(fixup! )?$TICKET_ID .*"
|
||||
|
||||
if [[ ! "${COMMIT_MESSAGE}" =~ $COMMIT_MESSAGE_REGEX ]]; then
|
||||
sed -i.bak -e "1s/^/$TICKET_ID /" $1
|
||||
|
@ -1,35 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
ROOT_DIR=$(dirname $(readlink -f "$0"))/..
|
||||
|
||||
. ${ROOT_DIR}/.env
|
||||
|
||||
if [ -f ${ROOT_DIR}/installed ]; then
|
||||
echo "RVR is already installed! To force reinstall, delete file 'installed' from the root directory!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Installing NPM packages..."
|
||||
(cd ${ROOT_DIR}/multi && npm install)
|
||||
|
||||
echo "Installing Yarn packages..."
|
||||
(cd ${ROOT_DIR}/public/static && yarn install)
|
||||
|
||||
echo "Installing RVR DB..."
|
||||
mysql --host=${DB_HOST} --user=${DB_USER} --password=${DB_PASSWORD} ${DB_NAME} < ${ROOT_DIR}/database/rvr.sql
|
||||
|
||||
echo "Migrating DB..."
|
||||
(cd ${ROOT_DIR} && ./rvr db:migrate)
|
||||
|
||||
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
|
||||
echo "Minifying JS, CSS and SVG files..."
|
||||
${ROOT_DIR}/scripts/minify.sh
|
||||
|
||||
echo "Linking view files..."
|
||||
(cd ${ROOT_DIR} && ./rvr view:link)
|
||||
else
|
||||
echo "Creating the first user..."
|
||||
(cd ${ROOT_DIR} && ./rvr user:add rvr@rvr.dev 123456 admin)
|
||||
fi
|
||||
|
||||
touch ${ROOT_DIR}/installed
|
@ -1,11 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
ROOT_DIR=$(dirname $(readlink -f "$0"))/..
|
||||
|
||||
. ${ROOT_DIR}/.env
|
||||
|
||||
find ${ROOT_DIR}/public/static/js -type f -iname '*.js' -exec uglifyjs {} -c -m -o {} \;
|
||||
|
||||
find ${ROOT_DIR}/public/static/css -type f -iname '*.css' -exec cleancss {} -o {} \;
|
||||
|
||||
find ${ROOT_DIR}/public/static/img -type f -iname '*.svg' -exec svgo {} -o {} \;
|
@ -1,17 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
ROOT_DIR=$(dirname $(readlink -f "$0"))/..
|
||||
|
||||
. ${ROOT_DIR}/.env
|
||||
|
||||
cd ${ROOT_DIR}
|
||||
|
||||
echo "Updating version info..."
|
||||
|
||||
VERSION=$(git describe --tags --always --match "Release_*" HEAD)
|
||||
REVISION=$(git rev-parse --short HEAD)
|
||||
REVISION_DATE=$(git show -s --format=%aI HEAD)
|
||||
|
||||
sed -i -E "s/const VERSION = '(.*)';/const VERSION = '${VERSION}';/" main.php
|
||||
sed -i -E "s/const REVISION = '(.*)';/const REVISION = '${REVISION}';/" main.php
|
||||
sed -i -E "s/const REVISION_DATE = '(.*)';/const REVISION_DATE = '${REVISION_DATE}';/" main.php
|
@ -1,29 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
ROOT_DIR=$(dirname $(readlink -f "$0"))/..
|
||||
|
||||
. ${ROOT_DIR}/.env
|
||||
|
||||
echo "Installing Composer packages..."
|
||||
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
|
||||
(cd ${ROOT_DIR} && composer install --no-dev)
|
||||
else
|
||||
(cd ${ROOT_DIR} && composer install --dev)
|
||||
fi
|
||||
|
||||
echo "Installing NPM packages..."
|
||||
(cd ${ROOT_DIR}/multi && npm install)
|
||||
|
||||
echo "Installing Yarn packages..."
|
||||
(cd ${ROOT_DIR}/public/static && yarn install)
|
||||
|
||||
echo "Migrating DB..."
|
||||
(cd ${ROOT_DIR} && ./rvr db:migrate)
|
||||
|
||||
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
|
||||
echo "Minifying JS, CSS and SVG files..."
|
||||
${ROOT_DIR}/scripts/minify.sh
|
||||
|
||||
echo "Linking view files..."
|
||||
(cd ${ROOT_DIR} && ./rvr view:link)
|
||||
fi
|
52
src/Cli/AddOAuthClientCommand.php
Normal file
52
src/Cli/AddOAuthClientCommand.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?php namespace RVR\Cli;
|
||||
|
||||
use DateTime;
|
||||
use RVR\PersistentData\Model\OAuthClient;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class AddOAuthClientCommand extends Command
|
||||
{
|
||||
public function configure(): void
|
||||
{
|
||||
$this->setName('oauth:add-client')
|
||||
->setDescription('Adding of OAuth client.')
|
||||
->addArgument('client-id', InputArgument::OPTIONAL, 'Client ID')
|
||||
->addArgument('preapproved', InputArgument::OPTIONAL, 'Preapproved');
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$clientId = $input->getArgument('client-id') ? $input->getArgument('client-id') : bin2hex(random_bytes(8));
|
||||
$clientSecret = bin2hex(random_bytes(20));
|
||||
|
||||
$oAuthClient = new OAuthClient();
|
||||
$oAuthClient->setClientId($clientId);
|
||||
$oAuthClient->setClientSecret($clientSecret);
|
||||
$oAuthClient->setCreatedDate(new DateTime());
|
||||
|
||||
if ($input->getArgument('preapproved')) {
|
||||
$oAuthClient->setPreapproved($input->getArgument('preapproved'));
|
||||
}
|
||||
|
||||
try {
|
||||
\Container::$persistentDataManager->saveToDb($oAuthClient);
|
||||
} catch (\Exception $e) {
|
||||
$output->writeln('<error>Adding OAuth client failed!</error>');
|
||||
$output->writeln('');
|
||||
|
||||
$output->writeln((string) $e);
|
||||
$output->writeln('');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$output->writeln('<info>OAuth client was successfully added!</info>');
|
||||
$output->writeln('<info>Client ID: ' . $clientId . '</info>');
|
||||
$output->writeln('<info>Client secret: ' . $clientSecret . '</info>');
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
52
src/Cli/AddOAuthRedirectUriCommand.php
Normal file
52
src/Cli/AddOAuthRedirectUriCommand.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?php namespace RVR\Cli;
|
||||
|
||||
use RVR\Repository\OAuthClientRepository;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class AddOAuthRedirectUriCommand extends Command
|
||||
{
|
||||
public function configure(): void
|
||||
{
|
||||
$this->setName('oauth:add-redirect-uri')
|
||||
->setDescription('Adding of redirect URI for OAuth client.')
|
||||
->addArgument('client_id', InputArgument::REQUIRED, 'The OAuth client ID')
|
||||
->addArgument('redirect_uris', InputArgument::IS_ARRAY, 'Redirect URIs to add');
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$oAuthClientRepository = new OAuthClientRepository();
|
||||
$oAuthClient = $oAuthClientRepository->getByClientId($input->getArgument('client_id'));
|
||||
|
||||
if ($oAuthClient === null) {
|
||||
$output->writeln('<error>OAuth client does not exist!</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$redirectUris = array_unique(array_merge($oAuthClient->getRedirectUrisArray(), $input->getArgument('redirect_uris')));
|
||||
|
||||
$oAuthClient->setRedirectUrisArray($redirectUris);
|
||||
|
||||
try {
|
||||
\Container::$persistentDataManager->saveToDb($oAuthClient);
|
||||
} catch (\Exception $e) {
|
||||
$output->writeln('<error>Adding redirect URI failed!</error>');
|
||||
$output->writeln('');
|
||||
|
||||
$output->writeln((string) $e);
|
||||
$output->writeln('');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$redirectUrisToPrint = [];
|
||||
foreach ($redirectUris as $redirectUri) $redirectUrisToPrint[] = '* ' . $redirectUri;
|
||||
|
||||
$output->writeln('<info>Redirect URIS were successfully added! Current URIs:' . "\n" . implode("\n", $redirectUrisToPrint) . '</info>');
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
<?php namespace RVR\Cli;
|
||||
|
||||
use DateTime;
|
||||
use SokoWeb\PersistentData\PersistentDataManager;
|
||||
use RVR\PersistentData\Model\User;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
@ -21,6 +20,11 @@ class AddUserCommand extends Command
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
if (!filter_var($input->getArgument('email'), FILTER_VALIDATE_EMAIL)) {
|
||||
$output->writeln('<error>Please provide a valid email address.</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$user = new User();
|
||||
$user->setEmail($input->getArgument('email'));
|
||||
$user->setPlainPassword($input->getArgument('password'));
|
||||
@ -31,8 +35,7 @@ class AddUserCommand extends Command
|
||||
}
|
||||
|
||||
try {
|
||||
$pdm = new PersistentDataManager();
|
||||
$pdm->saveToDb($user);
|
||||
\Container::$persistentDataManager->saveToDb($user);
|
||||
} catch (\Exception $e) {
|
||||
$output->writeln('<error>Adding user failed!</error>');
|
||||
$output->writeln('');
|
||||
|
@ -4,24 +4,28 @@ use DateTime;
|
||||
use SokoWeb\Database\Query\Modify;
|
||||
use SokoWeb\Database\Query\Select;
|
||||
use SokoWeb\Interfaces\Database\IResultSet;
|
||||
use SokoWeb\PersistentData\PersistentDataManager;
|
||||
use RVR\Repository\UserPasswordResetterRepository;
|
||||
use RVR\Repository\OAuthTokenRepository;
|
||||
use RVR\Repository\OAuthSessionRepository;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class MaintainDatabaseCommand extends Command
|
||||
{
|
||||
private PersistentDataManager $pdm;
|
||||
|
||||
private UserPasswordResetterRepository $userPasswordResetterRepository;
|
||||
|
||||
private OAuthTokenRepository $oauthTokenRepository;
|
||||
|
||||
private OAuthSessionRepository $oauthSessionRepository;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->pdm = new PersistentDataManager();
|
||||
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
|
||||
$this->oauthTokenRepository = new OAuthTokenRepository();
|
||||
$this->oauthSessionRepository = new OAuthSessionRepository();
|
||||
}
|
||||
|
||||
public function configure(): void
|
||||
@ -35,6 +39,8 @@ class MaintainDatabaseCommand extends Command
|
||||
try {
|
||||
$this->deleteExpiredPasswordResetters();
|
||||
$this->deleteExpiredSessions();
|
||||
$this->deleteExpiredOauthTokens();
|
||||
$this->deleteExpiredOauthSessions();
|
||||
} catch (\Exception $e) {
|
||||
$output->writeln('<error>Maintenance failed!</error>');
|
||||
$output->writeln('');
|
||||
@ -54,7 +60,7 @@ class MaintainDatabaseCommand extends Command
|
||||
private function deleteExpiredPasswordResetters(): void
|
||||
{
|
||||
foreach ($this->userPasswordResetterRepository->getAllExpired() as $passwordResetter) {
|
||||
$this->pdm->deleteFromDb($passwordResetter);
|
||||
\Container::$persistentDataManager->deleteFromDb($passwordResetter);
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,7 +69,7 @@ class MaintainDatabaseCommand extends Command
|
||||
//TODO: model may be used for sessions too
|
||||
$select = new Select(\Container::$dbConnection, 'sessions');
|
||||
$select->columns(['id']);
|
||||
$select->where('updated', '<', (new DateTime('-7 days'))->format('Y-m-d H:i:s'));
|
||||
$select->where('updated', '<', (new DateTime('-1 days'))->format('Y-m-d H:i:s'));
|
||||
|
||||
$result = $select->execute();
|
||||
|
||||
@ -73,4 +79,21 @@ class MaintainDatabaseCommand extends Command
|
||||
$modify->delete();
|
||||
}
|
||||
}
|
||||
|
||||
private function deleteExpiredOauthTokens(): void
|
||||
{
|
||||
foreach ($this->oauthTokenRepository->getAllExpired() as $oauthToken) {
|
||||
\Container::$persistentDataManager->deleteFromDb($oauthToken);
|
||||
}
|
||||
}
|
||||
|
||||
private function deleteExpiredOauthSessions(): void
|
||||
{
|
||||
foreach ($this->oauthSessionRepository->getAllExpired() as $oauthSession) {
|
||||
if ($this->oauthTokenRepository->countAllBySession($oauthSession) > 0) {
|
||||
continue;
|
||||
}
|
||||
\Container::$persistentDataManager->deleteFromDb($oauthSession);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,8 @@ class MigrateDatabaseCommand extends Command
|
||||
{
|
||||
$db = \Container::$dbConnection;
|
||||
|
||||
$this->createBaseDb();
|
||||
|
||||
$db->startTransaction();
|
||||
|
||||
$success = [];
|
||||
@ -62,10 +64,8 @@ class MigrateDatabaseCommand extends Command
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function readDir(string $type): array
|
||||
private function createBaseDb()
|
||||
{
|
||||
$done = [];
|
||||
|
||||
$migrationTableExists = \Container::$dbConnection->query('SELECT count(*)
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = \'' . $_ENV['DB_NAME'] . '\'
|
||||
@ -73,16 +73,25 @@ class MigrateDatabaseCommand extends Command
|
||||
->fetch(IResultSet::FETCH_NUM)[0];
|
||||
|
||||
if ($migrationTableExists != 0) {
|
||||
$select = new Select(\Container::$dbConnection, 'migrations');
|
||||
$select->columns(['migration']);
|
||||
$select->where('type', '=', $type);
|
||||
$select->orderBy('migration');
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $select->execute();
|
||||
\Container::$dbConnection->multiQuery(file_get_contents(ROOT . '/database/rvr.sql'));
|
||||
}
|
||||
|
||||
while ($migration = $result->fetch(IResultSet::FETCH_ASSOC)) {
|
||||
$done[] = $migration['migration'];
|
||||
}
|
||||
private function readDir(string $type): array
|
||||
{
|
||||
$done = [];
|
||||
|
||||
$select = new Select(\Container::$dbConnection, 'migrations');
|
||||
$select->columns(['migration']);
|
||||
$select->where('type', '=', $type);
|
||||
$select->orderBy('migration');
|
||||
|
||||
$result = $select->execute();
|
||||
|
||||
while ($migration = $result->fetch(IResultSet::FETCH_ASSOC)) {
|
||||
$done[] = $migration['migration'];
|
||||
}
|
||||
|
||||
$path = ROOT . '/database/migrations/' . $type;
|
||||
@ -96,7 +105,7 @@ class MigrateDatabaseCommand extends Command
|
||||
while ($file = readdir($dir)) {
|
||||
$filePath = $path . '/' . $file;
|
||||
|
||||
if (!is_file($filePath) || in_array(pathinfo($file, PATHINFO_FILENAME), $done)) {
|
||||
if (!is_file($filePath) || $file == '.gitkeep' || in_array(pathinfo($file, PATHINFO_FILENAME), $done)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
52
src/Cli/RemoveOAuthRedirectUriCommand.php
Normal file
52
src/Cli/RemoveOAuthRedirectUriCommand.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?php namespace RVR\Cli;
|
||||
|
||||
use RVR\Repository\OAuthClientRepository;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class RemoveOAuthRedirectUriCommand extends Command
|
||||
{
|
||||
public function configure(): void
|
||||
{
|
||||
$this->setName('oauth:remove-redirect-uri')
|
||||
->setDescription('Removing of redirect URI for OAuth client.')
|
||||
->addArgument('client_id', InputArgument::REQUIRED, 'The OAuth client ID')
|
||||
->addArgument('redirect_uris', InputArgument::IS_ARRAY, 'Redirect URIs to remove');
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$oAuthClientRepository = new OAuthClientRepository();
|
||||
$oAuthClient = $oAuthClientRepository->getByClientId($input->getArgument('client_id'));
|
||||
|
||||
if ($oAuthClient === null) {
|
||||
$output->writeln('<error>OAuth client does not exist!</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$redirectUris = array_diff($oAuthClient->getRedirectUrisArray(), $input->getArgument('redirect_uris'));
|
||||
|
||||
$oAuthClient->setRedirectUrisArray($redirectUris);
|
||||
|
||||
try {
|
||||
\Container::$persistentDataManager->saveToDb($oAuthClient);
|
||||
} catch (\Exception $e) {
|
||||
$output->writeln('<error>Removing redirect URI failed!</error>');
|
||||
$output->writeln('');
|
||||
|
||||
$output->writeln((string) $e);
|
||||
$output->writeln('');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$redirectUrisToPrint = [];
|
||||
foreach ($redirectUris as $redirectUri) $redirectUrisToPrint[] = '* ' . $redirectUri;
|
||||
|
||||
$output->writeln('<info>Redirect URIS were successfully removed! Current URIs:' . "\n" . implode("\n", $redirectUrisToPrint) . '</info>');
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
470
src/Controller/CommunityController.php
Normal file
470
src/Controller/CommunityController.php
Normal file
@ -0,0 +1,470 @@
|
||||
<?php namespace RVR\Controller;
|
||||
|
||||
use DateTime;
|
||||
use RVR\Finance\BalanceCalculator;
|
||||
use RVR\PersistentData\Model\Community;
|
||||
use RVR\PersistentData\Model\CommunityMember;
|
||||
use RVR\PersistentData\Model\Currency;
|
||||
use RVR\PersistentData\Model\CurrencyExchangeRate;
|
||||
use RVR\PersistentData\Model\User;
|
||||
use RVR\Repository\CommunityRepository;
|
||||
use RVR\Repository\CommunityMemberRepository;
|
||||
use RVR\Repository\CurrencyExchangeRateRepository;
|
||||
use RVR\Repository\CurrencyRepository;
|
||||
use RVR\Repository\EventRepository;
|
||||
use RVR\Repository\TransactionRepository;
|
||||
use RVR\Repository\UserRepository;
|
||||
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
|
||||
use SokoWeb\Interfaces\Response\IContent;
|
||||
use SokoWeb\Response\HtmlContent;
|
||||
use SokoWeb\Response\JsonContent;
|
||||
|
||||
class CommunityController implements IAuthenticationRequired
|
||||
{
|
||||
private UserRepository $userRepository;
|
||||
|
||||
private CommunityRepository $communityRepository;
|
||||
|
||||
private CommunityMemberRepository $communityMemberRepository;
|
||||
|
||||
private CurrencyRepository $currencyRepository;
|
||||
|
||||
private CurrencyExchangeRateRepository $currencyExchangeRatesRepository;
|
||||
|
||||
private TransactionRepository $transactionRepository;
|
||||
|
||||
private EventRepository $eventRepository;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->userRepository = new UserRepository();
|
||||
$this->communityRepository = new CommunityRepository();
|
||||
$this->communityMemberRepository = new CommunityMemberRepository();
|
||||
$this->currencyRepository = new CurrencyRepository();
|
||||
$this->currencyExchangeRatesRepository = new CurrencyExchangeRateRepository();
|
||||
$this->transactionRepository = new TransactionRepository();
|
||||
$this->eventRepository = new EventRepository();
|
||||
}
|
||||
|
||||
public function isAuthenticationRequired(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getCommunityHome(): ?IContent
|
||||
{
|
||||
if (!$this->checkPermission(\Container::$request->query('communitySlug'), false, $community, $ownCommunityMember)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
\Container::$persistentDataManager->loadRelationsFromDb($community, false, ['main_currency']);
|
||||
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = \Container::$request->user();
|
||||
$balanceCalculator = new BalanceCalculator($community, $user);
|
||||
$balance = $balanceCalculator->calculate();
|
||||
|
||||
return new HtmlContent('communities/community', [
|
||||
'community' => $community,
|
||||
'upcomingAndRecentEvents' => iterator_to_array($this->eventRepository->getUpcomingAndRecentByCommunity($community, new DateTime(), 30, 3)),
|
||||
'debtItems' => $balance['debtItems'],
|
||||
'debtBalance' => $balance['debtBalance'],
|
||||
'outstandingItems' => $balance['outstandingItems'],
|
||||
'outstandingBalance' => $balance['outstandingBalance'],
|
||||
'balance' => $balance['absoluteBalance'],
|
||||
'editPermission' => $ownCommunityMember->getOwner()
|
||||
]);
|
||||
}
|
||||
|
||||
public function getCommunitySettings(): ?IContent
|
||||
{
|
||||
if (!$this->checkPermission(\Container::$request->query('communitySlug'), false, $community, $ownCommunityMember)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new HtmlContent('communities/community_settings', [
|
||||
'community' => $community,
|
||||
'members' => $this->getMembers($community),
|
||||
'currencies' => $this->getCurrencies($community),
|
||||
'editPermission' => $ownCommunityMember->getOwner()
|
||||
]);
|
||||
}
|
||||
|
||||
public function getCommunityNew(): IContent
|
||||
{
|
||||
return new HtmlContent('communities/community_edit');
|
||||
}
|
||||
|
||||
public function getCommunityEdit(): ?IContent
|
||||
{
|
||||
if (!$this->checkPermission(\Container::$request->query('communitySlug'), true, $community, $ownCommunityMember)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new HtmlContent('communities/community_edit', [
|
||||
'community' => $community
|
||||
]);
|
||||
}
|
||||
|
||||
public function saveCommunity(): ?IContent
|
||||
{
|
||||
$name = \Container::$request->post('name');
|
||||
if (strlen($name) === 0) {
|
||||
return new JsonContent([
|
||||
'error' => ['errorText' => 'Please fill all required fields!']
|
||||
]);
|
||||
}
|
||||
|
||||
$communitySlug = \Container::$request->query('communitySlug');
|
||||
if ($communitySlug){
|
||||
if (!$this->checkPermission($communitySlug, true, $community, $ownCommunityMember)) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
$mainCurrencyCode = \Container::$request->post('main_currency_code');
|
||||
$mainCurrencyRoundDigits = \Container::$request->post('main_currency_round_digits');
|
||||
if (strlen($mainCurrencyCode) === 0 || strlen($mainCurrencyCode) > 3 || $mainCurrencyRoundDigits < 0 || $mainCurrencyRoundDigits > 9) {
|
||||
return new JsonContent([
|
||||
'error' => ['errorText' => 'Please fill all required fields!']
|
||||
]);
|
||||
}
|
||||
|
||||
$community = new Community();
|
||||
$community->setCreatedDate(new DateTime());
|
||||
}
|
||||
|
||||
$community->setName($name);
|
||||
\Container::$persistentDataManager->saveToDb($community);
|
||||
|
||||
if (!$communitySlug) {
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = \Container::$request->user();
|
||||
|
||||
$communityMember = new CommunityMember();
|
||||
$communityMember->setCommunity($community);
|
||||
$communityMember->setUser($user);
|
||||
$communityMember->setOwner(true);
|
||||
\Container::$persistentDataManager->saveToDb($communityMember);
|
||||
|
||||
$mainCurrency = new Currency();
|
||||
$mainCurrency->setCommunity($community);
|
||||
$mainCurrency->setCode($mainCurrencyCode);
|
||||
$mainCurrency->setRoundDigits($mainCurrencyRoundDigits);
|
||||
\Container::$persistentDataManager->saveToDb($mainCurrency);
|
||||
|
||||
$community->setMainCurrency($mainCurrency);
|
||||
\Container::$persistentDataManager->saveToDb($community);
|
||||
}
|
||||
|
||||
return new JsonContent([
|
||||
'redirect' => ['target' => \Container::$routeCollection->getRoute('community')->generateLink(['communitySlug' => $community->getSlug()])]
|
||||
]);
|
||||
}
|
||||
|
||||
public function deleteCommunity(): ?IContent
|
||||
{
|
||||
if (!$this->checkPermission(\Container::$request->query('communitySlug'), true, $community, $ownCommunityMember)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->transactionRepository->countAllByCommunity($community) > 0) {
|
||||
return new JsonContent([
|
||||
'error' => ['errorText' => 'There are transactions for this community!']
|
||||
]);
|
||||
}
|
||||
|
||||
if ($this->eventRepository->countAllByCommunity($community) > 0) {
|
||||
return new JsonContent([
|
||||
'error' => ['errorText' => 'There are events for this community!']
|
||||
]);
|
||||
}
|
||||
|
||||
foreach ($this->communityMemberRepository->getAllByCommunity($community) as $communityMember) {
|
||||
\Container::$persistentDataManager->deleteFromDb($communityMember);
|
||||
}
|
||||
|
||||
$community->setMainCurrencyId(null);
|
||||
\Container::$persistentDataManager->saveToDb($community);
|
||||
|
||||
foreach ($this->currencyRepository->getAllByCommunity($community) as $currency) {
|
||||
foreach ($this->currencyExchangeRatesRepository->getAllByCurrency($currency) as $currencyExchangeRate) {
|
||||
\Container::$persistentDataManager->deleteFromDb($currencyExchangeRate);
|
||||
}
|
||||
\Container::$persistentDataManager->deleteFromDb($currency);
|
||||
}
|
||||
|
||||
\Container::$persistentDataManager->deleteFromDb($community);
|
||||
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
public function getMembersEdit(): ?IContent
|
||||
{
|
||||
if (!$this->checkPermission(\Container::$request->query('communitySlug'), true, $community, $ownCommunityMember)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new HtmlContent('communities/community_members', [
|
||||
'community' => $community,
|
||||
'members' => $this->getMembers($community)
|
||||
]);
|
||||
}
|
||||
|
||||
public function saveMember(): ?IContent
|
||||
{
|
||||
if (!$this->checkPermission(\Container::$request->query('communitySlug'), true, $community, $ownCommunityMember)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$communityMemberId = \Container::$request->query('community_member_id');
|
||||
if ($communityMemberId) {
|
||||
$communityMember = $this->communityMemberRepository->getById($communityMemberId);
|
||||
if ($communityMember->getUserId() === $ownCommunityMember->getUserId()) {
|
||||
return new JsonContent([
|
||||
'error' => ['errorText' => 'Own user cannot be edited.']
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
if ($this->transactionRepository->isAnyCommon()) {
|
||||
return new JsonContent([
|
||||
'error' => ['errorText' => 'There are transactions with common payee!']
|
||||
]);
|
||||
}
|
||||
|
||||
$user = $this->userRepository->getById(\Container::$request->post('user_id'));
|
||||
if ($this->communityMemberRepository->getByCommunityAndUser($community, $user) !== null) {
|
||||
return new JsonContent([
|
||||
'error' => ['errorText' => 'This user is already a member of this community.']
|
||||
]);
|
||||
}
|
||||
|
||||
$communityMember = new CommunityMember();
|
||||
$communityMember->setCommunity($community);
|
||||
$communityMember->setUser($user);
|
||||
}
|
||||
|
||||
$communityMember->setOwner((bool)\Container::$request->post('owner'));
|
||||
\Container::$persistentDataManager->saveToDb($communityMember);
|
||||
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
public function deleteMember(): ?IContent
|
||||
{
|
||||
if (!$this->checkPermission(\Container::$request->query('communitySlug'), true, $community, $ownCommunityMember)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$communityMember = $this->communityMemberRepository->getById(\Container::$request->query('community_member_id'));
|
||||
if ($communityMember->getUserId() === \Container::$request->user()->getUniqueId()) {
|
||||
return new JsonContent([
|
||||
'error' => ['errorText' => 'Own user cannot be deleted.']
|
||||
]);
|
||||
}
|
||||
|
||||
\Container::$persistentDataManager->loadRelationsFromDb($communityMember, false, ['user']);
|
||||
if ($this->transactionRepository->isAnyForUser($communityMember->getUser())) {
|
||||
return new JsonContent([
|
||||
'error' => ['errorText' => 'There are transactions where the member is payer or payee!']
|
||||
]);
|
||||
}
|
||||
|
||||
\Container::$persistentDataManager->deleteFromDb($communityMember);
|
||||
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
public function getCurrenciesEdit(): ?IContent
|
||||
{
|
||||
if (!$this->checkPermission(\Container::$request->query('communitySlug'), true, $community, $ownCommunityMember)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new HtmlContent('communities/community_currencies', [
|
||||
'community' => $community,
|
||||
'currencies' => $this->getCurrencies($community)
|
||||
]);
|
||||
}
|
||||
|
||||
public function saveCurrency(): ?IContent
|
||||
{
|
||||
if (!$this->checkPermission(\Container::$request->query('communitySlug'), true, $community, $ownCommunityMember)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$code = \Container::$request->post('code');
|
||||
$roundDigits = (int)\Container::$request->post('round_digits');
|
||||
if (strlen($code) === 0 || strlen($code) > 3 || $roundDigits < 0 || $roundDigits > 9) {
|
||||
return new JsonContent([
|
||||
'error' => ['errorText' => 'Please fill all required fields!']
|
||||
]);
|
||||
}
|
||||
|
||||
$currencyId = \Container::$request->query('currency_id');
|
||||
if ($currencyId){
|
||||
$currency = $this->currencyRepository->getById($currencyId);
|
||||
} else {
|
||||
$currency = new Currency();
|
||||
$currency->setCommunity($community);
|
||||
}
|
||||
|
||||
$existingCurrency = $this->currencyRepository->getByCommunityAndCurrencyCode($community, $code);
|
||||
if ($existingCurrency !== null && $currency->getId() !== $existingCurrency->getId()) {
|
||||
return new JsonContent([
|
||||
'error' => ['errorText' => 'A currency with the same code exists for this community.']
|
||||
]);
|
||||
}
|
||||
|
||||
$currency->setCode($code);
|
||||
$currency->setRoundDigits($roundDigits);
|
||||
\Container::$persistentDataManager->saveToDb($currency);
|
||||
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
public function deleteCurrency(): ?IContent
|
||||
{
|
||||
if (!$this->checkPermission(\Container::$request->query('communitySlug'), true, $community, $ownCommunityMember)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$currency = $this->currencyRepository->getById(\Container::$request->query('currency_id'));
|
||||
if ($currency->getId() === $community->getMainCurrencyId()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->transactionRepository->isAnyForCurrency($currency)) {
|
||||
return new JsonContent([
|
||||
'error' => ['errorText' => 'There are transactions with this currency!']
|
||||
]);
|
||||
}
|
||||
|
||||
foreach ($this->currencyExchangeRatesRepository->getAllByCurrency($currency) as $currencyExchangeRate) {
|
||||
\Container::$persistentDataManager->deleteFromDb($currencyExchangeRate);
|
||||
}
|
||||
|
||||
\Container::$persistentDataManager->deleteFromDb($currency);
|
||||
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
public function getCurrencyExchangeRates(): ?IContent
|
||||
{
|
||||
if (!$this->checkPermission(\Container::$request->query('communitySlug'), true, $community, $ownCommunityMember)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$currency = $this->currencyRepository->getByCommunityAndCurrencyCode($community, \Container::$request->query('code'));
|
||||
if ($currency === null || $currency->getId() === $community->getMainCurrencyId()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$currencyExchangeRates = $this->currencyExchangeRatesRepository->getAllByCurrency($currency);
|
||||
|
||||
return new HtmlContent('communities/currency_exchange_rates', [
|
||||
'community' => $community,
|
||||
'currency' => $currency,
|
||||
'currencyExchangeRates' => $currencyExchangeRates,
|
||||
'editPermission' => $ownCommunityMember->getOwner()
|
||||
]);
|
||||
}
|
||||
|
||||
public function saveCurrencyExchangeRate(): ?IContent
|
||||
{
|
||||
if (!$this->checkPermission(\Container::$request->query('communitySlug'), true, $community, $ownCommunityMember)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$currency = $this->currencyRepository->getByCommunityAndCurrencyCode($community, \Container::$request->query('code'));
|
||||
if ($currency === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$exchangeRate = (float)\Container::$request->post('exchange_rate');
|
||||
if ($exchangeRate < 0) {
|
||||
return new JsonContent([
|
||||
'error' => ['errorText' => 'Please fill all required fields!']
|
||||
]);
|
||||
}
|
||||
|
||||
$currencyExchangeRateId = \Container::$request->query('currency_exchange_rate_id');
|
||||
if ($currencyExchangeRateId){
|
||||
$currencyExchangeRate = $this->currencyExchangeRatesRepository->getById($currencyExchangeRateId);
|
||||
} else {
|
||||
$currencyExchangeRate = new CurrencyExchangeRate();
|
||||
$currencyExchangeRate->setCurrency($currency);
|
||||
}
|
||||
|
||||
$currencyExchangeRate->setExchangeRate($exchangeRate);
|
||||
$currencyExchangeRate->setValidFromDate(new DateTime(\Container::$request->post('valid_from')));
|
||||
\Container::$persistentDataManager->saveToDb($currencyExchangeRate);
|
||||
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
public function deleteCurrencyExchangeRate(): ?IContent
|
||||
{
|
||||
if (!$this->checkPermission(\Container::$request->query('communitySlug'), true, $community, $ownCommunityMember)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$currency = $this->currencyRepository->getByCommunityAndCurrencyCode($community, \Container::$request->query('code'));
|
||||
if ($currency === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$currencyExchangeRate = $this->currencyExchangeRatesRepository->getById(\Container::$request->query('currency_exchange_rate_id'));
|
||||
\Container::$persistentDataManager->deleteFromDb($currencyExchangeRate);
|
||||
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
private function getMembers(Community $community): array
|
||||
{
|
||||
$members = iterator_to_array($this->communityMemberRepository->getAllByCommunity($community, true, ['user']));
|
||||
usort($members, function($a, $b) {
|
||||
return strnatcmp($a->getUser()->getDisplayName(), $b->getUser()->getDisplayName());
|
||||
});
|
||||
return $members;
|
||||
}
|
||||
|
||||
private function getCurrencies(Community $community): array
|
||||
{
|
||||
$currencies = iterator_to_array($this->currencyRepository->getAllByCommunity($community));
|
||||
usort($currencies, function($a, $b) {
|
||||
return strnatcmp($a->getCode(), $b->getCode());
|
||||
});
|
||||
usort($currencies, function($a, $b) use ($community) {
|
||||
return (int)($b->getId() === $community->getMainCurrencyId()) - (int)($a->getId() === $community->getMainCurrencyId());
|
||||
});
|
||||
return $currencies;
|
||||
}
|
||||
|
||||
private function checkPermission(
|
||||
string $communitySlug,
|
||||
bool $needToBeOwner,
|
||||
?Community &$community,
|
||||
?CommunityMember &$ownCommunityMember): bool
|
||||
{
|
||||
$community = $this->communityRepository->getBySlug($communitySlug);
|
||||
if ($community === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = \Container::$request->user();
|
||||
|
||||
$ownCommunityMember = $this->communityMemberRepository->getByCommunityAndUser($community, $user);
|
||||
if ($ownCommunityMember === null || ($needToBeOwner && !$ownCommunityMember->getOwner())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
193
src/Controller/EventController.php
Normal file
193
src/Controller/EventController.php
Normal file
@ -0,0 +1,193 @@
|
||||
<?php namespace RVR\Controller;
|
||||
|
||||
use Container;
|
||||
use DateTime;
|
||||
use RVR\Finance\BalanceCalculator;
|
||||
use RVR\Finance\ExchangeRateCalculator;
|
||||
use RVR\PersistentData\Model\Community;
|
||||
use RVR\PersistentData\Model\CommunityMember;
|
||||
use RVR\PersistentData\Model\Event;
|
||||
use RVR\PersistentData\Model\User;
|
||||
use RVR\Repository\CommunityMemberRepository;
|
||||
use RVR\Repository\CommunityRepository;
|
||||
use RVR\Repository\EventRepository;
|
||||
use RVR\Repository\TransactionRepository;
|
||||
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
|
||||
use SokoWeb\Interfaces\Authorization\ISecured;
|
||||
use SokoWeb\Interfaces\Response\IContent;
|
||||
use SokoWeb\Response\HtmlContent;
|
||||
use SokoWeb\Response\JsonContent;
|
||||
|
||||
class EventController implements IAuthenticationRequired, ISecured
|
||||
{
|
||||
private CommunityRepository $communityRepository;
|
||||
|
||||
private CommunityMemberRepository $communityMemberRepository;
|
||||
|
||||
private EventRepository $eventRepository;
|
||||
|
||||
private TransactionRepository $transactionRepository;
|
||||
|
||||
private ?Community $community;
|
||||
|
||||
private ?CommunityMember $ownCommunityMember;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->communityRepository = new CommunityRepository();
|
||||
$this->communityMemberRepository = new CommunityMemberRepository();
|
||||
$this->eventRepository = new EventRepository();
|
||||
$this->transactionRepository = new TransactionRepository();
|
||||
}
|
||||
|
||||
public function isAuthenticationRequired(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
$communitySlug = \Container::$request->query('communitySlug');
|
||||
$this->community = $this->communityRepository->getBySlug($communitySlug);
|
||||
if ($this->community === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = \Container::$request->user();
|
||||
$this->ownCommunityMember = $this->communityMemberRepository->getByCommunityAndUser($this->community, $user);
|
||||
if ($this->ownCommunityMember === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getEvents(): IContent
|
||||
{
|
||||
$itemsPerPage = 10;
|
||||
$numberOfEvents = $this->eventRepository->countAllByCommunity($this->community);
|
||||
$currentPage = Container::$request->query('page') ?: 1;
|
||||
$events = $this->eventRepository->getPagedByCommunity(
|
||||
$this->community,
|
||||
$currentPage,
|
||||
$itemsPerPage
|
||||
);
|
||||
|
||||
return new HtmlContent('events/events', [
|
||||
'community' => $this->community,
|
||||
'pages' => ceil($numberOfEvents / $itemsPerPage),
|
||||
'currentPage' => $currentPage,
|
||||
'numberOfEvents' => $numberOfEvents,
|
||||
'events' => $events
|
||||
]);
|
||||
}
|
||||
|
||||
public function searchEvent(): IContent
|
||||
{
|
||||
$events = iterator_to_array($this->eventRepository->searchByTitle($this->community, Container::$request->query('q')));
|
||||
$results = [];
|
||||
foreach ($events as $event) {
|
||||
$results[] = ['value' => $event->getId(), 'label' => $event->getTitle()];
|
||||
}
|
||||
|
||||
return new JsonContent([
|
||||
'results' => $results
|
||||
]);
|
||||
}
|
||||
|
||||
public function getEvent(): ?IContent
|
||||
{
|
||||
$event = $this->eventRepository->getBySlug(Container::$request->query('eventSlug'));
|
||||
if (!$event) {
|
||||
return null;
|
||||
}
|
||||
Container::$persistentDataManager->loadRelationsFromDb($this->community, true, ['main_currency']);
|
||||
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = \Container::$request->user();
|
||||
$balanceCalculator = new BalanceCalculator($this->community, $user, $event);
|
||||
$balance = $balanceCalculator->calculate();
|
||||
|
||||
return new HtmlContent('events/event', [
|
||||
'community' => $this->community,
|
||||
'event' => $event,
|
||||
'totalCost' => $this->sumTransactions($event),
|
||||
'debtItems' => $balance['debtItems'],
|
||||
'debtBalance' => $balance['debtBalance'],
|
||||
'outstandingItems' => $balance['outstandingItems'],
|
||||
'outstandingBalance' => $balance['outstandingBalance'],
|
||||
'balance' => $balance['absoluteBalance'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function getEventEdit(): ?IContent
|
||||
{
|
||||
$eventSlug = Container::$request->query('eventSlug');
|
||||
if ($eventSlug) {
|
||||
$event = $this->eventRepository->getBySlug($eventSlug);
|
||||
if ($event === null) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
$event = null;
|
||||
}
|
||||
|
||||
return new HtmlContent('events/event_edit', [
|
||||
'community' => $this->community,
|
||||
'event' => $event
|
||||
]);
|
||||
}
|
||||
|
||||
public function saveEvent(): ?IContent
|
||||
{
|
||||
$eventSlug = Container::$request->query('eventSlug');
|
||||
if ($eventSlug) {
|
||||
$event = $this->eventRepository->getBySlug($eventSlug);
|
||||
} else {
|
||||
$event = new Event();
|
||||
$event->setCommunity($this->community);
|
||||
}
|
||||
|
||||
$event->setTitle(Container::$request->post('title'));
|
||||
$event->setDescription(Container::$request->post('description'));
|
||||
$event->setStartDate(new DateTime(Container::$request->post('start')));
|
||||
$event->setEndDate(new DateTime(Container::$request->post('end')));
|
||||
Container::$persistentDataManager->saveToDb($event);
|
||||
|
||||
return new JsonContent([
|
||||
'redirect' => ['target' => \Container::$routeCollection->getRoute('community.event')->generateLink(['communitySlug' => $this->community->getSlug(), 'eventSlug' => $event->getSlug()])]
|
||||
]);
|
||||
}
|
||||
|
||||
public function deleteEvent(): IContent
|
||||
{
|
||||
$event = $this->eventRepository->getBySlug(Container::$request->query('eventSlug'));
|
||||
|
||||
foreach ($this->transactionRepository->getAllByEvent($event) as $transaction) {
|
||||
$transaction->setEventId(null);
|
||||
Container::$persistentDataManager->saveToDb($transaction);
|
||||
}
|
||||
|
||||
Container::$persistentDataManager->deleteFromDb($event);
|
||||
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
private function sumTransactions(Event $event): float
|
||||
{
|
||||
$exchangeRateCalculator = new ExchangeRateCalculator($this->community->getMainCurrency());
|
||||
$transactions = $this->transactionRepository->getAllByEvent($event, true, ['currency']);
|
||||
$sum = 0.0;
|
||||
|
||||
foreach ($transactions as $transaction) {
|
||||
$sum += $exchangeRateCalculator->calculate($transaction->getSum(), $transaction->getCurrency(), $transaction->getTimeDate());
|
||||
}
|
||||
|
||||
return $sum;
|
||||
}
|
||||
}
|
70
src/Controller/EventRedirectController.php
Normal file
70
src/Controller/EventRedirectController.php
Normal file
@ -0,0 +1,70 @@
|
||||
<?php namespace RVR\Controller;
|
||||
|
||||
use DateTime;
|
||||
use Container;
|
||||
use RVR\PersistentData\Model\Event;
|
||||
use RVR\PersistentData\Model\User;
|
||||
use RVR\Repository\EventRepository;
|
||||
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
|
||||
use SokoWeb\Interfaces\Response\IRedirect;
|
||||
use SokoWeb\Response\HtmlContent;
|
||||
use SokoWeb\Response\Redirect;
|
||||
|
||||
class EventRedirectController implements IAuthenticationRequired
|
||||
{
|
||||
private EventRepository $eventRepository;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->eventRepository = new EventRepository();
|
||||
}
|
||||
|
||||
public function isAuthenticationRequired(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getEvent()
|
||||
{
|
||||
$currentEvent = $this->getCurrentEvent();
|
||||
if ($currentEvent === null) {
|
||||
return new HtmlContent('event_redirect/no_event');
|
||||
}
|
||||
|
||||
return new Redirect(
|
||||
\Container::$routeCollection->getRoute('community.event')
|
||||
->generateLink([
|
||||
'communitySlug' => $currentEvent->getCommunity()->getSlug(),
|
||||
'eventSlug' => $currentEvent->getSlug()
|
||||
]),
|
||||
IRedirect::TEMPORARY
|
||||
);
|
||||
}
|
||||
|
||||
public function getEventNewTransaction()
|
||||
{
|
||||
$currentEvent = $this->getCurrentEvent();
|
||||
if ($currentEvent === null) {
|
||||
return new HtmlContent('event_redirect/no_event');
|
||||
}
|
||||
|
||||
return new Redirect(
|
||||
\Container::$routeCollection->getRoute('community.transactions.new')
|
||||
->generateLink([
|
||||
'communitySlug' => $currentEvent->getCommunity()->getSlug(),
|
||||
'event' => $currentEvent->getSlug()
|
||||
]),
|
||||
IRedirect::TEMPORARY
|
||||
);
|
||||
}
|
||||
|
||||
private function getCurrentEvent(): ?Event
|
||||
{
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = Container::$request->user();
|
||||
|
||||
return $this->eventRepository->getCurrentByUser($user, new DateTime(), 30, true, ['community']);
|
||||
}
|
||||
}
|
@ -1,27 +1,50 @@
|
||||
<?php namespace RVR\Controller;
|
||||
|
||||
use SokoWeb\Interfaces\Authorization\ISecured;
|
||||
use SokoWeb\Interfaces\Request\IRequest;
|
||||
use DateTime;
|
||||
use RVR\PersistentData\Model\Community;
|
||||
use RVR\PersistentData\Model\User;
|
||||
use RVR\Repository\CommunityMemberRepository;
|
||||
use RVR\Repository\EventRepository;
|
||||
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
|
||||
use SokoWeb\Interfaces\Response\IContent;
|
||||
use SokoWeb\Response\HtmlContent;
|
||||
use SokoWeb\Response\JsonContent;
|
||||
|
||||
class HomeController implements ISecured
|
||||
class HomeController implements IAuthenticationRequired
|
||||
{
|
||||
private IRequest $request;
|
||||
private CommunityMemberRepository $communityMemberRepository;
|
||||
|
||||
public function __construct(IRequest $request)
|
||||
private EventRepository $eventRepository;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->communityMemberRepository = new CommunityMemberRepository();
|
||||
$this->eventRepository = new EventRepository();
|
||||
}
|
||||
|
||||
public function authorize(): bool
|
||||
public function isAuthenticationRequired(): bool
|
||||
{
|
||||
return $this->request->user() !== null;
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getIndex(): IContent
|
||||
public function getHome(): IContent
|
||||
{
|
||||
return new HtmlContent('index');
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = \Container::$request->user();
|
||||
|
||||
$ownCommunityMembers = $this->communityMemberRepository->getAllByUser($user, true, ['community']);
|
||||
$communities = [];
|
||||
foreach ($ownCommunityMembers as $ownCommunityMember) {
|
||||
$communities[] = $ownCommunityMember->getCommunity();
|
||||
}
|
||||
usort($communities, function($a, $b) {
|
||||
return strnatcmp($a->getName(), $b->getName());
|
||||
});
|
||||
|
||||
return new HtmlContent('home', [
|
||||
'communities' => $communities,
|
||||
'upcomingAndRecentEvents' => iterator_to_array($this->eventRepository->getUpcomingAndRecentByUser($user, new DateTime(), 30, 3, true, ['community']))
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -2,13 +2,11 @@
|
||||
|
||||
use DateTime;
|
||||
use SokoWeb\Http\Request;
|
||||
use SokoWeb\Interfaces\Request\IRequest;
|
||||
use SokoWeb\Interfaces\Response\IContent;
|
||||
use SokoWeb\Interfaces\Response\IRedirect;
|
||||
use SokoWeb\Mailing\Mail;
|
||||
use SokoWeb\OAuth\GoogleOAuth;
|
||||
use RVR\PersistentData\Model\UserPasswordResetter;
|
||||
use SokoWeb\PersistentData\PersistentDataManager;
|
||||
use RVR\Repository\UserPasswordResetterRepository;
|
||||
use RVR\Repository\UserRepository;
|
||||
use SokoWeb\Response\HtmlContent;
|
||||
@ -19,29 +17,29 @@ use SokoWeb\Util\JwtParser;
|
||||
|
||||
class LoginController
|
||||
{
|
||||
private IRequest $request;
|
||||
|
||||
private PersistentDataManager $pdm;
|
||||
|
||||
private UserRepository $userRepository;
|
||||
|
||||
private UserPasswordResetterRepository $userPasswordResetterRepository;
|
||||
|
||||
public function __construct(IRequest $request)
|
||||
private string $redirectUrl;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->pdm = new PersistentDataManager();
|
||||
$this->userRepository = new UserRepository();
|
||||
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
|
||||
$this->redirectUrl = \Container::$request->session()->has('redirect_after_login') ?
|
||||
\Container::$request->session()->get('redirect_after_login') :
|
||||
\Container::$routeCollection->getRoute('home')->generateLink();
|
||||
}
|
||||
|
||||
public function getLoginForm()
|
||||
{
|
||||
if ($this->request->user() !== null) {
|
||||
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
|
||||
if (\Container::$request->user() !== null) {
|
||||
$this->deleteRedirectUrl();
|
||||
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
|
||||
}
|
||||
|
||||
return new HtmlContent('login/login');
|
||||
return new HtmlContent('login/login', ['redirectUrl' => $this->redirectUrl]);
|
||||
}
|
||||
|
||||
public function getGoogleLoginRedirect(): IRedirect
|
||||
@ -49,13 +47,13 @@ class LoginController
|
||||
$state = bin2hex(random_bytes(16));
|
||||
$nonce = bin2hex(random_bytes(16));
|
||||
|
||||
$this->request->session()->set('oauth_state', $state);
|
||||
$this->request->session()->set('oauth_nonce', $nonce);
|
||||
\Container::$request->session()->set('oauth_state', $state);
|
||||
\Container::$request->session()->set('oauth_nonce', $nonce);
|
||||
|
||||
$oAuth = new GoogleOAuth(new Request());
|
||||
$url = $oAuth->getDialogUrl(
|
||||
$state,
|
||||
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink(),
|
||||
\Container::$request->getBase() . \Container::$routeCollection->getRoute('login.google-action')->generateLink(),
|
||||
$nonce
|
||||
);
|
||||
|
||||
@ -64,11 +62,12 @@ class LoginController
|
||||
|
||||
public function getRequestPasswordResetForm()
|
||||
{
|
||||
if ($this->request->user() !== null) {
|
||||
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
|
||||
if (\Container::$request->user() !== null) {
|
||||
$this->deleteRedirectUrl();
|
||||
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
|
||||
}
|
||||
|
||||
return new HtmlContent('login/password_reset_request', ['email' => $this->request->query('email')]);
|
||||
return new HtmlContent('login/password_reset_request', ['email' => \Container::$request->query('email')]);
|
||||
}
|
||||
|
||||
public function getRequestPasswordResetSuccess(): IContent
|
||||
@ -78,11 +77,12 @@ class LoginController
|
||||
|
||||
public function getResetPasswordForm()
|
||||
{
|
||||
if ($this->request->user() !== null) {
|
||||
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
|
||||
if (\Container::$request->user() !== null) {
|
||||
$this->deleteRedirectUrl();
|
||||
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
|
||||
}
|
||||
|
||||
$token = $this->request->query('token');
|
||||
$token = \Container::$request->query('token');
|
||||
$resetter = $this->userPasswordResetterRepository->getByToken($token);
|
||||
|
||||
if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) {
|
||||
@ -91,112 +91,108 @@ class LoginController
|
||||
|
||||
$user = $this->userRepository->getById($resetter->getUserId());
|
||||
|
||||
return new HtmlContent('login/reset_password', ['success' => true, 'token' => $token, 'email' => $user->getEmail()]);
|
||||
return new HtmlContent('login/reset_password', ['success' => true, 'token' => $token, 'email' => $user->getEmail(), 'redirectUrl' => $this->redirectUrl]);
|
||||
}
|
||||
|
||||
public function login(): IContent
|
||||
{
|
||||
if ($this->request->user() !== null) {
|
||||
if (\Container::$request->user() !== null) {
|
||||
$this->deleteRedirectUrl();
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
$user = $this->userRepository->getByEmail($this->request->post('email'));
|
||||
if ($user === null || !$user->checkPassword($this->request->post('password'))) {
|
||||
$user = $this->userRepository->getByEmailOrUsername(\Container::$request->post('email'));
|
||||
if ($user === null || !$user->checkPassword(\Container::$request->post('password'))) {
|
||||
return new JsonContent([
|
||||
'error' => [
|
||||
'errorText' => 'No user found with the given email address or the given password is wrong. You can <a href="/password/requestReset?email=' .
|
||||
urlencode($this->request->post('email')) . '" title="Request password reset">request password reset</a>!'
|
||||
'errorText' => 'No user found with the given email address / username or the given password is wrong. You can <a href="' .
|
||||
\Container::$routeCollection->getRoute('password.requestReset')->generateLink(['email' => \Container::$request->post('email')]) . '" title="Request password reset">request password reset</a>!'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
$this->request->setUser($user);
|
||||
\Container::$request->setUser($user);
|
||||
|
||||
$this->deleteRedirectUrl();
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
public function loginWithGoogle()
|
||||
{
|
||||
$redirectUrl = $this->request->session()->get('redirect_after_login');
|
||||
if ($redirectUrl === null) {
|
||||
$redirectUrl = \Container::$routeCollection->getRoute('index')->generateLink();
|
||||
$defaultError = 'Authentication with Google failed. Please <a href="' . \Container::$routeCollection->getRoute('login.google')->generateLink() . '" title="Login with Google">try again</a>!';
|
||||
|
||||
if (\Container::$request->user() !== null) {
|
||||
$this->deleteRedirectUrl();
|
||||
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
|
||||
}
|
||||
|
||||
if ($this->request->user() !== null) {
|
||||
$this->request->session()->delete('redirect_after_login');
|
||||
return new Redirect($redirectUrl, IRedirect::TEMPORARY);
|
||||
}
|
||||
|
||||
if ($this->request->query('state') !== $this->request->session()->get('oauth_state')) {
|
||||
return new HtmlContent('login/google_login');
|
||||
if (\Container::$request->query('state') !== \Container::$request->session()->get('oauth_state')) {
|
||||
return new HtmlContent('login/google_login_error', ['error' => $defaultError]);
|
||||
}
|
||||
|
||||
$oAuth = new GoogleOAuth(new Request());
|
||||
$tokenData = $oAuth->getToken(
|
||||
$this->request->query('code'),
|
||||
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink()
|
||||
\Container::$request->query('code'),
|
||||
\Container::$request->getBase() . \Container::$routeCollection->getRoute('login.google-action')->generateLink()
|
||||
);
|
||||
|
||||
if (!isset($tokenData['id_token'])) {
|
||||
return new HtmlContent('login/google_login');
|
||||
return new HtmlContent('login/google_login_error', ['error' => $defaultError]);
|
||||
}
|
||||
|
||||
$jwtParser = new JwtParser($tokenData['id_token']);
|
||||
$idToken = $jwtParser->getPayload();
|
||||
|
||||
if ($idToken['nonce'] !== $this->request->session()->get('oauth_nonce')) {
|
||||
return new HtmlContent('login/google_login');
|
||||
if ($idToken['nonce'] !== \Container::$request->session()->get('oauth_nonce')) {
|
||||
return new HtmlContent('login/google_login_error', ['error' => $defaultError]);
|
||||
}
|
||||
|
||||
if (!$idToken['email_verified']) {
|
||||
return new HtmlContent('login/google_login');
|
||||
return new HtmlContent('login/google_login_error', ['error' => $defaultError]);
|
||||
}
|
||||
|
||||
$user = $this->userRepository->getByGoogleSub($idToken['sub']);
|
||||
if ($user === null) {
|
||||
return new JsonContent([
|
||||
'error' => [
|
||||
'errorText' => 'No user found for this Google account.'
|
||||
]
|
||||
]);
|
||||
return new HtmlContent('login/google_login_error', ['error' => 'No user found for this Google account.']);
|
||||
}
|
||||
|
||||
$this->request->setUser($user);
|
||||
\Container::$request->setUser($user);
|
||||
|
||||
$this->request->session()->delete('redirect_after_login');
|
||||
return new Redirect($redirectUrl, IRedirect::TEMPORARY);
|
||||
$this->deleteRedirectUrl();
|
||||
return new Redirect($this->redirectUrl, IRedirect::TEMPORARY);
|
||||
}
|
||||
|
||||
public function logout(): IRedirect
|
||||
{
|
||||
$this->request->setUser(null);
|
||||
\Container::$request->setUser(null);
|
||||
|
||||
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
|
||||
return new Redirect(\Container::$routeCollection->getRoute('home')->generateLink(), IRedirect::TEMPORARY);
|
||||
}
|
||||
|
||||
public function requestPasswordReset(): IContent
|
||||
{
|
||||
if ($this->request->user() !== null) {
|
||||
if (\Container::$request->user() !== null) {
|
||||
$this->deleteRedirectUrl();
|
||||
return new JsonContent([
|
||||
'redirect' => [
|
||||
'target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink()
|
||||
'target' => $this->redirectUrl
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
if (!empty($_ENV['RECAPTCHA_SITEKEY'])) {
|
||||
if (!$this->request->post('g-recaptcha-response')) {
|
||||
if (!\Container::$request->post('g-recaptcha-response')) {
|
||||
return new JsonContent(['error' => ['errorText' => 'Please check "I\'m not a robot" in the reCAPTCHA box!']]);
|
||||
}
|
||||
|
||||
$captchaValidator = new CaptchaValidator();
|
||||
$captchaResponse = $captchaValidator->validate($this->request->post('g-recaptcha-response'));
|
||||
$captchaResponse = $captchaValidator->validate(\Container::$request->post('g-recaptcha-response'));
|
||||
if (!$captchaResponse['success']) {
|
||||
return new JsonContent(['error' => ['errorText' => 'reCAPTCHA challenge failed. Please try again!']]);
|
||||
}
|
||||
}
|
||||
|
||||
$user = $this->userRepository->getByEmail($this->request->post('email'));
|
||||
$user = $this->userRepository->getByEmailOrUsername(\Container::$request->post('email'));
|
||||
if ($user === null) {
|
||||
return new JsonContent([
|
||||
'error' => [
|
||||
@ -222,15 +218,11 @@ class LoginController
|
||||
$passwordResetter->setToken($token);
|
||||
$passwordResetter->setExpiresDate($expires);
|
||||
|
||||
\Container::$dbConnection->startTransaction();
|
||||
|
||||
if ($existingResetter !== null) {
|
||||
$this->pdm->deleteFromDb($existingResetter);
|
||||
\Container::$persistentDataManager->deleteFromDb($existingResetter);
|
||||
}
|
||||
|
||||
$this->pdm->saveToDb($passwordResetter);
|
||||
|
||||
\Container::$dbConnection->commit();
|
||||
\Container::$persistentDataManager->saveToDb($passwordResetter);
|
||||
|
||||
$this->sendPasswordResetEmail($user->getEmail(), $token, $expires);
|
||||
|
||||
@ -239,26 +231,27 @@ class LoginController
|
||||
|
||||
public function resetPassword(): IContent
|
||||
{
|
||||
if ($this->request->user() !== null) {
|
||||
if (\Container::$request->user() !== null) {
|
||||
$this->deleteRedirectUrl();
|
||||
return new JsonContent([
|
||||
'redirect' => [
|
||||
'target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink()
|
||||
'target' => $this->redirectUrl
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
$token = $this->request->query('token');
|
||||
$token = \Container::$request->query('token');
|
||||
$resetter = $this->userPasswordResetterRepository->getByToken($token);
|
||||
|
||||
if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) {
|
||||
return new JsonContent([
|
||||
'redirect' => [
|
||||
'target' => '/' . \Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token])
|
||||
'target' => \Container::$routeCollection->getRoute('password.reset')->generateLink(['token' => $token])
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
if (strlen($this->request->post('password')) < 6) {
|
||||
if (strlen(\Container::$request->post('password')) < 6) {
|
||||
return new JsonContent([
|
||||
'error' => [
|
||||
'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!'
|
||||
@ -266,23 +259,20 @@ class LoginController
|
||||
]);
|
||||
}
|
||||
|
||||
if ($this->request->post('password') !== $this->request->post('password_confirm')) {
|
||||
if (\Container::$request->post('password') !== \Container::$request->post('password_confirm')) {
|
||||
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
|
||||
}
|
||||
|
||||
\Container::$dbConnection->startTransaction();
|
||||
|
||||
$this->pdm->deleteFromDb($resetter);
|
||||
\Container::$persistentDataManager->deleteFromDb($resetter);
|
||||
|
||||
$user = $this->userRepository->getById($resetter->getUserId());
|
||||
$user->setPlainPassword($this->request->post('password'));
|
||||
$user->setPlainPassword(\Container::$request->post('password'));
|
||||
|
||||
$this->pdm->saveToDb($user);
|
||||
\Container::$persistentDataManager->saveToDb($user);
|
||||
|
||||
\Container::$dbConnection->commit();
|
||||
|
||||
$this->request->setUser($user);
|
||||
\Container::$request->setUser($user);
|
||||
|
||||
$this->deleteRedirectUrl();
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
@ -293,10 +283,15 @@ class LoginController
|
||||
$mail->setSubject($_ENV['APP_NAME'] . ' - Password reset');
|
||||
$mail->setBodyFromTemplate('password-reset', [
|
||||
'EMAIL' => $email,
|
||||
'RESET_LINK' => $this->request->getBase() . '/' .
|
||||
\Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token]),
|
||||
'RESET_LINK' => \Container::$request->getBase() .
|
||||
\Container::$routeCollection->getRoute('password.reset')->generateLink(['token' => $token]),
|
||||
'EXPIRES' => $expires->format('Y-m-d H:i T')
|
||||
]);
|
||||
$mail->send();
|
||||
}
|
||||
|
||||
private function deleteRedirectUrl(): void
|
||||
{
|
||||
\Container::$request->session()->delete('redirect_after_login');
|
||||
}
|
||||
}
|
||||
|
383
src/Controller/OAuthController.php
Normal file
383
src/Controller/OAuthController.php
Normal file
@ -0,0 +1,383 @@
|
||||
<?php namespace RVR\Controller;
|
||||
|
||||
use DateTime;
|
||||
use Firebase\JWT\JWT;
|
||||
use Firebase\JWT\Key;
|
||||
use Firebase\JWT\SignatureInvalidException;
|
||||
use Firebase\JWT\BeforeValidException;
|
||||
use Firebase\JWT\ExpiredException;
|
||||
use RVR\Repository\OAuthSessionRepository;
|
||||
use RVR\Repository\OAuthTokenRepository;
|
||||
use RVR\Repository\UserRepository;
|
||||
use RVR\PersistentData\Model\User;
|
||||
use RVR\PersistentData\Model\OAuthSession;
|
||||
use RVR\PersistentData\Model\OAuthToken;
|
||||
use RVR\Repository\OAuthClientRepository;
|
||||
use SokoWeb\Interfaces\Response\IContent;
|
||||
use SokoWeb\Response\JsonContent;
|
||||
|
||||
class OAuthController
|
||||
{
|
||||
private OAuthClientRepository $oAuthClientRepository;
|
||||
|
||||
private OAuthSessionRepository $oAuthSessionRepository;
|
||||
|
||||
private OAuthTokenRepository $oAuthTokenRepository;
|
||||
|
||||
private UserRepository $userRepository;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->oAuthClientRepository = new OAuthClientRepository();
|
||||
$this->oAuthSessionRepository = new OAuthSessionRepository();
|
||||
$this->oAuthTokenRepository = new OAuthTokenRepository();
|
||||
$this->userRepository = new UserRepository();
|
||||
}
|
||||
|
||||
public function generateToken(): ?IContent
|
||||
{
|
||||
$credentials = $this->getClientCredentials();
|
||||
$code = \Container::$request->post('code');
|
||||
$redirectUri = \Container::$request->post('redirect_uri');
|
||||
|
||||
if (!$credentials['clientId'] || !$code || !$redirectUri) {
|
||||
return new JsonContent([
|
||||
'error' => 'An invalid request was made.'
|
||||
]);
|
||||
}
|
||||
|
||||
$client = $this->oAuthClientRepository->getByClientId($credentials['clientId']);
|
||||
if ($client === null) {
|
||||
return new JsonContent([
|
||||
'error' => 'Client is not found.'
|
||||
]);
|
||||
}
|
||||
|
||||
$redirectUriBase = explode('?', $redirectUri)[0];
|
||||
if (!in_array($redirectUriBase, $client->getRedirectUrisArray())) {
|
||||
return new JsonContent([
|
||||
'error' => 'Redirect URI \'' . $redirectUriBase .'\' is not allowed for this client.'
|
||||
]);
|
||||
}
|
||||
|
||||
$session = $this->oAuthSessionRepository->getByCode($code);
|
||||
if ($session === null || $session->getTokenClaimed() || $session->getExpiresDate() < new DateTime()) {
|
||||
return new JsonContent([
|
||||
'error' => 'The provided code is invalid.'
|
||||
]);
|
||||
}
|
||||
|
||||
$codeChallenge = $session->getCodeChallenge();
|
||||
if ($codeChallenge === null && $client->getClientSecret() !== $credentials['clientSecret']) {
|
||||
return new JsonContent([
|
||||
'error' => 'Client is not authorized.'
|
||||
]);
|
||||
}
|
||||
|
||||
if ($session->getClientId() !== $credentials['clientId']) {
|
||||
return new JsonContent([
|
||||
'error' => 'This code cannot be used by this client!'
|
||||
]);
|
||||
}
|
||||
|
||||
if ($codeChallenge !== null) {
|
||||
$codeVerifier = \Container::$request->post('code_verifier') ?: '';
|
||||
if ($session->getCodeChallengeMethod() === 'S256') {
|
||||
$hash = rtrim(strtr(base64_encode(hash('sha256', $codeVerifier, true)), '+/', '-_'), '=');
|
||||
} else {
|
||||
$hash = $codeVerifier;
|
||||
}
|
||||
if ($codeChallenge !== $hash) {
|
||||
return new JsonContent([
|
||||
'error' => 'Code challenge failed!'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$session->setTokenClaimed(true);
|
||||
\Container::$persistentDataManager->saveToDb($session);
|
||||
|
||||
$token = new OAuthToken();
|
||||
$token->setSession($session);
|
||||
$token->setCreatedDate(new DateTime());
|
||||
$token->setExpiresDate(new DateTime('+1 hours'));
|
||||
\Container::$persistentDataManager->saveToDb($token);
|
||||
|
||||
$commonPayload = [
|
||||
'iss' => $_ENV['APP_URL'],
|
||||
'iat' => $token->getCreatedDate()->getTimestamp(),
|
||||
'nbf' => $session->getCreatedDate()->getTimestamp(),
|
||||
'exp' => $token->getExpiresDate()->getTimestamp(),
|
||||
'aud' => $session->getClientId(),
|
||||
'nonce' => $session->getNonce()
|
||||
];
|
||||
$idTokenPayload = array_merge($commonPayload, $this->getUserInfoInternal(
|
||||
$this->userRepository->getById($session->getUserId()),
|
||||
$session->getScopeArray())
|
||||
);
|
||||
$accessTokenPayload = array_merge($commonPayload, [
|
||||
'jti' => $token->getId(),
|
||||
]);
|
||||
|
||||
$privateKey = file_get_contents(ROOT . '/' . $_ENV['JWT_RSA_PRIVATE_KEY']);
|
||||
$idToken = JWT::encode($idTokenPayload, $privateKey, 'RS256', $_ENV['JWT_KEY_KID']);
|
||||
$accessToken = JWT::encode($accessTokenPayload, $privateKey, 'RS256', $_ENV['JWT_KEY_KID']);
|
||||
|
||||
return new JsonContent([
|
||||
'access_token' => $accessToken,
|
||||
'expires_in' => $token->getExpiresDate()->getTimestamp() - (new DateTime())->getTimestamp(),
|
||||
'scope' => $session->getScope(),
|
||||
'id_token' => $idToken,
|
||||
'token_type' => 'Bearer'
|
||||
]);
|
||||
}
|
||||
|
||||
public function introspectToken(): ?IContent
|
||||
{
|
||||
$credentials = $this->getClientCredentials();
|
||||
$accessToken = \Container::$request->post('token');
|
||||
|
||||
if (!$credentials['clientId'] || !$credentials['clientSecret'] || !$accessToken) {
|
||||
return new JsonContent([
|
||||
'error' => 'An invalid request was made.'
|
||||
]);
|
||||
}
|
||||
|
||||
$client = $this->oAuthClientRepository->getByClientId($credentials['clientId']);
|
||||
if ($client === null || $client->getClientSecret() !== $credentials['clientSecret']) {
|
||||
return new JsonContent([
|
||||
'error' => 'Client is not authorized.'
|
||||
]);
|
||||
}
|
||||
|
||||
$tokenValidated = $this->validateTokenAndSession($accessToken, $token, $session);
|
||||
if (!$tokenValidated) {
|
||||
return new JsonContent([
|
||||
'active' => false
|
||||
]);
|
||||
}
|
||||
|
||||
if ($session->getClientId() !== $credentials['clientId']) {
|
||||
return new JsonContent([
|
||||
'active' => false
|
||||
]);
|
||||
}
|
||||
|
||||
return new JsonContent([
|
||||
'active' => true,
|
||||
'scope' => $session->getScope(),
|
||||
'client_id' => $session->getClientId(),
|
||||
'exp' => $token->getExpiresDate()->getTimestamp(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function revokeToken(): ?IContent
|
||||
{
|
||||
$credentials = $this->getClientCredentials();
|
||||
$accessToken = \Container::$request->post('token');
|
||||
|
||||
if (!$credentials['clientId'] || !$credentials['clientSecret'] || !$accessToken) {
|
||||
return new JsonContent([
|
||||
'error' => 'An invalid request was made.'
|
||||
]);
|
||||
}
|
||||
|
||||
$client = $this->oAuthClientRepository->getByClientId($credentials['clientId']);
|
||||
if ($client === null || $client->getClientSecret() !== $credentials['clientSecret']) {
|
||||
return new JsonContent([
|
||||
'error' => 'Client is not authorized.'
|
||||
]);
|
||||
}
|
||||
|
||||
$tokenValidated = $this->validateTokenAndSession($accessToken, $token, $session);
|
||||
if (!$tokenValidated) {
|
||||
return new JsonContent([]);
|
||||
}
|
||||
|
||||
$session = $this->oAuthSessionRepository->getById($token->getSessionId());
|
||||
if ($session->getClientId() !== $credentials['clientId']) {
|
||||
return new JsonContent([]);
|
||||
}
|
||||
|
||||
\Container::$persistentDataManager->deleteFromDb($token);
|
||||
|
||||
return new JsonContent([]);
|
||||
}
|
||||
|
||||
public function getUserInfo() : IContent
|
||||
{
|
||||
$authorization = \Container::$request->header('Authorization');
|
||||
if ($authorization === null) {
|
||||
return new JsonContent([
|
||||
'error' => 'No Authorization header was sent.'
|
||||
]);
|
||||
}
|
||||
|
||||
$accessToken = substr($authorization, strlen('Bearer '));
|
||||
$tokenValidated = $this->validateTokenAndSession($accessToken, $token, $session);
|
||||
if (!$tokenValidated) {
|
||||
return new JsonContent([
|
||||
'error' => 'The provided access token is invalid.'
|
||||
]);
|
||||
}
|
||||
|
||||
return new JsonContent(
|
||||
$this->getUserInfoInternal(
|
||||
$this->userRepository->getById($session->getUserId()),
|
||||
$session->getScopeArray()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function getConfig(): IContent
|
||||
{
|
||||
return new JsonContent([
|
||||
'issuer' => $_ENV['APP_URL'],
|
||||
'authorization_endpoint' => \Container::$request->getBase() . \Container::$routeCollection->getRoute('oauth.auth')->generateLink(),
|
||||
'token_endpoint' => \Container::$request->getBase() . \Container::$routeCollection->getRoute('oauth.token')->generateLink(),
|
||||
'introspection_endpoint' => \Container::$request->getBase() . \Container::$routeCollection->getRoute('oauth.token.introspect')->generateLink(),
|
||||
'revocation_endpoint' => \Container::$request->getBase() . \Container::$routeCollection->getRoute('oauth.token.revoke')->generateLink(),
|
||||
'userinfo_endpoint' => \Container::$request->getBase() . \Container::$routeCollection->getRoute('oauth.userinfo')->generateLink(),
|
||||
'end_session_endpoint' => \Container::$request->getBase() . \Container::$routeCollection->getRoute('logout')->generateLink(),
|
||||
'jwks_uri' => \Container::$request->getBase() . \Container::$routeCollection->getRoute('oauth.certs')->generateLink(),
|
||||
'response_types_supported' =>
|
||||
[
|
||||
'code',
|
||||
],
|
||||
'subject_types_supported' =>
|
||||
[
|
||||
'public',
|
||||
],
|
||||
'id_token_signing_alg_values_supported' =>
|
||||
[
|
||||
'RS256',
|
||||
],
|
||||
'scopes_supported' =>
|
||||
[
|
||||
'openid',
|
||||
'email',
|
||||
'profile',
|
||||
],
|
||||
'token_endpoint_auth_methods_supported' =>
|
||||
[
|
||||
'client_secret_basic',
|
||||
'client_secret_post',
|
||||
],
|
||||
'claims_supported' =>
|
||||
[
|
||||
'aud',
|
||||
'email',
|
||||
'exp',
|
||||
'full_name',
|
||||
'iat',
|
||||
'id_number',
|
||||
'iss',
|
||||
'nickname',
|
||||
'phone',
|
||||
'picture',
|
||||
'sub',
|
||||
'username',
|
||||
],
|
||||
'code_challenge_methods_supported' =>
|
||||
[
|
||||
'plain',
|
||||
'S256',
|
||||
],
|
||||
'grant_types_supported' =>
|
||||
[
|
||||
'authorization_code',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function getCerts(): IContent
|
||||
{
|
||||
$publicKey = file_get_contents(ROOT . '/' . $_ENV['JWT_RSA_PUBLIC_KEY']);
|
||||
$keyInfo = openssl_pkey_get_details(openssl_pkey_get_public($publicKey));
|
||||
|
||||
return new JsonContent(['keys' => [
|
||||
[
|
||||
'kty' => 'RSA',
|
||||
'alg' => 'RS256',
|
||||
'use' => 'sig',
|
||||
'kid' => $_ENV['JWT_KEY_KID'],
|
||||
'n' => str_replace(['+', '/'], ['-', '_'], base64_encode($keyInfo['rsa']['n'])),
|
||||
'e' => str_replace(['+', '/'], ['-', '_'], base64_encode($keyInfo['rsa']['e'])),
|
||||
]
|
||||
]]);
|
||||
}
|
||||
|
||||
private function getClientCredentials(): array
|
||||
{
|
||||
$authorization = \Container::$request->header('Authorization');
|
||||
if ($authorization !== null) {
|
||||
$basicAuthEncoded = substr($authorization, strlen('Basic '));
|
||||
$basicAuth = explode(':', base64_decode($basicAuthEncoded));
|
||||
if (count($basicAuth) === 2) {
|
||||
$clientId = rawurldecode($basicAuth[0]);
|
||||
$clientSecret = rawurldecode($basicAuth[1]);
|
||||
} else {
|
||||
$clientId = null;
|
||||
$clientSecret = null;
|
||||
}
|
||||
} else {
|
||||
$clientId = \Container::$request->post('client_id');
|
||||
$clientSecret = \Container::$request->post('client_secret');
|
||||
}
|
||||
return ['clientId' => $clientId, 'clientSecret' => $clientSecret];
|
||||
}
|
||||
|
||||
private function validateTokenAndSession(
|
||||
string $accessToken,
|
||||
?OAuthToken &$token,
|
||||
?OAuthSession &$session): bool
|
||||
{
|
||||
$publicKey = file_get_contents(ROOT . '/' . $_ENV['JWT_RSA_PUBLIC_KEY']);
|
||||
try {
|
||||
$payload = JWT::decode($accessToken, new Key($publicKey, 'RS256'));
|
||||
$token = $this->oAuthTokenRepository->getById($payload->jti);
|
||||
} catch (SignatureInvalidException | BeforeValidException | ExpiredException) {
|
||||
$token = null;
|
||||
} catch (\UnexpectedValueException $e) {
|
||||
error_log($e->getMessage() . ' Token was: ' . $accessToken);
|
||||
$token = null;
|
||||
}
|
||||
|
||||
if ($token === null || $token->getExpiresDate() < new DateTime()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$session = $this->oAuthSessionRepository->getById($token->getSessionId());
|
||||
if ($session === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param string[] $scope
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function getUserInfoInternal(User $user, array $scope): array
|
||||
{
|
||||
$userInfo = [];
|
||||
if (in_array('openid', $scope)) {
|
||||
$userInfo['sub'] = (string)$user->getId();
|
||||
}
|
||||
if (in_array('email', $scope)) {
|
||||
$userInfo['email'] = $user->getEmail();
|
||||
}
|
||||
if (in_array('profile', $scope)) {
|
||||
if ($user->getUsername() !== null) {
|
||||
$userInfo['preferred_username'] = $user->getUsername();
|
||||
}
|
||||
$userInfo['name'] = $user->getFullName();
|
||||
$userInfo['nickname'] = $user->getNickname();
|
||||
$userInfo['phone_number'] = $user->getPhone();
|
||||
$userInfo['id_number'] = $user->getIdNumber();
|
||||
}
|
||||
return $userInfo;
|
||||
}
|
||||
}
|
90
src/Controller/OAuthSessionController.php
Normal file
90
src/Controller/OAuthSessionController.php
Normal file
@ -0,0 +1,90 @@
|
||||
<?php namespace RVR\Controller;
|
||||
|
||||
use DateTime;
|
||||
use RVR\PersistentData\Model\OAuthSession;
|
||||
use RVR\PersistentData\Model\User;
|
||||
use RVR\Repository\OAuthClientRepository;
|
||||
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
|
||||
use SokoWeb\Interfaces\Response\IRedirect;
|
||||
use SokoWeb\Response\Redirect;
|
||||
use SokoWeb\Response\HtmlContent;
|
||||
|
||||
class OAuthSessionController implements IAuthenticationRequired
|
||||
{
|
||||
private OAuthClientRepository $oAuthClientRepository;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->oAuthClientRepository = new OAuthClientRepository();
|
||||
}
|
||||
|
||||
public function isAuthenticationRequired(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function auth(): HtmlContent|Redirect
|
||||
{
|
||||
$redirectUri = \Container::$request->query('redirect_uri');
|
||||
$clientId = \Container::$request->query('client_id');
|
||||
$scope = \Container::$request->query('scope') ? \Container::$request->query('scope'): '';
|
||||
$state = \Container::$request->query('state');
|
||||
$nonce = \Container::$request->query('nonce') ? \Container::$request->query('nonce'): '';
|
||||
$codeChallenge = \Container::$request->query('code_challenge') ?: null;
|
||||
$codeChallengeMethod = \Container::$request->query('code_challenge_method') ?: null;
|
||||
|
||||
if (!$clientId || !$redirectUri || !$state || (!$codeChallenge && $codeChallengeMethod)) {
|
||||
return new HtmlContent('oauth/oauth_error', ['error' => 'An invalid request was made. Please start authentication again.']);
|
||||
}
|
||||
|
||||
if ($codeChallenge && (strlen($codeChallenge) < 43 || strlen($codeChallenge) > 128)) {
|
||||
return new HtmlContent('oauth/oauth_error', ['error' => 'Code challenge should be one between 43 and 128 characters long.']);
|
||||
}
|
||||
$possibleCodeChallengeMethods = ['plain', 'S256'];
|
||||
if ($codeChallenge && !in_array($codeChallengeMethod, $possibleCodeChallengeMethods)) {
|
||||
return new HtmlContent('oauth/oauth_error', ['error' => 'Code challenge method should be one of the following: ' . implode(',', $possibleCodeChallengeMethods)]);
|
||||
}
|
||||
|
||||
$client = $this->oAuthClientRepository->getByClientId($clientId);
|
||||
if ($client === null) {
|
||||
return new HtmlContent('oauth/oauth_error', ['error' => 'Client is not found.']);
|
||||
}
|
||||
|
||||
$redirectUriQueryParsed = [];
|
||||
if (str_contains('?', $redirectUri)) {
|
||||
[$redirectUriBase, $redirectUriQuery] = explode('?', $redirectUri, 2);
|
||||
parse_str($redirectUriQuery, $redirectUriQueryParsed);
|
||||
} else {
|
||||
$redirectUriBase = $redirectUri;
|
||||
}
|
||||
if (!in_array($redirectUriBase, $client->getRedirectUrisArray())) {
|
||||
return new HtmlContent('oauth/oauth_error', ['error' => 'Redirect URI \'' . $redirectUriBase .'\' is not allowed for this client.']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @var ?User $user
|
||||
*/
|
||||
$user = \Container::$request->user();
|
||||
$code = bin2hex(random_bytes(16));
|
||||
|
||||
$session = new OAuthSession();
|
||||
$session->setClientId($clientId);
|
||||
$session->setNonce($nonce);
|
||||
$session->setScope($scope);
|
||||
$session->setCodeChallenge($codeChallenge);
|
||||
$session->setCodeChallengeMethod($codeChallengeMethod);
|
||||
$session->setUser($user);
|
||||
$session->setCode($code);
|
||||
$session->setCreatedDate(new DateTime());
|
||||
$session->setExpiresDate(new DateTime('+5 minutes'));
|
||||
\Container::$persistentDataManager->saveToDb($session);
|
||||
|
||||
$redirectUriQueryParsed = array_merge($redirectUriQueryParsed, [
|
||||
'state' => $state,
|
||||
'code' => $code
|
||||
]);
|
||||
$finalRedirectUri = $redirectUriBase . '?' . http_build_query($redirectUriQueryParsed);
|
||||
|
||||
return new Redirect($finalRedirectUri, IRedirect::TEMPORARY);
|
||||
}
|
||||
}
|
239
src/Controller/TransactionController.php
Normal file
239
src/Controller/TransactionController.php
Normal file
@ -0,0 +1,239 @@
|
||||
<?php namespace RVR\Controller;
|
||||
|
||||
use Container;
|
||||
use DateTime;
|
||||
use RVR\Finance\ExchangeRateCalculator;
|
||||
use RVR\PersistentData\Model\Community;
|
||||
use RVR\PersistentData\Model\CommunityMember;
|
||||
use RVR\PersistentData\Model\Transaction;
|
||||
use RVR\PersistentData\Model\TransactionPayee;
|
||||
use RVR\PersistentData\Model\User;
|
||||
use RVR\Repository\CommunityMemberRepository;
|
||||
use RVR\Repository\CommunityRepository;
|
||||
use RVR\Repository\CurrencyRepository;
|
||||
use RVR\Repository\TransactionRepository;
|
||||
use RVR\Repository\TransactionPayeeRepository;
|
||||
use RVR\Repository\EventRepository;
|
||||
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
|
||||
use SokoWeb\Interfaces\Authorization\ISecured;
|
||||
use SokoWeb\Interfaces\Response\IContent;
|
||||
use SokoWeb\Response\HtmlContent;
|
||||
use SokoWeb\Response\JsonContent;
|
||||
|
||||
class TransactionController implements IAuthenticationRequired, ISecured
|
||||
{
|
||||
private CommunityRepository $communityRepository;
|
||||
|
||||
private CommunityMemberRepository $communityMemberRepository;
|
||||
|
||||
private CurrencyRepository $currencyRepository;
|
||||
|
||||
private TransactionRepository $transactionRepository;
|
||||
|
||||
private TransactionPayeeRepository $transactionPayeeRepository;
|
||||
|
||||
private EventRepository $eventRepository;
|
||||
|
||||
private ?Community $community;
|
||||
|
||||
private ?CommunityMember $ownCommunityMember;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->communityRepository = new CommunityRepository();
|
||||
$this->communityMemberRepository = new CommunityMemberRepository();
|
||||
$this->currencyRepository = new CurrencyRepository();
|
||||
$this->transactionRepository = new TransactionRepository();
|
||||
$this->transactionPayeeRepository = new TransactionPayeeRepository();
|
||||
$this->eventRepository = new EventRepository();
|
||||
}
|
||||
|
||||
public function isAuthenticationRequired(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
$communitySlug = \Container::$request->query('communitySlug');
|
||||
$this->community = $this->communityRepository->getBySlug($communitySlug);
|
||||
if ($this->community === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = \Container::$request->user();
|
||||
$this->ownCommunityMember = $this->communityMemberRepository->getByCommunityAndUser($this->community, $user);
|
||||
if ($this->ownCommunityMember === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getTransactions(): IContent
|
||||
{
|
||||
Container::$persistentDataManager->loadRelationsFromDb($this->community, true, ['main_currency']);
|
||||
$exchangeRateCalculator = new ExchangeRateCalculator($this->community->getMainCurrency());
|
||||
|
||||
$eventSlug = Container::$request->query('event');
|
||||
if ($eventSlug) {
|
||||
$event = $this->eventRepository->getBySlug($eventSlug);
|
||||
} else {
|
||||
$event = null;
|
||||
}
|
||||
|
||||
$itemsPerPage = 50;
|
||||
$numberOfTransactions = $event ?
|
||||
$this->transactionRepository->countAllByEvent($event) :
|
||||
$this->transactionRepository->countAllByCommunity($this->community);
|
||||
$currentPage = Container::$request->query('page') ?: 1;
|
||||
$transactions = $event ?
|
||||
$this->transactionRepository->getPagedByEvent(
|
||||
$event,
|
||||
$currentPage,
|
||||
$itemsPerPage,
|
||||
true,
|
||||
['currency', 'payer_user']
|
||||
) :
|
||||
$this->transactionRepository->getPagedByCommunity(
|
||||
$this->community,
|
||||
$currentPage,
|
||||
$itemsPerPage,
|
||||
true,
|
||||
['event', 'currency', 'payer_user']
|
||||
);
|
||||
|
||||
$transactions = iterator_to_array($transactions);
|
||||
Container::$persistentDataManager->loadMultiRelationsFromDb($transactions, 'payees', true, ['user']);
|
||||
|
||||
return new HtmlContent('communities/transactions', [
|
||||
'community' => $this->community,
|
||||
'event' => $event,
|
||||
'exchangeRateCalculator' => $exchangeRateCalculator,
|
||||
'pages' => ceil($numberOfTransactions / $itemsPerPage),
|
||||
'currentPage' => $currentPage,
|
||||
'numberOfTransactions' => $numberOfTransactions,
|
||||
'transactions' => $transactions,
|
||||
'members' => $this->getMembers($this->community)
|
||||
]);
|
||||
}
|
||||
|
||||
public function getTransactionEdit(): ?IContent
|
||||
{
|
||||
$transactionId = Container::$request->query('transactionId');
|
||||
if ($transactionId) {
|
||||
$transaction = $this->transactionRepository->getById($transactionId);
|
||||
if ($transaction === null) {
|
||||
return null;
|
||||
}
|
||||
Container::$persistentDataManager->loadRelationsFromDb($transaction, false, ['event']);
|
||||
$event = $transaction->getEvent();
|
||||
$payeeUserIds = [];
|
||||
foreach ($this->transactionPayeeRepository->getAllByTransaction($transaction) as $payee) {
|
||||
$payeeUserIds[] = $payee->getUserId();
|
||||
}
|
||||
} else {
|
||||
$transaction = null;
|
||||
$eventSlug = Container::$request->query('event');
|
||||
if ($eventSlug) {
|
||||
$event = $this->eventRepository->getBySlug($eventSlug);
|
||||
} else {
|
||||
$event = null;
|
||||
}
|
||||
$payeeUserIds = [];
|
||||
}
|
||||
|
||||
return new HtmlContent('communities/transaction_edit', [
|
||||
'community' => $this->community,
|
||||
'transaction' => $transaction,
|
||||
'payeeUserIds' => $payeeUserIds,
|
||||
'event' => $event,
|
||||
'members' => $this->getMembers($this->community),
|
||||
'currencies' => $this->getCurrencies($this->community)
|
||||
]);
|
||||
}
|
||||
|
||||
public function saveTransaction(): ?IContent
|
||||
{
|
||||
$transactionId = Container::$request->query('transactionId');
|
||||
if ($transactionId) {
|
||||
$transaction = $this->transactionRepository->getById($transactionId);
|
||||
} else {
|
||||
$transaction = new Transaction();
|
||||
$transaction->setCommunity($this->community);
|
||||
}
|
||||
|
||||
$transaction->setEventId(Container::$request->post('event_id') ?: null);
|
||||
$transaction->setCurrencyId(Container::$request->post('currency_id'));
|
||||
$transaction->setPayerUserId(Container::$request->post('payer_user_id'));
|
||||
$transaction->setDescription(Container::$request->post('description'));
|
||||
$transaction->setSum(Container::$request->post('sum'));
|
||||
$transaction->setTimeDate(new DateTime(Container::$request->post('time')));
|
||||
Container::$persistentDataManager->saveToDb($transaction);
|
||||
|
||||
$payeeUserIds = array_unique(Container::$request->post('payee_user_ids'));
|
||||
if (count($payeeUserIds) === $this->communityMemberRepository->countAllByCommunity($this->community)) {
|
||||
$payeeUserIds = [];
|
||||
}
|
||||
|
||||
$currentPayees = [];
|
||||
foreach ($payeeUserIds as $payeeUserId) {
|
||||
$payee = new TransactionPayee();
|
||||
$payee->setTransaction($transaction);
|
||||
$payee->setUserId((int)$payeeUserId);
|
||||
$currentPayees[(int)$payeeUserId] = $payee;
|
||||
}
|
||||
$existingPayees = [];
|
||||
if ($transactionId) {
|
||||
foreach ($this->transactionPayeeRepository->getAllByTransaction($transaction) as $payee) {
|
||||
$existingPayees[$payee->getUserId()] = $payee;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (array_diff_key($currentPayees, $existingPayees) as $newPayee) {
|
||||
Container::$persistentDataManager->saveToDb($newPayee);
|
||||
}
|
||||
foreach (array_diff_key($existingPayees, $currentPayees) as $deletedPayee) {
|
||||
Container::$persistentDataManager->deleteFromDb($deletedPayee);
|
||||
}
|
||||
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
public function deleteTransaction(): IContent
|
||||
{
|
||||
$transaction = $this->transactionRepository->getById(Container::$request->query('transactionId'));
|
||||
|
||||
foreach ($this->transactionPayeeRepository->getAllByTransaction($transaction) as $payee) {
|
||||
Container::$persistentDataManager->deleteFromDb($payee);
|
||||
}
|
||||
|
||||
Container::$persistentDataManager->deleteFromDb($transaction);
|
||||
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
private function getMembers(Community $community): array
|
||||
{
|
||||
$members = iterator_to_array($this->communityMemberRepository->getAllByCommunity($community, true, ['user']));
|
||||
usort($members, function ($a, $b) {
|
||||
return strnatcmp($a->getUser()->getDisplayName(), $b->getUser()->getDisplayName());
|
||||
});
|
||||
return $members;
|
||||
}
|
||||
|
||||
private function getCurrencies(Community $community): array
|
||||
{
|
||||
$currencies = iterator_to_array($this->currencyRepository->getAllByCommunity($community));
|
||||
usort($currencies, function ($a, $b) {
|
||||
return strnatcmp($a->getCode(), $b->getCode());
|
||||
});
|
||||
usort($currencies, function ($a, $b) use ($community) {
|
||||
return (int)($b->getId() === $community->getMainCurrencyId()) - (int)($a->getId() === $community->getMainCurrencyId());
|
||||
});
|
||||
return $currencies;
|
||||
}
|
||||
}
|
@ -2,33 +2,29 @@
|
||||
|
||||
use DateTime;
|
||||
use SokoWeb\Http\Request;
|
||||
use SokoWeb\Interfaces\Authorization\ISecured;
|
||||
use SokoWeb\Interfaces\Request\IRequest;
|
||||
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
|
||||
use SokoWeb\Interfaces\Response\IContent;
|
||||
use SokoWeb\Interfaces\Response\IRedirect;
|
||||
use SokoWeb\OAuth\GoogleOAuth;
|
||||
use SokoWeb\PersistentData\PersistentDataManager;
|
||||
use RVR\PersistentData\Model\User;
|
||||
use SokoWeb\Response\HtmlContent;
|
||||
use SokoWeb\Response\JsonContent;
|
||||
use SokoWeb\Response\Redirect;
|
||||
use SokoWeb\Util\JwtParser;
|
||||
use RVR\Repository\UserRepository;
|
||||
|
||||
class UserController implements ISecured
|
||||
class UserController implements IAuthenticationRequired
|
||||
{
|
||||
private IRequest $request;
|
||||
private UserRepository $userRepository;
|
||||
|
||||
private PersistentDataManager $pdm;
|
||||
|
||||
public function __construct(IRequest $request)
|
||||
public function __construct()
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->pdm = new PersistentDataManager();
|
||||
$this->userRepository = new UserRepository();
|
||||
}
|
||||
|
||||
public function authorize(): bool
|
||||
public function isAuthenticationRequired(): bool
|
||||
{
|
||||
return $this->request->user() !== null;
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getAccount(): IContent
|
||||
@ -36,29 +32,153 @@ class UserController implements ISecured
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = $this->request->user();
|
||||
$user = \Container::$request->user();
|
||||
|
||||
return new HtmlContent('account/account', ['user' => $user->toArray()]);
|
||||
}
|
||||
|
||||
public function getGoogleConnectRedirect(): IRedirect
|
||||
{
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = \Container::$request->user();
|
||||
|
||||
$state = bin2hex(random_bytes(16));
|
||||
$nonce = bin2hex(random_bytes(16));
|
||||
|
||||
\Container::$request->session()->set('oauth_state', $state);
|
||||
\Container::$request->session()->set('oauth_nonce', $nonce);
|
||||
|
||||
$oAuth = new GoogleOAuth(new Request());
|
||||
|
||||
$url = $oAuth->getDialogUrl(
|
||||
$state,
|
||||
\Container::$request->getBase() . \Container::$routeCollection->getRoute('account.googleConnect-confirm')->generateLink(),
|
||||
$nonce,
|
||||
$user->getEmail()
|
||||
);
|
||||
|
||||
return new Redirect($url, IRedirect::TEMPORARY);
|
||||
}
|
||||
|
||||
public function getGoogleConnectConfirm(): IContent
|
||||
{
|
||||
$defaultError = 'Authentication with Google failed. Please <a href="' . \Container::$routeCollection->getRoute('account.googleConnect')->generateLink() . '" title="Connect with Google">try again</a>!';
|
||||
|
||||
if (\Container::$request->query('state') !== \Container::$request->session()->get('oauth_state')) {
|
||||
return new HtmlContent('account/google_connect', ['success' => false, 'error' => $defaultError]);
|
||||
}
|
||||
|
||||
$oAuth = new GoogleOAuth(new Request());
|
||||
$tokenData = $oAuth->getToken(
|
||||
\Container::$request->query('code'),
|
||||
\Container::$request->getBase() . \Container::$routeCollection->getRoute('account.googleConnect-confirm')->generateLink()
|
||||
);
|
||||
if (!isset($tokenData['id_token'])) {
|
||||
return new HtmlContent('account/google_connect', ['success' => false, 'error' => $defaultError]);
|
||||
}
|
||||
|
||||
$jwtParser = new JwtParser($tokenData['id_token']);
|
||||
$idToken = $jwtParser->getPayload();
|
||||
if ($idToken['nonce'] !== \Container::$request->session()->get('oauth_nonce')) {
|
||||
return new HtmlContent('account/google_connect', ['success' => false, 'error' => $defaultError]);
|
||||
}
|
||||
|
||||
$anotherUser = $this->userRepository->getByGoogleSub($idToken['sub']);
|
||||
if ($anotherUser !== null) {
|
||||
return new HtmlContent('account/google_connect', [
|
||||
'success' => false,
|
||||
'error' => 'This Google account is linked to another account.'
|
||||
]);
|
||||
}
|
||||
|
||||
\Container::$request->session()->set('google_user_data', $idToken);
|
||||
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = \Container::$request->user();
|
||||
|
||||
return new HtmlContent('account/google_connect', [
|
||||
'success' => true,
|
||||
'googleAccount' => $idToken['email'],
|
||||
'userEmail' => $user->getEmail()
|
||||
]);
|
||||
}
|
||||
|
||||
public function connectGoogle(): IContent
|
||||
{
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = \Container::$request->user();
|
||||
if (!$user->checkPassword(\Container::$request->post('password'))) {
|
||||
return new JsonContent([
|
||||
'error' => [
|
||||
'errorText' => 'The given password is wrong.'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
$googleUserData = \Container::$request->session()->get('google_user_data');
|
||||
$user->setGoogleSub($googleUserData['sub']);
|
||||
\Container::$persistentDataManager->saveToDb($user);
|
||||
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
public function getGoogleDisconnectConfirm(): IContent
|
||||
{
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = \Container::$request->user();
|
||||
|
||||
return new HtmlContent('account/google_disconnect', [
|
||||
'success' => true,
|
||||
'userEmail' => $user->getEmail()
|
||||
]);
|
||||
}
|
||||
|
||||
public function disconnectGoogle(): IContent
|
||||
{
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = \Container::$request->user();
|
||||
if (!$user->checkPassword(\Container::$request->post('password'))) {
|
||||
return new JsonContent([
|
||||
'error' => [
|
||||
'errorText' => 'The given password is wrong.'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
$user->setGoogleSub(null);
|
||||
\Container::$persistentDataManager->saveToDb($user);
|
||||
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
public function getGoogleAuthenticateRedirect(): IRedirect
|
||||
{
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = $this->request->user();
|
||||
$user = \Container::$request->user();
|
||||
|
||||
$state = bin2hex(random_bytes(16));
|
||||
$nonce = bin2hex(random_bytes(16));
|
||||
|
||||
$this->request->session()->set('oauth_state', $state);
|
||||
$this->request->session()->set('oauth_nonce', $nonce);
|
||||
\Container::$request->session()->set('oauth_state', $state);
|
||||
\Container::$request->session()->set('oauth_nonce', $nonce);
|
||||
|
||||
$oAuth = new GoogleOAuth(new Request());
|
||||
|
||||
$url = $oAuth->getDialogUrl(
|
||||
$state,
|
||||
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink(),
|
||||
\Container::$request->getBase() . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink(),
|
||||
$nonce,
|
||||
$user->getEmail()
|
||||
);
|
||||
@ -71,16 +191,16 @@ class UserController implements ISecured
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = $this->request->user();
|
||||
$user = \Container::$request->user();
|
||||
|
||||
if ($this->request->query('state') !== $this->request->session()->get('oauth_state')) {
|
||||
if (\Container::$request->query('state') !== \Container::$request->session()->get('oauth_state')) {
|
||||
return new HtmlContent('account/google_authenticate', ['success' => false]);
|
||||
}
|
||||
|
||||
$oAuth = new GoogleOAuth(new Request());
|
||||
$tokenData = $oAuth->getToken(
|
||||
$this->request->query('code'),
|
||||
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink()
|
||||
\Container::$request->query('code'),
|
||||
\Container::$request->getBase() . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink()
|
||||
);
|
||||
|
||||
if (!isset($tokenData['id_token'])) {
|
||||
@ -90,7 +210,7 @@ class UserController implements ISecured
|
||||
$jwtParser = new JwtParser($tokenData['id_token']);
|
||||
$idToken = $jwtParser->getPayload();
|
||||
|
||||
if ($idToken['nonce'] !== $this->request->session()->get('oauth_nonce')) {
|
||||
if ($idToken['nonce'] !== \Container::$request->session()->get('oauth_nonce')) {
|
||||
return new HtmlContent('account/google_authenticate', ['success' => false]);
|
||||
}
|
||||
|
||||
@ -102,7 +222,7 @@ class UserController implements ISecured
|
||||
}
|
||||
|
||||
$authenticatedWithGoogleUntil = new DateTime('+45 seconds');
|
||||
$this->request->session()->set('authenticated_with_google_until', $authenticatedWithGoogleUntil);
|
||||
\Container::$request->session()->set('authenticated_with_google_until', $authenticatedWithGoogleUntil);
|
||||
|
||||
return new HtmlContent('account/google_authenticate', [
|
||||
'success' => true,
|
||||
@ -115,19 +235,50 @@ class UserController implements ISecured
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = $this->request->user();
|
||||
$user = \Container::$request->user();
|
||||
|
||||
if (!$this->confirmUserIdentity(
|
||||
$user,
|
||||
$this->request->session()->get('authenticated_with_google_until'),
|
||||
$this->request->post('password'),
|
||||
\Container::$request->session()->get('authenticated_with_google_until'),
|
||||
\Container::$request->post('password'),
|
||||
$error
|
||||
)) {
|
||||
return new JsonContent(['error' => ['errorText' => $error]]);
|
||||
}
|
||||
|
||||
if (strlen($this->request->post('password_new')) > 0) {
|
||||
if (strlen($this->request->post('password_new')) < 6) {
|
||||
$newEmail = \Container::$request->post('email');
|
||||
if ($newEmail !== $user->getEmail()) {
|
||||
if (!filter_var($newEmail, FILTER_VALIDATE_EMAIL)) {
|
||||
return new JsonContent(['error' => ['errorText' => 'Please provide a valid email address.']]);
|
||||
}
|
||||
|
||||
if ($this->userRepository->getByEmail($newEmail) !== null) {
|
||||
return new JsonContent(['error' => ['errorText' => 'The given email address belongs to another account.']]);
|
||||
}
|
||||
|
||||
$user->setEmail($newEmail);
|
||||
}
|
||||
|
||||
$newUsername = \Container::$request->post('username');
|
||||
if ($newUsername !== $user->getUsername()) {
|
||||
if (strlen($newUsername) > 0) {
|
||||
if (filter_var($newUsername, FILTER_VALIDATE_EMAIL)) {
|
||||
return new JsonContent(['error' => ['errorText' => 'Please select a username that is not a valid email address.']]);
|
||||
}
|
||||
|
||||
if ($this->userRepository->getByUsername($newUsername) !== null) {
|
||||
return new JsonContent(['error' => ['errorText' => 'The given username is already taken.']]);
|
||||
}
|
||||
|
||||
$user->setUsername($newUsername);
|
||||
} else {
|
||||
$user->setUsername(null);
|
||||
}
|
||||
}
|
||||
|
||||
$newPassword = \Container::$request->post('password_new');
|
||||
if (strlen($newPassword) > 0) {
|
||||
if (strlen($newPassword) < 6) {
|
||||
return new JsonContent([
|
||||
'error' => [
|
||||
'errorText' => 'The given new password is too short. Please choose a password that is at least 6 characters long!'
|
||||
@ -135,7 +286,7 @@ class UserController implements ISecured
|
||||
]);
|
||||
}
|
||||
|
||||
if ($this->request->post('password_new') !== $this->request->post('password_new_confirm')) {
|
||||
if ($newPassword !== \Container::$request->post('password_new_confirm')) {
|
||||
return new JsonContent([
|
||||
'error' => [
|
||||
'errorText' => 'The given new passwords do not match.'
|
||||
@ -143,12 +294,16 @@ class UserController implements ISecured
|
||||
]);
|
||||
}
|
||||
|
||||
$user->setPlainPassword($this->request->post('password_new'));
|
||||
$user->setPlainPassword($newPassword);
|
||||
}
|
||||
|
||||
$this->pdm->saveToDb($user);
|
||||
$user->setNickname(\Container::$request->post('nickname'));
|
||||
$user->setPhone(\Container::$request->post('phone'));
|
||||
$user->setIdNumber(\Container::$request->post('id_number'));
|
||||
|
||||
$this->request->session()->delete('authenticated_with_google_until');
|
||||
\Container::$persistentDataManager->saveToDb($user);
|
||||
|
||||
\Container::$request->session()->delete('authenticated_with_google_until');
|
||||
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
38
src/Controller/UserSearchController.php
Normal file
38
src/Controller/UserSearchController.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php namespace RVR\Controller;
|
||||
|
||||
use RVR\Repository\UserRepository;
|
||||
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
|
||||
use SokoWeb\Interfaces\Response\IContent;
|
||||
use SokoWeb\Response\JsonContent;
|
||||
|
||||
class UserSearchController implements IAuthenticationRequired
|
||||
{
|
||||
private UserRepository $userRepository;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->userRepository = new UserRepository();
|
||||
}
|
||||
|
||||
public function isAuthenticationRequired(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function searchUser(): IContent
|
||||
{
|
||||
$users = iterator_to_array($this->userRepository->searchByName(\Container::$request->query('q')));
|
||||
usort($users, function($a, $b) {
|
||||
return strnatcmp($a->getDisplayName(), $b->getDisplayName());
|
||||
});
|
||||
|
||||
$results = [];
|
||||
foreach ($users as $user) {
|
||||
$results[] = ['value' => $user->getId(), 'label' => $user->getFullDisplayName()];
|
||||
}
|
||||
|
||||
return new JsonContent([
|
||||
'results' => $results
|
||||
]);
|
||||
}
|
||||
}
|
18
src/Database/AuditLogger.php
Normal file
18
src/Database/AuditLogger.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php namespace RVR\Database;
|
||||
|
||||
use SokoWeb\Database\AuditLoggerBase;
|
||||
|
||||
class AuditLogger extends AuditLoggerBase
|
||||
{
|
||||
protected function getModifierId()
|
||||
{
|
||||
if (\Container::$request === null) {
|
||||
return null;
|
||||
}
|
||||
$user = \Container::$request->user();
|
||||
if ($user === null) {
|
||||
return null;
|
||||
}
|
||||
return $user->getUniqueId();
|
||||
}
|
||||
}
|
136
src/Finance/BalanceCalculator.php
Normal file
136
src/Finance/BalanceCalculator.php
Normal file
@ -0,0 +1,136 @@
|
||||
<?php namespace RVR\Finance;
|
||||
|
||||
use Container;
|
||||
use RVR\PersistentData\Model\Community;
|
||||
use RVR\PersistentData\Model\Event;
|
||||
use RVR\PersistentData\Model\User;
|
||||
use RVR\Repository\CommunityMemberRepository;
|
||||
use RVR\Repository\TransactionRepository;
|
||||
|
||||
class BalanceCalculator
|
||||
{
|
||||
private Community $community;
|
||||
|
||||
private User $user;
|
||||
|
||||
private ?Event $event;
|
||||
|
||||
private TransactionRepository $transactionRepository;
|
||||
|
||||
private CommunityMemberRepository $communityMemberRepository;
|
||||
|
||||
private ExchangeRateCalculator $exchangeRateCalculator;
|
||||
|
||||
private array $members;
|
||||
|
||||
private array $payments;
|
||||
|
||||
private array $actualDebts;
|
||||
|
||||
public function __construct(Community $community, User $user, ?Event $event = null)
|
||||
{
|
||||
$this->community = $community;
|
||||
$this->user = $user;
|
||||
$this->event = $event;
|
||||
$this->transactionRepository = new TransactionRepository();
|
||||
$this->communityMemberRepository = new CommunityMemberRepository();
|
||||
$this->exchangeRateCalculator = new ExchangeRateCalculator($this->community->getMainCurrency());
|
||||
}
|
||||
|
||||
public function calculate(): array
|
||||
{
|
||||
$this->collectMembers();
|
||||
$this->createPaymentsMatrix();
|
||||
$this->sumTransactions();
|
||||
$this->calculateActualDebts();
|
||||
return $this->calculateBalanceForUser();
|
||||
}
|
||||
|
||||
private function collectMembers(): void
|
||||
{
|
||||
$this->members = [];
|
||||
foreach ($this->communityMemberRepository->getAllByCommunity($this->community, true, ['user']) as $member) {
|
||||
$this->members[$member->getUserId()] = $member;
|
||||
}
|
||||
}
|
||||
|
||||
private function createPaymentsMatrix(): void
|
||||
{
|
||||
$this->payments = [];
|
||||
foreach ($this->members as $payerUserId => $member) {
|
||||
$this->payments[$payerUserId] = [];
|
||||
foreach ($this->members as $payeeUserId => $member) {
|
||||
$this->payments[$payerUserId][$payeeUserId] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function sumTransactions(): void
|
||||
{
|
||||
$membersCount = count($this->members);
|
||||
if ($this->event !== null) {
|
||||
$transactions = iterator_to_array($this->transactionRepository->getAllByEvent($this->event, true, ['currency']));
|
||||
} else {
|
||||
$transactions = iterator_to_array($this->transactionRepository->getAllByCommunity($this->community, true, ['currency']));
|
||||
}
|
||||
Container::$persistentDataManager->loadMultiRelationsFromDb($transactions, 'payees');
|
||||
|
||||
foreach ($transactions as $transaction) {
|
||||
$sum = $this->exchangeRateCalculator->calculate($transaction->getSum(), $transaction->getCurrency(), $transaction->getTimeDate());
|
||||
$payees = $transaction->getPayees();
|
||||
$payeeCount = count($payees);
|
||||
|
||||
if ($payeeCount > 0) {
|
||||
foreach ($payees as $payee) {
|
||||
$this->payments[$transaction->getPayerUserId()][$payee->getUserId()] += $sum / $payeeCount;
|
||||
}
|
||||
} else {
|
||||
foreach ($this->members as $payeeUserId => $member) {
|
||||
$this->payments[$transaction->getPayerUserId()][$payeeUserId] += $sum / $membersCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function calculateActualDebts(): void
|
||||
{
|
||||
$this->actualDebts = [];
|
||||
|
||||
foreach ($this->payments as $payerUserId => $paymentsOfPayer) {
|
||||
foreach ($paymentsOfPayer as $payeeUserId => $sum) {
|
||||
$actualDebt = $this->payments[$payeeUserId][$payerUserId] - $sum;
|
||||
|
||||
if (round($actualDebt, $this->community->getMainCurrency()->getRoundDigits()) > 0.0) {
|
||||
$this->actualDebts[] = ['payer' => $this->members[$payerUserId], 'payee' => $this->members[$payeeUserId], 'amount' => $actualDebt];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function calculateBalanceForUser(): array
|
||||
{
|
||||
$debtItems = [];
|
||||
$debtBalance = 0.0;
|
||||
$outstandingItems = [];
|
||||
$outstandingBalance = 0.0;
|
||||
foreach ($this->actualDebts as $debt) {
|
||||
if ($debt['payer']->getId() === $this->user->getId()) {
|
||||
$debtBalance += $debt['amount'];
|
||||
$debtItems[] = $debt;
|
||||
}
|
||||
if ($debt['payee']->getId() === $this->user->getId()) {
|
||||
$outstandingBalance += $debt['amount'];
|
||||
$outstandingItems[] = $debt;
|
||||
}
|
||||
}
|
||||
$absoluteBalance = $outstandingBalance - $debtBalance;
|
||||
|
||||
return [
|
||||
'absoluteBalance' => $absoluteBalance,
|
||||
'debtItems' => $debtItems,
|
||||
'debtBalance' => $debtBalance,
|
||||
'outstandingItems' => $outstandingItems,
|
||||
'outstandingBalance' => $outstandingBalance
|
||||
];
|
||||
}
|
||||
}
|
52
src/Finance/ExchangeRateCalculator.php
Normal file
52
src/Finance/ExchangeRateCalculator.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?php namespace RVR\Finance;
|
||||
|
||||
use DateTime;
|
||||
use RVR\PersistentData\Model\Currency;
|
||||
use RVR\PersistentData\Model\CurrencyExchangeRate;
|
||||
use RVR\Repository\CurrencyExchangeRateRepository;
|
||||
|
||||
class ExchangeRateCalculator
|
||||
{
|
||||
private Currency $mainCurrency;
|
||||
|
||||
private CurrencyExchangeRateRepository $currencyExchangeRateRepository;
|
||||
|
||||
private array $exchangeRates = [];
|
||||
|
||||
public function __construct(Currency $mainCurrency)
|
||||
{
|
||||
$this->mainCurrency = $mainCurrency;
|
||||
$this->currencyExchangeRateRepository = new CurrencyExchangeRateRepository();
|
||||
}
|
||||
|
||||
public function calculate(float $sumInCurrency, Currency $currency, DateTime $time): float
|
||||
{
|
||||
if ($currency->getId() === $this->mainCurrency->getId()) {
|
||||
return $sumInCurrency;
|
||||
}
|
||||
|
||||
$currentExchangeRate = $this->getCurrentExchangeRate($currency, $time);
|
||||
if ($currentExchangeRate === null) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return $sumInCurrency * $currentExchangeRate->getExchangeRate();
|
||||
}
|
||||
|
||||
private function getCurrentExchangeRate(Currency $currency, DateTime $time): ?CurrencyExchangeRate
|
||||
{
|
||||
if (!isset($this->exchangeRates[$currency->getId()])) {
|
||||
$this->exchangeRates[$currency->getId()] = iterator_to_array($this->currencyExchangeRateRepository->getAllByCurrency($currency));
|
||||
}
|
||||
|
||||
$currentExchangeRate = null;
|
||||
foreach ($this->exchangeRates[$currency->getId()] as $exchangeRate) {
|
||||
if ($exchangeRate->getValidFromDate() > $time) {
|
||||
break;
|
||||
}
|
||||
$currentExchangeRate = $exchangeRate;
|
||||
}
|
||||
|
||||
return $currentExchangeRate;
|
||||
}
|
||||
}
|
85
src/PersistentData/Model/Community.php
Normal file
85
src/PersistentData/Model/Community.php
Normal file
@ -0,0 +1,85 @@
|
||||
<?php namespace RVR\PersistentData\Model;
|
||||
|
||||
use DateTime;
|
||||
use SokoWeb\PersistentData\Model\ModelWithSlug;
|
||||
|
||||
class Community extends ModelWithSlug
|
||||
{
|
||||
protected static string $table = 'communities';
|
||||
|
||||
protected static array $fields = ['name', 'currency', 'main_currency_id', 'created'];
|
||||
|
||||
protected static array $relations = ['main_currency' => Currency::class];
|
||||
|
||||
protected static string $slugSource = 'name';
|
||||
|
||||
private string $name = '';
|
||||
|
||||
private string $currency = '';
|
||||
|
||||
private ?Currency $mainCurrency = null;
|
||||
|
||||
private ?int $mainCurrencyId = null;
|
||||
|
||||
private DateTime $created;
|
||||
|
||||
public function setName(string $name): void
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
public function setCurrency(string $currency): void
|
||||
{
|
||||
$this->currency = $currency;
|
||||
}
|
||||
|
||||
public function setMainCurrency(?Currency $mainCurrency): void
|
||||
{
|
||||
$this->mainCurrency = $mainCurrency;
|
||||
}
|
||||
|
||||
public function setMainCurrencyId(?int $mainCurrencyId): void
|
||||
{
|
||||
$this->mainCurrencyId = $mainCurrencyId;
|
||||
}
|
||||
|
||||
public function setCreatedDate(DateTime $created): void
|
||||
{
|
||||
$this->created = $created;
|
||||
}
|
||||
|
||||
public function setCreated(string $created): void
|
||||
{
|
||||
$this->created = new DateTime($created);
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getCurrency(): string
|
||||
{
|
||||
return $this->currency;
|
||||
}
|
||||
|
||||
public function getMainCurrency(): ?Currency
|
||||
{
|
||||
return $this->mainCurrency;
|
||||
}
|
||||
|
||||
public function getMainCurrencyId(): ?int
|
||||
{
|
||||
return $this->mainCurrencyId;
|
||||
}
|
||||
|
||||
public function getCreatedDate(): DateTime
|
||||
{
|
||||
return $this->created;
|
||||
}
|
||||
|
||||
public function getCreated(): string
|
||||
{
|
||||
return $this->created->format('Y-m-d H:i:s');
|
||||
}
|
||||
}
|
72
src/PersistentData/Model/CommunityMember.php
Normal file
72
src/PersistentData/Model/CommunityMember.php
Normal file
@ -0,0 +1,72 @@
|
||||
<?php namespace RVR\PersistentData\Model;
|
||||
|
||||
use SokoWeb\PersistentData\Model\Model;
|
||||
|
||||
class CommunityMember extends Model
|
||||
{
|
||||
protected static string $table = 'community_members';
|
||||
|
||||
protected static array $fields = ['community_id', 'user_id', 'owner'];
|
||||
|
||||
protected static array $relations = ['community' => Community::class, 'user' => User::class];
|
||||
|
||||
private ?Community $community = null;
|
||||
|
||||
private ?int $communityId = null;
|
||||
|
||||
private ?User $user = null;
|
||||
|
||||
private ?int $userId = null;
|
||||
|
||||
private bool $owner = false;
|
||||
|
||||
public function setCommunity(Community $community): void
|
||||
{
|
||||
$this->community = $community;
|
||||
}
|
||||
|
||||
public function setCommunityId(int $communityId): void
|
||||
{
|
||||
$this->communityId = $communityId;
|
||||
}
|
||||
|
||||
public function setUser(User $user): void
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function setUserId(int $userId): void
|
||||
{
|
||||
$this->userId = $userId;
|
||||
}
|
||||
|
||||
public function setOwner(bool $owner): void
|
||||
{
|
||||
$this->owner = $owner;
|
||||
}
|
||||
|
||||
public function getCommunity(): ?Community
|
||||
{
|
||||
return $this->community;
|
||||
}
|
||||
|
||||
public function getCommunityId(): ?int
|
||||
{
|
||||
return $this->communityId;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function getUserId(): ?int
|
||||
{
|
||||
return $this->userId;
|
||||
}
|
||||
|
||||
public function getOwner(): bool
|
||||
{
|
||||
return $this->owner;
|
||||
}
|
||||
}
|
60
src/PersistentData/Model/Currency.php
Normal file
60
src/PersistentData/Model/Currency.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?php namespace RVR\PersistentData\Model;
|
||||
|
||||
use SokoWeb\PersistentData\Model\Model;
|
||||
|
||||
class Currency extends Model
|
||||
{
|
||||
protected static string $table = 'currencies';
|
||||
|
||||
protected static array $fields = ['community_id', 'code', 'round_digits'];
|
||||
|
||||
protected static array $relations = ['community' => Community::class];
|
||||
|
||||
private ?Community $community = null;
|
||||
|
||||
private ?int $communityId = null;
|
||||
|
||||
private string $code = '';
|
||||
|
||||
private int $roundDigits = 0;
|
||||
|
||||
public function setCommunity(Community $community): void
|
||||
{
|
||||
$this->community = $community;
|
||||
}
|
||||
|
||||
public function setCommunityId(int $communityId): void
|
||||
{
|
||||
$this->communityId = $communityId;
|
||||
}
|
||||
|
||||
public function setCode(string $code): void
|
||||
{
|
||||
$this->code = $code;
|
||||
}
|
||||
|
||||
public function setRoundDigits(int $roundDigits): void
|
||||
{
|
||||
$this->roundDigits = $roundDigits;
|
||||
}
|
||||
|
||||
public function getCommunity(): ?Community
|
||||
{
|
||||
return $this->community;
|
||||
}
|
||||
|
||||
public function getCommunityId(): ?int
|
||||
{
|
||||
return $this->communityId;
|
||||
}
|
||||
|
||||
public function getCode(): string
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
public function getRoundDigits(): int
|
||||
{
|
||||
return $this->roundDigits;
|
||||
}
|
||||
}
|
71
src/PersistentData/Model/CurrencyExchangeRate.php
Normal file
71
src/PersistentData/Model/CurrencyExchangeRate.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php namespace RVR\PersistentData\Model;
|
||||
|
||||
use DateTime;
|
||||
use SokoWeb\PersistentData\Model\Model;
|
||||
|
||||
class CurrencyExchangeRate extends Model
|
||||
{
|
||||
protected static string $table = 'currency_exchange_rates';
|
||||
|
||||
protected static array $fields = ['currency_id', 'exchange_rate', 'valid_from'];
|
||||
|
||||
protected static array $relations = ['currency' => Currency::class];
|
||||
|
||||
private ?Currency $currency = null;
|
||||
|
||||
private ?int $currencyId = null;
|
||||
|
||||
private float $exchangeRate = 0.0;
|
||||
|
||||
private DateTime $validFrom;
|
||||
|
||||
public function setCurrency(Currency $currency): void
|
||||
{
|
||||
$this->currency = $currency;
|
||||
}
|
||||
|
||||
public function setCurrencyId(int $currencyId): void
|
||||
{
|
||||
$this->currencyId = $currencyId;
|
||||
}
|
||||
|
||||
public function setExchangeRate(float $exchangeRate): void
|
||||
{
|
||||
$this->exchangeRate = $exchangeRate;
|
||||
}
|
||||
|
||||
public function setValidFromDate(DateTime $validFrom): void
|
||||
{
|
||||
$this->validFrom = $validFrom;
|
||||
}
|
||||
|
||||
public function setValidFrom(string $validFrom): void
|
||||
{
|
||||
$this->validFrom = new DateTime($validFrom);
|
||||
}
|
||||
|
||||
public function getCurrency(): ?Currency
|
||||
{
|
||||
return $this->currency;
|
||||
}
|
||||
|
||||
public function getCurrencyId(): ?int
|
||||
{
|
||||
return $this->currencyId;
|
||||
}
|
||||
|
||||
public function getExchangeRate(): float
|
||||
{
|
||||
return $this->exchangeRate;
|
||||
}
|
||||
|
||||
public function getValidFromDate(): DateTime
|
||||
{
|
||||
return $this->validFrom;
|
||||
}
|
||||
|
||||
public function getValidFrom(): string
|
||||
{
|
||||
return $this->validFrom->format('Y-m-d H:i:s');
|
||||
}
|
||||
}
|
107
src/PersistentData/Model/Event.php
Normal file
107
src/PersistentData/Model/Event.php
Normal file
@ -0,0 +1,107 @@
|
||||
<?php namespace RVR\PersistentData\Model;
|
||||
|
||||
use DateTime;
|
||||
use SokoWeb\PersistentData\Model\ModelWithSlug;
|
||||
|
||||
class Event extends ModelWithSlug
|
||||
{
|
||||
protected static string $table = 'events';
|
||||
|
||||
protected static array $fields = ['community_id', 'start', 'end', 'title', 'description'];
|
||||
|
||||
protected static array $relations = ['community' => Community::class];
|
||||
|
||||
protected static string $slugSource = 'title';
|
||||
|
||||
private ?Community $community = null;
|
||||
|
||||
private int $communityId;
|
||||
|
||||
private DateTime $start;
|
||||
|
||||
private DateTime $end;
|
||||
|
||||
private string $title = '';
|
||||
|
||||
private string $description = '';
|
||||
|
||||
public function setCommunity(Community $community): void
|
||||
{
|
||||
$this->community = $community;
|
||||
}
|
||||
|
||||
public function setCommunityId(int $communityId): void
|
||||
{
|
||||
$this->communityId = $communityId;
|
||||
}
|
||||
|
||||
public function setStartDate(DateTime $start): void
|
||||
{
|
||||
$this->start = $start;
|
||||
}
|
||||
|
||||
public function setStart(string $start): void
|
||||
{
|
||||
$this->start = new DateTime($start);
|
||||
}
|
||||
|
||||
public function setEndDate(DateTime $end): void
|
||||
{
|
||||
$this->end = $end;
|
||||
}
|
||||
|
||||
public function setEnd(string $end): void
|
||||
{
|
||||
$this->end = new DateTime($end);
|
||||
}
|
||||
|
||||
public function setTitle(string $title): void
|
||||
{
|
||||
$this->title = $title;
|
||||
}
|
||||
|
||||
public function setDescription(string $description): void
|
||||
{
|
||||
$this->description = $description;
|
||||
}
|
||||
|
||||
public function getCommunity(): ?Community
|
||||
{
|
||||
return $this->community;
|
||||
}
|
||||
|
||||
public function getCommunityId(): int
|
||||
{
|
||||
return $this->communityId;
|
||||
}
|
||||
|
||||
public function getStartDate(): DateTime
|
||||
{
|
||||
return $this->start;
|
||||
}
|
||||
|
||||
public function getStart(): string
|
||||
{
|
||||
return $this->start->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
public function getEndDate(): DateTime
|
||||
{
|
||||
return $this->end;
|
||||
}
|
||||
|
||||
public function getEnd(): string
|
||||
{
|
||||
return $this->end->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
}
|
91
src/PersistentData/Model/OAuthClient.php
Normal file
91
src/PersistentData/Model/OAuthClient.php
Normal file
@ -0,0 +1,91 @@
|
||||
<?php namespace RVR\PersistentData\Model;
|
||||
|
||||
use DateTime;
|
||||
use SokoWeb\PersistentData\Model\Model;
|
||||
|
||||
class OAuthClient extends Model
|
||||
{
|
||||
protected static string $table = 'oauth_clients';
|
||||
|
||||
protected static array $fields = ['client_id', 'client_secret', 'redirect_uris', 'preapproved', 'created'];
|
||||
|
||||
private string $clientId = '';
|
||||
|
||||
private string $clientSecret = '';
|
||||
|
||||
private array $redirectUris = [];
|
||||
|
||||
private bool $preapproved = false;
|
||||
|
||||
private DateTime $created;
|
||||
|
||||
public function setClientId(string $clientId): void
|
||||
{
|
||||
$this->clientId = $clientId;
|
||||
}
|
||||
|
||||
public function setClientSecret(string $clientSecret): void
|
||||
{
|
||||
$this->clientSecret = $clientSecret;
|
||||
}
|
||||
|
||||
public function setRedirectUrisArray(array $redirectUris): void
|
||||
{
|
||||
$this->redirectUris = $redirectUris;
|
||||
}
|
||||
|
||||
public function setRedirectUris(string $redirectUris): void
|
||||
{
|
||||
$this->redirectUris = json_decode($redirectUris, true);
|
||||
}
|
||||
|
||||
public function setPreapproved(bool $preapproved): void
|
||||
{
|
||||
$this->preapproved = $preapproved;
|
||||
}
|
||||
|
||||
public function setCreatedDate(DateTime $created): void
|
||||
{
|
||||
$this->created = $created;
|
||||
}
|
||||
|
||||
public function setCreated(string $created): void
|
||||
{
|
||||
$this->created = new DateTime($created);
|
||||
}
|
||||
|
||||
public function getClientId(): string
|
||||
{
|
||||
return $this->clientId;
|
||||
}
|
||||
|
||||
public function getClientSecret(): string
|
||||
{
|
||||
return $this->clientSecret;
|
||||
}
|
||||
|
||||
public function getRedirectUrisArray(): array
|
||||
{
|
||||
return $this->redirectUris;
|
||||
}
|
||||
|
||||
public function getRedirectUris(): string
|
||||
{
|
||||
return json_encode($this->redirectUris);
|
||||
}
|
||||
|
||||
public function getPreapproved(): bool
|
||||
{
|
||||
return $this->preapproved;
|
||||
}
|
||||
|
||||
public function getCreatedDate(): DateTime
|
||||
{
|
||||
return $this->created;
|
||||
}
|
||||
|
||||
public function getCreated(): string
|
||||
{
|
||||
return $this->created->format('Y-m-d H:i:s');
|
||||
}
|
||||
}
|
182
src/PersistentData/Model/OAuthSession.php
Normal file
182
src/PersistentData/Model/OAuthSession.php
Normal file
@ -0,0 +1,182 @@
|
||||
<?php namespace RVR\PersistentData\Model;
|
||||
|
||||
use DateTime;
|
||||
use SokoWeb\PersistentData\Model\Model;
|
||||
|
||||
class OAuthSession extends Model
|
||||
{
|
||||
protected static string $table = 'oauth_sessions';
|
||||
|
||||
protected static array $fields = ['client_id', 'scope', 'nonce', 'code_challenge', 'code_challenge_method', 'user_id', 'code', 'created', 'expires', 'token_claimed'];
|
||||
|
||||
protected static array $relations = ['user' => User::class];
|
||||
|
||||
private static array $possibleScopeValues = ['openid', 'email', 'profile', 'union_profile'];
|
||||
|
||||
private static array $possibleCodeChallengeMethodValues = ['plain', 'S256'];
|
||||
|
||||
private string $clientId = '';
|
||||
|
||||
private array $scope = [];
|
||||
|
||||
private string $nonce = '';
|
||||
|
||||
private ?string $codeChallenge = null;
|
||||
|
||||
private ?string $codeChallengeMethod = null;
|
||||
|
||||
private ?User $user = null;
|
||||
|
||||
private ?int $userId = null;
|
||||
|
||||
private string $code = '';
|
||||
|
||||
private DateTime $created;
|
||||
|
||||
private DateTime $expires;
|
||||
|
||||
private bool $tokenClaimed = false;
|
||||
|
||||
public function setScopeArray(array $scope): void
|
||||
{
|
||||
$this->scope = array_intersect($scope, self::$possibleScopeValues);
|
||||
}
|
||||
|
||||
public function setClientId(string $clientId): void
|
||||
{
|
||||
$this->clientId = $clientId;
|
||||
}
|
||||
|
||||
public function setScope(string $scope): void
|
||||
{
|
||||
$this->setScopeArray(explode(' ', $scope));
|
||||
}
|
||||
|
||||
public function setNonce(string $nonce): void
|
||||
{
|
||||
$this->nonce = $nonce;
|
||||
}
|
||||
|
||||
public function setCodeChallenge(?string $codeChallenge): void
|
||||
{
|
||||
$this->codeChallenge = $codeChallenge;
|
||||
}
|
||||
|
||||
public function setCodeChallengeMethod(?string $codeChallengeMethod): void
|
||||
{
|
||||
if ($codeChallengeMethod !== null && !in_array($codeChallengeMethod, self::$possibleCodeChallengeMethodValues)) {
|
||||
throw new \UnexpectedValueException($codeChallengeMethod . ' is not possible for challengeMethod!');
|
||||
}
|
||||
$this->codeChallengeMethod = $codeChallengeMethod;
|
||||
}
|
||||
|
||||
public function setUser(User $user): void
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function setUserId(int $userId): void
|
||||
{
|
||||
$this->userId = $userId;
|
||||
}
|
||||
|
||||
public function setCode(string $code): void
|
||||
{
|
||||
$this->code = $code;
|
||||
}
|
||||
|
||||
public function setCreatedDate(DateTime $created): void
|
||||
{
|
||||
$this->created = $created;
|
||||
}
|
||||
|
||||
public function setExpiresDate(DateTime $expires): void
|
||||
{
|
||||
$this->expires = $expires;
|
||||
}
|
||||
|
||||
public function setCreated(string $created): void
|
||||
{
|
||||
$this->created = new DateTime($created);
|
||||
}
|
||||
|
||||
public function setExpires(string $expires): void
|
||||
{
|
||||
$this->expires = new DateTime($expires);
|
||||
}
|
||||
|
||||
public function setTokenClaimed(bool $tokenClaimed): void
|
||||
{
|
||||
$this->tokenClaimed = $tokenClaimed;
|
||||
}
|
||||
|
||||
public function getClientId(): string
|
||||
{
|
||||
return $this->clientId;
|
||||
}
|
||||
|
||||
public function getScope(): string
|
||||
{
|
||||
return implode(' ', $this->scope);
|
||||
}
|
||||
|
||||
public function getScopeArray(): array
|
||||
{
|
||||
return $this->scope;
|
||||
}
|
||||
|
||||
public function getNonce(): string
|
||||
{
|
||||
return $this->nonce;
|
||||
}
|
||||
|
||||
public function getCodeChallenge(): ?string
|
||||
{
|
||||
return $this->codeChallenge;
|
||||
}
|
||||
|
||||
public function getCodeChallengeMethod(): ?string
|
||||
{
|
||||
return $this->codeChallengeMethod;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function getUserId(): ?int
|
||||
{
|
||||
return $this->userId;
|
||||
}
|
||||
|
||||
public function getCode(): string
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
public function getCreatedDate(): DateTime
|
||||
{
|
||||
return $this->created;
|
||||
}
|
||||
|
||||
public function getCreated(): string
|
||||
{
|
||||
return $this->created->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
public function getExpiresDate(): DateTime
|
||||
{
|
||||
return $this->expires;
|
||||
}
|
||||
|
||||
public function getExpires(): string
|
||||
{
|
||||
return $this->expires->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
public function getTokenClaimed(): bool
|
||||
{
|
||||
return $this->tokenClaimed;
|
||||
}
|
||||
}
|
81
src/PersistentData/Model/OAuthToken.php
Normal file
81
src/PersistentData/Model/OAuthToken.php
Normal file
@ -0,0 +1,81 @@
|
||||
<?php namespace RVR\PersistentData\Model;
|
||||
|
||||
use DateTime;
|
||||
use SokoWeb\PersistentData\Model\Model;
|
||||
|
||||
class OAuthToken extends Model
|
||||
{
|
||||
protected static string $table = 'oauth_tokens';
|
||||
|
||||
protected static array $fields = ['session_id', 'created', 'expires'];
|
||||
|
||||
protected static array $relations = ['session' => OAuthSession::class];
|
||||
|
||||
private ?OAuthSession $session = null;
|
||||
|
||||
private ?int $sessionId = null;
|
||||
|
||||
private DateTime $created;
|
||||
|
||||
private DateTime $expires;
|
||||
|
||||
public function setSession(OAuthSession $session): void
|
||||
{
|
||||
$this->session = $session;
|
||||
}
|
||||
|
||||
public function setSessionId(int $sessionId): void
|
||||
{
|
||||
$this->sessionId = $sessionId;
|
||||
}
|
||||
|
||||
public function setCreatedDate(DateTime $created): void
|
||||
{
|
||||
$this->created = $created;
|
||||
}
|
||||
|
||||
public function setExpiresDate(DateTime $expires): void
|
||||
{
|
||||
$this->expires = $expires;
|
||||
}
|
||||
|
||||
public function setCreated(string $created): void
|
||||
{
|
||||
$this->created = new DateTime($created);
|
||||
}
|
||||
|
||||
public function setExpires(string $expires): void
|
||||
{
|
||||
$this->expires = new DateTime($expires);
|
||||
}
|
||||
|
||||
public function getSession(): ?OAuthSession
|
||||
{
|
||||
return $this->session;
|
||||
}
|
||||
|
||||
public function getSessionId(): ?int
|
||||
{
|
||||
return $this->sessionId;
|
||||
}
|
||||
|
||||
public function getCreatedDate(): DateTime
|
||||
{
|
||||
return $this->created;
|
||||
}
|
||||
|
||||
public function getCreated(): string
|
||||
{
|
||||
return $this->created->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
public function getExpiresDate(): DateTime
|
||||
{
|
||||
return $this->expires;
|
||||
}
|
||||
|
||||
public function getExpires(): string
|
||||
{
|
||||
return $this->expires->format('Y-m-d H:i:s');
|
||||
}
|
||||
}
|
201
src/PersistentData/Model/Transaction.php
Normal file
201
src/PersistentData/Model/Transaction.php
Normal file
@ -0,0 +1,201 @@
|
||||
<?php namespace RVR\PersistentData\Model;
|
||||
|
||||
use DateTime;
|
||||
use SokoWeb\PersistentData\Model\Model;
|
||||
|
||||
class Transaction extends Model
|
||||
{
|
||||
protected static string $table = 'transactions';
|
||||
|
||||
protected static array $fields = ['community_id', 'event_id', 'currency_id', 'payer_user_id', 'payee_user_id', 'description', 'sum', 'time'];
|
||||
|
||||
protected static array $relations = [
|
||||
'community' => Community::class,
|
||||
'event' => Event::class,
|
||||
'currency' => Currency::class,
|
||||
'payer_user' => User::class,
|
||||
'payee_user' => User::class
|
||||
];
|
||||
|
||||
protected static array $multiRelations = [
|
||||
'payees' => [TransactionPayee::class, 'transaction']
|
||||
];
|
||||
|
||||
private ?Community $community = null;
|
||||
|
||||
private int $communityId;
|
||||
|
||||
private ?Event $event = null;
|
||||
|
||||
private ?int $eventId = null;
|
||||
|
||||
private ?Currency $currency = null;
|
||||
|
||||
private int $currencyId;
|
||||
|
||||
private ?User $payerUser = null;
|
||||
|
||||
private int $payerUserId;
|
||||
|
||||
private ?User $payeeUser = null;
|
||||
|
||||
private ?int $payeeUserId = null;
|
||||
|
||||
private ?array $payees = null;
|
||||
|
||||
private string $description = '';
|
||||
|
||||
private float $sum = 0.0;
|
||||
|
||||
private DateTime $time;
|
||||
|
||||
public function setCommunity(Community $community): void
|
||||
{
|
||||
$this->community = $community;
|
||||
}
|
||||
|
||||
public function setCommunityId(int $communityId): void
|
||||
{
|
||||
$this->communityId = $communityId;
|
||||
}
|
||||
|
||||
public function setEvent(?Event $event): void
|
||||
{
|
||||
$this->event = $event;
|
||||
}
|
||||
|
||||
public function setEventId(?int $eventId): void
|
||||
{
|
||||
$this->eventId = $eventId;
|
||||
}
|
||||
|
||||
public function setCurrency(Currency $currency): void
|
||||
{
|
||||
$this->currency = $currency;
|
||||
}
|
||||
|
||||
public function setCurrencyId(int $currencyId): void
|
||||
{
|
||||
$this->currencyId = $currencyId;
|
||||
}
|
||||
|
||||
public function setPayerUser(User $payerUser): void
|
||||
{
|
||||
$this->payerUser = $payerUser;
|
||||
}
|
||||
|
||||
public function setPayerUserId(int $payerUserId): void
|
||||
{
|
||||
$this->payerUserId = $payerUserId;
|
||||
}
|
||||
|
||||
public function setPayeeUser(?User $payeeUser): void
|
||||
{
|
||||
$this->payeeUser = $payeeUser;
|
||||
}
|
||||
|
||||
public function setPayeeUserId(?int $payeeUserId): void
|
||||
{
|
||||
$this->payeeUserId = $payeeUserId;
|
||||
}
|
||||
|
||||
public function setPayees(array $payees): void
|
||||
{
|
||||
$this->payees = $payees;
|
||||
}
|
||||
|
||||
public function setDescription(string $description): void
|
||||
{
|
||||
$this->description = $description;
|
||||
}
|
||||
|
||||
public function setSum(float $sum): void
|
||||
{
|
||||
$this->sum = $sum;
|
||||
}
|
||||
|
||||
public function setTimeDate(DateTime $time): void
|
||||
{
|
||||
$this->time = $time;
|
||||
}
|
||||
|
||||
public function setTime(string $time): void
|
||||
{
|
||||
$this->time = new DateTime($time);
|
||||
}
|
||||
|
||||
public function getCommunity(): ?Community
|
||||
{
|
||||
return $this->community;
|
||||
}
|
||||
|
||||
public function getCommunityId(): int
|
||||
{
|
||||
return $this->communityId;
|
||||
}
|
||||
|
||||
public function getEvent(): ?Event
|
||||
{
|
||||
return $this->event;
|
||||
}
|
||||
|
||||
public function getEventId(): ?int
|
||||
{
|
||||
return $this->eventId;
|
||||
}
|
||||
|
||||
public function getCurrency(): ?Currency
|
||||
{
|
||||
return $this->currency;
|
||||
}
|
||||
|
||||
public function getCurrencyId(): int
|
||||
{
|
||||
return $this->currencyId;
|
||||
}
|
||||
|
||||
public function getPayerUser(): ?User
|
||||
{
|
||||
return $this->payerUser;
|
||||
}
|
||||
|
||||
public function getPayerUserId(): int
|
||||
{
|
||||
return $this->payerUserId;
|
||||
}
|
||||
|
||||
public function getPayeeUser(): ?User
|
||||
{
|
||||
return $this->payeeUser;
|
||||
}
|
||||
|
||||
public function getPayeeUserId(): ?int
|
||||
{
|
||||
return $this->payeeUserId;
|
||||
}
|
||||
|
||||
public function getPayees(): ?array
|
||||
{
|
||||
return $this->payees;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function getSum(): float
|
||||
{
|
||||
return $this->sum;
|
||||
}
|
||||
|
||||
public function getTimeDate(): DateTime
|
||||
{
|
||||
return $this->time;
|
||||
}
|
||||
|
||||
public function getTime(): string
|
||||
{
|
||||
return $this->time->format('Y-m-d H:i:s');
|
||||
}
|
||||
}
|
60
src/PersistentData/Model/TransactionPayee.php
Normal file
60
src/PersistentData/Model/TransactionPayee.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?php namespace RVR\PersistentData\Model;
|
||||
|
||||
use SokoWeb\PersistentData\Model\Model;
|
||||
|
||||
class TransactionPayee extends Model
|
||||
{
|
||||
protected static string $table = 'transaction_payees';
|
||||
|
||||
protected static array $fields = ['transaction_id', 'user_id'];
|
||||
|
||||
protected static array $relations = ['transaction' => Transaction::class, 'user' => User::class];
|
||||
|
||||
private ?Transaction $transaction = null;
|
||||
|
||||
private ?int $transactionId = null;
|
||||
|
||||
private ?User $user = null;
|
||||
|
||||
private ?int $userId = null;
|
||||
|
||||
public function setTransaction(Transaction $transaction): void
|
||||
{
|
||||
$this->transaction = $transaction;
|
||||
}
|
||||
|
||||
public function setTransactionId(int $transactionId): void
|
||||
{
|
||||
$this->transactionId = $transactionId;
|
||||
}
|
||||
|
||||
public function setUser(User $user): void
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function setUserId(int $userId): void
|
||||
{
|
||||
$this->userId = $userId;
|
||||
}
|
||||
|
||||
public function getTransaction(): ?Transaction
|
||||
{
|
||||
return $this->transaction;
|
||||
}
|
||||
|
||||
public function getTransactionId(): ?int
|
||||
{
|
||||
return $this->transactionId;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function getUserId(): ?int
|
||||
{
|
||||
return $this->userId;
|
||||
}
|
||||
}
|
@ -8,12 +8,14 @@ class User extends Model implements IUser
|
||||
{
|
||||
protected static string $table = 'users';
|
||||
|
||||
protected static array $fields = ['email', 'password', 'type', 'google_sub', 'created'];
|
||||
protected static array $fields = ['email', 'username', 'password', 'type', 'google_sub', 'created', 'full_name', 'nickname', 'phone', 'id_number'];
|
||||
|
||||
private static array $types = ['user', 'admin'];
|
||||
|
||||
private string $email = '';
|
||||
|
||||
private ?string $username = null;
|
||||
|
||||
private ?string $password = null;
|
||||
|
||||
private string $type = 'user';
|
||||
@ -22,11 +24,24 @@ class User extends Model implements IUser
|
||||
|
||||
private DateTime $created;
|
||||
|
||||
private string $fullName = '';
|
||||
|
||||
private string $nickname = '';
|
||||
|
||||
private string $phone = '';
|
||||
|
||||
private string $idNumber = '';
|
||||
|
||||
public function setEmail(string $email): void
|
||||
{
|
||||
$this->email = $email;
|
||||
}
|
||||
|
||||
public function setUsername(?string $username): void
|
||||
{
|
||||
$this->username = $username;
|
||||
}
|
||||
|
||||
public function setPassword(?string $hashedPassword): void
|
||||
{
|
||||
$this->password = $hashedPassword;
|
||||
@ -59,11 +74,36 @@ class User extends Model implements IUser
|
||||
$this->created = new DateTime($created);
|
||||
}
|
||||
|
||||
public function setFullName(string $fullName): void
|
||||
{
|
||||
$this->fullName = $fullName;
|
||||
}
|
||||
|
||||
public function setNickname(string $nickname): void
|
||||
{
|
||||
$this->nickname = $nickname;
|
||||
}
|
||||
|
||||
public function setPhone(string $phone): void
|
||||
{
|
||||
$this->phone = $phone;
|
||||
}
|
||||
|
||||
public function setIdNumber(string $idNumber): void
|
||||
{
|
||||
$this->idNumber = $idNumber;
|
||||
}
|
||||
|
||||
public function getEmail(): string
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function getUsername(): ?string
|
||||
{
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
public function getPassword(): ?string
|
||||
{
|
||||
return $this->password;
|
||||
@ -89,6 +129,26 @@ class User extends Model implements IUser
|
||||
return $this->created->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
public function getFullName(): string
|
||||
{
|
||||
return $this->fullName;
|
||||
}
|
||||
|
||||
public function getNickname(): string
|
||||
{
|
||||
return $this->nickname;
|
||||
}
|
||||
|
||||
public function getPhone(): string
|
||||
{
|
||||
return $this->phone;
|
||||
}
|
||||
|
||||
public function getIdNumber(): string
|
||||
{
|
||||
return $this->idNumber;
|
||||
}
|
||||
|
||||
public function hasPermission(int $permission): bool
|
||||
{
|
||||
switch ($permission) {
|
||||
@ -108,7 +168,12 @@ class User extends Model implements IUser
|
||||
|
||||
public function getDisplayName(): string
|
||||
{
|
||||
return $this->email;
|
||||
return $this->nickname ?: $this->fullName;
|
||||
}
|
||||
|
||||
public function getFullDisplayName(): string
|
||||
{
|
||||
return $this->nickname ? $this->nickname . ' (' . $this->fullName . ')' : $this->fullName;
|
||||
}
|
||||
|
||||
public function checkPassword(string $password): bool
|
||||
|
51
src/Repository/CommunityMemberRepository.php
Normal file
51
src/Repository/CommunityMemberRepository.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php namespace RVR\Repository;
|
||||
|
||||
use Generator;
|
||||
use RVR\PersistentData\Model\Community;
|
||||
use RVR\PersistentData\Model\CommunityMember;
|
||||
use RVR\PersistentData\Model\User;
|
||||
use SokoWeb\Database\Query\Select;
|
||||
|
||||
class CommunityMemberRepository
|
||||
{
|
||||
public function getById(int $id): ?CommunityMember
|
||||
{
|
||||
return \Container::$persistentDataManager->selectFromDbById($id, CommunityMember::class);
|
||||
}
|
||||
|
||||
public function getAllByCommunity(Community $community, bool $useRelations = false, array $withRelations = []): Generator
|
||||
{
|
||||
$select = $this->selectAllByCommunity($community);
|
||||
|
||||
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, CommunityMember::class, $useRelations, $withRelations);
|
||||
}
|
||||
|
||||
public function countAllByCommunity(Community $community): int
|
||||
{
|
||||
return $this->selectAllByCommunity($community)->count();
|
||||
}
|
||||
|
||||
public function getAllByUser(User $user, bool $useRelations = false, array $withRelations = []): Generator
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('user_id', '=', $user->getId());
|
||||
|
||||
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, CommunityMember::class, $useRelations, $withRelations);
|
||||
}
|
||||
|
||||
public function getByCommunityAndUser(Community $community, User $user) : ?CommunityMember
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('community_id', '=', $community->getId());
|
||||
$select->where('user_id', '=', $user->getId());
|
||||
|
||||
return \Container::$persistentDataManager->selectFromDb($select, CommunityMember::class);
|
||||
}
|
||||
|
||||
private function selectAllByCommunity(Community $community): Select
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection, CommunityMember::getTable());
|
||||
$select->where('community_id', '=', $community->getId());
|
||||
return $select;
|
||||
}
|
||||
}
|
16
src/Repository/CommunityRepository.php
Normal file
16
src/Repository/CommunityRepository.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php namespace RVR\Repository;
|
||||
|
||||
use RVR\PersistentData\Model\Community;
|
||||
|
||||
class CommunityRepository
|
||||
{
|
||||
public function getById(int $id): ?Community
|
||||
{
|
||||
return \Container::$persistentDataManager->selectFromDbById($id, Community::class);
|
||||
}
|
||||
|
||||
public function getBySlug(string $slug): ?Community
|
||||
{
|
||||
return \Container::$persistentDataManager->selectFromDbBySlug($slug, Community::class);
|
||||
}
|
||||
}
|
24
src/Repository/CurrencyExchangeRateRepository.php
Normal file
24
src/Repository/CurrencyExchangeRateRepository.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php namespace RVR\Repository;
|
||||
|
||||
use Generator;
|
||||
use Container;
|
||||
use RVR\PersistentData\Model\Currency;
|
||||
use RVR\PersistentData\Model\CurrencyExchangeRate;
|
||||
use SokoWeb\Database\Query\Select;
|
||||
|
||||
class CurrencyExchangeRateRepository
|
||||
{
|
||||
public function getById(int $id): ?CurrencyExchangeRate
|
||||
{
|
||||
return \Container::$persistentDataManager->selectFromDbById($id, CurrencyExchangeRate::class);
|
||||
}
|
||||
|
||||
public function getAllByCurrency(Currency $currency): Generator
|
||||
{
|
||||
$select = new Select(Container::$dbConnection);
|
||||
$select->where('currency_id', '=', $currency->getId());
|
||||
$select->orderBy('valid_from');
|
||||
|
||||
yield from Container::$persistentDataManager->selectMultipleFromDb($select, CurrencyExchangeRate::class);
|
||||
}
|
||||
}
|
32
src/Repository/CurrencyRepository.php
Normal file
32
src/Repository/CurrencyRepository.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php namespace RVR\Repository;
|
||||
|
||||
use Generator;
|
||||
use Container;
|
||||
use RVR\PersistentData\Model\Community;
|
||||
use RVR\PersistentData\Model\Currency;
|
||||
use SokoWeb\Database\Query\Select;
|
||||
|
||||
class CurrencyRepository
|
||||
{
|
||||
public function getById(int $id): ?Currency
|
||||
{
|
||||
return \Container::$persistentDataManager->selectFromDbById($id, Currency::class);
|
||||
}
|
||||
|
||||
public function getAllByCommunity(Community $community, bool $useRelations = false): Generator
|
||||
{
|
||||
$select = new Select(Container::$dbConnection);
|
||||
$select->where('community_id', '=', $community->getId());
|
||||
|
||||
yield from Container::$persistentDataManager->selectMultipleFromDb($select, Currency::class, $useRelations);
|
||||
}
|
||||
|
||||
public function getByCommunityAndCurrencyCode(Community $community, string $code): ?Currency
|
||||
{
|
||||
$select = new Select(Container::$dbConnection);
|
||||
$select->where('community_id', '=', $community->getId());
|
||||
$select->where('code', '=', $code);
|
||||
|
||||
return Container::$persistentDataManager->selectFromDb($select, Currency::class);
|
||||
}
|
||||
}
|
119
src/Repository/EventRepository.php
Normal file
119
src/Repository/EventRepository.php
Normal file
@ -0,0 +1,119 @@
|
||||
<?php namespace RVR\Repository;
|
||||
|
||||
use Container;
|
||||
use DateTime;
|
||||
use DateInterval;
|
||||
use Generator;
|
||||
use RVR\PersistentData\Model\Community;
|
||||
use RVR\PersistentData\Model\Event;
|
||||
use RVR\PersistentData\Model\User;
|
||||
use SokoWeb\Database\Query\Select;
|
||||
|
||||
class EventRepository
|
||||
{
|
||||
public function getById(int $id): ?Event
|
||||
{
|
||||
return Container::$persistentDataManager->selectFromDbById($id, Event::class);
|
||||
}
|
||||
|
||||
public function getBySlug(string $slug): ?Event
|
||||
{
|
||||
return \Container::$persistentDataManager->selectFromDbBySlug($slug, Event::class);
|
||||
}
|
||||
|
||||
public function getAllByCommunity(Community $community, bool $useRelations = false, array $withRelations = []): Generator
|
||||
{
|
||||
$select = new Select(Container::$dbConnection, Event::getTable());
|
||||
$this->selectAllByCommunity($select, $community);
|
||||
|
||||
yield from Container::$persistentDataManager->selectMultipleFromDb($select, Event::class, $useRelations, $withRelations);
|
||||
}
|
||||
|
||||
public function countAllByCommunity(Community $community): int
|
||||
{
|
||||
$select = new Select(Container::$dbConnection, Event::getTable());
|
||||
$this->selectAllByCommunity($select, $community);
|
||||
|
||||
return $select->count();
|
||||
}
|
||||
|
||||
public function getUpcomingAndRecentByCommunity(Community $community, DateTime $from, int $days, int $limit, bool $useRelations = false, array $withRelations = []): Generator
|
||||
{
|
||||
$select = new Select(Container::$dbConnection, Event::getTable());
|
||||
$this->selectAllByCommunity($select, $community);
|
||||
$this->selectUpcomingAndRecent($select, $from, $days);
|
||||
$select->orderBy('start', 'DESC');
|
||||
$select->limit($limit, 0);
|
||||
|
||||
yield from Container::$persistentDataManager->selectMultipleFromDb($select, Event::class, $useRelations, $withRelations);
|
||||
}
|
||||
|
||||
public function getUpcomingAndRecentByUser(User $user, DateTime $from, int $days, int $limit, bool $useRelations = false, array $withRelations = []): Generator
|
||||
{
|
||||
$select = new Select(Container::$dbConnection, Event::getTable());
|
||||
$this->selectAllByUser($select, $user);
|
||||
$this->selectUpcomingAndRecent($select, $from, $days);
|
||||
$select->orderBy('start', 'DESC');
|
||||
$select->limit($limit, 0);
|
||||
|
||||
yield from Container::$persistentDataManager->selectMultipleFromDb($select, Event::class, $useRelations, $withRelations);
|
||||
}
|
||||
|
||||
public function getCurrentByUser(User $user, DateTime $from, int $days, bool $useRelations = false, array $withRelations = []): ?Event
|
||||
{
|
||||
$select = new Select(Container::$dbConnection, Event::getTable());
|
||||
$this->selectAllByUser($select, $user);
|
||||
$this->selectUpcomingAndRecent($select, $from, $days);
|
||||
$select->orderBy('start', 'DESC');
|
||||
$select->limit(1, 0);
|
||||
|
||||
return Container::$persistentDataManager->selectFromDb($select, Event::class, $useRelations, $withRelations);
|
||||
}
|
||||
|
||||
public function getPagedByCommunity(Community $community, int $page, int $itemsPerPage, bool $useRelations = false, array $withRelations = []): Generator
|
||||
{
|
||||
$select = new Select(Container::$dbConnection, Event::getTable());
|
||||
$this->selectAllByCommunity($select, $community);
|
||||
$select->orderBy('start', 'DESC');
|
||||
$select->paginate($page, $itemsPerPage);
|
||||
|
||||
yield from Container::$persistentDataManager->selectMultipleFromDb($select, Event::class, $useRelations, $withRelations);
|
||||
}
|
||||
|
||||
public function searchByTitle(Community $community, string $title): Generator
|
||||
{
|
||||
$select = new Select(Container::$dbConnection, Event::getTable());
|
||||
$this->selectAllByCommunity($select, $community);
|
||||
$select->where('title', 'LIKE', '%' . $title . '%');
|
||||
$select->orderBy('start', 'DESC');
|
||||
$select->limit(10);
|
||||
|
||||
yield from Container::$persistentDataManager->selectMultipleFromDb($select, Event::class);
|
||||
}
|
||||
|
||||
private function selectAllByCommunity(Select $select, Community $community): void
|
||||
{
|
||||
$select->where('community_id', '=', $community->getId());
|
||||
}
|
||||
|
||||
private function selectAllByUser(Select $select, User $user): void
|
||||
{
|
||||
$select->innerJoin('communities', ['communities', 'id'], '=', ['events', 'community_id']);
|
||||
$select->innerJoin('community_members', ['communities', 'id'], '=', ['community_members', 'community_id']);
|
||||
$select->where(['community_members', 'user_id'], '=', $user->getId());
|
||||
}
|
||||
|
||||
private function selectUpcomingAndRecent(Select $select, DateTime $from, int $days): void
|
||||
{
|
||||
$select->where(function (Select $select) use ($from, $days) {
|
||||
$select->where(function (Select $select) use ($from, $days) {
|
||||
$select->where('start', '<', (clone $from)->add(DateInterval::createFromDateString("$days days"))->format('Y-m-d H:i:s'));
|
||||
$select->where('end', '>', $from->format('Y-m-d H:i:s'));
|
||||
});
|
||||
$select->orWhere(function (Select $select) use ($from, $days) {
|
||||
$select->where('end', '>', (clone $from)->sub(DateInterval::createFromDateString("$days days"))->format('Y-m-d H:i:s'));
|
||||
$select->where('start', '<', $from->format('Y-m-d H:i:s'));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
20
src/Repository/OAuthClientRepository.php
Normal file
20
src/Repository/OAuthClientRepository.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php namespace RVR\Repository;
|
||||
|
||||
use SokoWeb\Database\Query\Select;
|
||||
use RVR\PersistentData\Model\OAuthClient;
|
||||
|
||||
class OAuthClientRepository
|
||||
{
|
||||
public function getById(int $id): ?OAuthClient
|
||||
{
|
||||
return \Container::$persistentDataManager->selectFromDbById($id, OAuthClient::class);
|
||||
}
|
||||
|
||||
public function getByClientId(string $clientId): ?OAuthClient
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('client_id', '=', $clientId);
|
||||
|
||||
return \Container::$persistentDataManager->selectFromDb($select, OAuthClient::class);
|
||||
}
|
||||
}
|
30
src/Repository/OAuthSessionRepository.php
Normal file
30
src/Repository/OAuthSessionRepository.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php namespace RVR\Repository;
|
||||
|
||||
use DateTime;
|
||||
use Generator;
|
||||
use SokoWeb\Database\Query\Select;
|
||||
use RVR\PersistentData\Model\OAuthSession;
|
||||
|
||||
class OAuthSessionRepository
|
||||
{
|
||||
public function getById(int $id): ?OAuthSession
|
||||
{
|
||||
return \Container::$persistentDataManager->selectFromDbById($id, OAuthSession::class);
|
||||
}
|
||||
|
||||
public function getByCode(string $code): ?OAuthSession
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('code', '=', $code);
|
||||
|
||||
return \Container::$persistentDataManager->selectFromDb($select, OAuthSession::class);
|
||||
}
|
||||
|
||||
public function getAllExpired(): Generator
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('expires', '<', (new DateTime())->format('Y-m-d H:i:s'));
|
||||
|
||||
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, OAuthSession::class);
|
||||
}
|
||||
}
|
42
src/Repository/OAuthTokenRepository.php
Normal file
42
src/Repository/OAuthTokenRepository.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php namespace RVR\Repository;
|
||||
|
||||
use DateTime;
|
||||
use Generator;
|
||||
use SokoWeb\Database\Query\Select;
|
||||
use RVR\PersistentData\Model\OAuthToken;
|
||||
use RVR\PersistentData\Model\OAuthSession;
|
||||
|
||||
class OAuthTokenRepository
|
||||
{
|
||||
public function getById(int $id): ?OAuthToken
|
||||
{
|
||||
return \Container::$persistentDataManager->selectFromDbById($id, OAuthToken::class);
|
||||
}
|
||||
|
||||
public function getAllBySession(OAuthSession $session, bool $useRelations = false, array $withRelations = []): Generator
|
||||
{
|
||||
$select = $this->selectAllBySession($session);
|
||||
|
||||
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, OAuthToken::class, $useRelations, $withRelations);
|
||||
}
|
||||
|
||||
public function countAllBySession(OAuthSession $session): int
|
||||
{
|
||||
return $this->selectAllBySession($session)->count();
|
||||
}
|
||||
|
||||
public function getAllExpired(): Generator
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('expires', '<', (new DateTime())->format('Y-m-d H:i:s'));
|
||||
|
||||
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, OAuthToken::class);
|
||||
}
|
||||
|
||||
private function selectAllBySession(OAuthSession $session): Select
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection, OAuthToken::getTable());
|
||||
$select->where('session_id', '=', $session->getId());
|
||||
return $select;
|
||||
}
|
||||
}
|
23
src/Repository/TransactionPayeeRepository.php
Normal file
23
src/Repository/TransactionPayeeRepository.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php namespace RVR\Repository;
|
||||
|
||||
use Container;
|
||||
use Generator;
|
||||
use RVR\PersistentData\Model\Transaction;
|
||||
use RVR\PersistentData\Model\TransactionPayee;
|
||||
use SokoWeb\Database\Query\Select;
|
||||
|
||||
class TransactionPayeeRepository
|
||||
{
|
||||
public function getById(int $id): ?TransactionPayee
|
||||
{
|
||||
return Container::$persistentDataManager->selectFromDbById($id, TransactionPayee::class);
|
||||
}
|
||||
|
||||
public function getAllByTransaction(Transaction $transaction, bool $useRelations = false, array $withRelations = []): Generator
|
||||
{
|
||||
$select = new Select(Container::$dbConnection);
|
||||
$select->where('transaction_id', '=', $transaction->getId());
|
||||
|
||||
yield from Container::$persistentDataManager->selectMultipleFromDb($select, TransactionPayee::class, $useRelations, $withRelations);
|
||||
}
|
||||
}
|
100
src/Repository/TransactionRepository.php
Normal file
100
src/Repository/TransactionRepository.php
Normal file
@ -0,0 +1,100 @@
|
||||
<?php namespace RVR\Repository;
|
||||
|
||||
use Container;
|
||||
use Generator;
|
||||
use RVR\PersistentData\Model\Community;
|
||||
use RVR\PersistentData\Model\Currency;
|
||||
use RVR\PersistentData\Model\Event;
|
||||
use RVR\PersistentData\Model\Transaction;
|
||||
use RVR\PersistentData\Model\User;
|
||||
use SokoWeb\Database\Query\Select;
|
||||
|
||||
class TransactionRepository
|
||||
{
|
||||
public function getById(int $id): ?Transaction
|
||||
{
|
||||
return Container::$persistentDataManager->selectFromDbById($id, Transaction::class);
|
||||
}
|
||||
|
||||
public function getAllByCommunity(Community $community, bool $useRelations = false, array $withRelations = []): Generator
|
||||
{
|
||||
$select = $this->selectAllByCommunity($community);
|
||||
|
||||
yield from Container::$persistentDataManager->selectMultipleFromDb($select, Transaction::class, $useRelations, $withRelations);
|
||||
}
|
||||
|
||||
public function getAllByEvent(Event $event, bool $useRelations = false, array $withRelations = []): Generator
|
||||
{
|
||||
$select = $this->selectAllByEvent($event);
|
||||
|
||||
yield from Container::$persistentDataManager->selectMultipleFromDb($select, Transaction::class, $useRelations, $withRelations);
|
||||
}
|
||||
|
||||
public function countAllByCommunity(Community $community): int
|
||||
{
|
||||
return $this->selectAllByCommunity($community)->count();
|
||||
}
|
||||
|
||||
public function countAllByEvent(Event $event): int
|
||||
{
|
||||
return $this->selectAllByEvent($event)->count();
|
||||
}
|
||||
|
||||
public function isAnyForUser(User $user): bool
|
||||
{
|
||||
$select = new Select(Container::$dbConnection, Transaction::getTable());
|
||||
$select->where('payer_user_id', '=', $user->getId());
|
||||
$select->orWhere('payee_user_id', '=', $user->getId());
|
||||
$select->orWhere('payee_user_id', '=', null);
|
||||
|
||||
return $select->count() > 0;
|
||||
}
|
||||
|
||||
public function isAnyCommon(): bool
|
||||
{
|
||||
$select = new Select(Container::$dbConnection, Transaction::getTable());
|
||||
$select->where('payee_user_id', '=', null);
|
||||
|
||||
return $select->count() > 0;
|
||||
}
|
||||
|
||||
public function isAnyForCurrency(Currency $currency): bool
|
||||
{
|
||||
$select = new Select(Container::$dbConnection, Transaction::getTable());
|
||||
$select->where('currency_id', '=', $currency->getId());
|
||||
|
||||
return $select->count() > 0;
|
||||
}
|
||||
|
||||
public function getPagedByCommunity(Community $community, int $page, int $itemsPerPage, bool $useRelations = false, array $withRelations = []): Generator
|
||||
{
|
||||
$select = $this->selectAllByCommunity($community);
|
||||
$select->orderBy('time', 'DESC');
|
||||
$select->paginate($page, $itemsPerPage);
|
||||
|
||||
yield from Container::$persistentDataManager->selectMultipleFromDb($select, Transaction::class, $useRelations, $withRelations);
|
||||
}
|
||||
|
||||
public function getPagedByEvent(Event $event, int $page, int $itemsPerPage, bool $useRelations = false, array $withRelations = []): Generator
|
||||
{
|
||||
$select = $this->selectAllByEvent($event);
|
||||
$select->orderBy('time', 'DESC');
|
||||
$select->paginate($page, $itemsPerPage);
|
||||
|
||||
yield from Container::$persistentDataManager->selectMultipleFromDb($select, Transaction::class, $useRelations, $withRelations);
|
||||
}
|
||||
|
||||
private function selectAllByCommunity(Community $community)
|
||||
{
|
||||
$select = new Select(Container::$dbConnection, Transaction::getTable());
|
||||
$select->where('community_id', '=', $community->getId());
|
||||
return $select;
|
||||
}
|
||||
|
||||
private function selectAllByEvent(Event $event)
|
||||
{
|
||||
$select = new Select(Container::$dbConnection, Transaction::getTable());
|
||||
$select->where('event_id', '=', $event->getId());
|
||||
return $select;
|
||||
}
|
||||
}
|
@ -5,20 +5,12 @@ use Generator;
|
||||
use SokoWeb\Database\Query\Select;
|
||||
use RVR\PersistentData\Model\User;
|
||||
use RVR\PersistentData\Model\UserPasswordResetter;
|
||||
use SokoWeb\PersistentData\PersistentDataManager;
|
||||
|
||||
class UserPasswordResetterRepository
|
||||
{
|
||||
private PersistentDataManager $pdm;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->pdm = new PersistentDataManager();
|
||||
}
|
||||
|
||||
public function getById(int $userConfirmationId): ?UserPasswordResetter
|
||||
{
|
||||
return $this->pdm->selectFromDbById($userConfirmationId, UserPasswordResetter::class);
|
||||
return \Container::$persistentDataManager->selectFromDbById($userConfirmationId, UserPasswordResetter::class);
|
||||
}
|
||||
|
||||
public function getByToken(string $token): ?UserPasswordResetter
|
||||
@ -26,7 +18,7 @@ class UserPasswordResetterRepository
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('token', '=', $token);
|
||||
|
||||
return $this->pdm->selectFromDb($select, UserPasswordResetter::class);
|
||||
return \Container::$persistentDataManager->selectFromDb($select, UserPasswordResetter::class);
|
||||
}
|
||||
|
||||
public function getByUser(User $user): ?UserPasswordResetter
|
||||
@ -34,7 +26,7 @@ class UserPasswordResetterRepository
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('user_id', '=', $user->getId());
|
||||
|
||||
return $this->pdm->selectFromDb($select, UserPasswordResetter::class);
|
||||
return \Container::$persistentDataManager->selectFromDb($select, UserPasswordResetter::class);
|
||||
}
|
||||
|
||||
public function getAllExpired(): Generator
|
||||
@ -42,6 +34,6 @@ class UserPasswordResetterRepository
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('expires', '<', (new DateTime())->format('Y-m-d H:i:s'));
|
||||
|
||||
yield from $this->pdm->selectMultipleFromDb($select, UserPasswordResetter::class);
|
||||
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, UserPasswordResetter::class);
|
||||
}
|
||||
}
|
||||
|
@ -1,22 +1,15 @@
|
||||
<?php namespace RVR\Repository;
|
||||
|
||||
use Generator;
|
||||
use SokoWeb\Interfaces\Repository\IUserRepository;
|
||||
use SokoWeb\Database\Query\Select;
|
||||
use RVR\PersistentData\Model\User;
|
||||
use SokoWeb\PersistentData\PersistentDataManager;
|
||||
|
||||
class UserRepository implements IUserRepository
|
||||
{
|
||||
private PersistentDataManager $pdm;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->pdm = new PersistentDataManager();
|
||||
}
|
||||
|
||||
public function getById(int $userId): ?User
|
||||
{
|
||||
return $this->pdm->selectFromDbById($userId, User::class);
|
||||
return \Container::$persistentDataManager->selectFromDbById($userId, User::class);
|
||||
}
|
||||
|
||||
public function getByEmail(string $email): ?User
|
||||
@ -24,7 +17,24 @@ class UserRepository implements IUserRepository
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('email', '=', $email);
|
||||
|
||||
return $this->pdm->selectFromDb($select, User::class);
|
||||
return \Container::$persistentDataManager->selectFromDb($select, User::class);
|
||||
}
|
||||
|
||||
public function getByUsername(string $username): ?User
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('username', '=', $username);
|
||||
|
||||
return \Container::$persistentDataManager->selectFromDb($select, User::class);
|
||||
}
|
||||
|
||||
public function getByEmailOrUsername(string $emailOrUsername): ?User
|
||||
{
|
||||
if (filter_var($emailOrUsername, FILTER_VALIDATE_EMAIL)) {
|
||||
return $this->getByEmail($emailOrUsername);
|
||||
}
|
||||
|
||||
return $this->getByUsername($emailOrUsername);
|
||||
}
|
||||
|
||||
public function getByGoogleSub(string $sub): ?User
|
||||
@ -32,6 +42,16 @@ class UserRepository implements IUserRepository
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('google_sub', '=', $sub);
|
||||
|
||||
return $this->pdm->selectFromDb($select, User::class);
|
||||
return \Container::$persistentDataManager->selectFromDb($select, User::class);
|
||||
}
|
||||
|
||||
public function searchByName(string $name): Generator
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('full_name', 'LIKE', '%' . $name . '%');
|
||||
$select->orWhere('nickname', 'LIKE', '%' . $name . '%');
|
||||
$select->limit(10);
|
||||
|
||||
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, User::class);
|
||||
}
|
||||
}
|
||||
|
0
tests/.gitkeep
Normal file
0
tests/.gitkeep
Normal file
@ -4,33 +4,64 @@
|
||||
|
||||
@section(main)
|
||||
<h2>Account</h2>
|
||||
<div class="box">
|
||||
<form id="accountForm" action="/account" method="post" data-observe-inputs="password_new,password_new_confirm">
|
||||
<div class="box compactBox">
|
||||
<form id="accountForm" action="<?= Container::$routeCollection->getRoute('account-action')->generateLink() ?>" method="post" data-reload-on-success="true" data-observe-inputs="email,username,password_new,password_new_confirm,nickname,phone,id_number">
|
||||
<?php if ($user['password'] !== null && $user['google_sub'] !== null): ?>
|
||||
<p class="justify small">Please confirm your identity with your password or with Google to modify your account.</p>
|
||||
<div class="inputWithButton">
|
||||
<input type="password" class="text name="password" placeholder="Current password" required minlength="6" autofocus><!--
|
||||
--><button id="authenticateWithGoogleButton" class="yellow" type="button">Google</button>
|
||||
<input type="password" class="text" name="password" autocomplete="current-password" required minlength="6" autofocus><!--
|
||||
--><button id="authenticateWithGoogleButton" class="yellow" type="button"><i class="fa-brands fa-google"></i></button>
|
||||
</div>
|
||||
<?php elseif ($user['password'] !== null): ?>
|
||||
<p class="justify small">Please confirm your identity with your password to modify your account.</p>
|
||||
<input type="password" class="text big fullWidth" name="password" placeholder="Current password" required minlength="6" autofocus>
|
||||
<input type="password" class="text big fullWidth" name="password" autocomplete="current-password" required minlength="6" autofocus>
|
||||
<?php elseif ($user['google_sub'] !== null): ?>
|
||||
<p class="justify small">Please confirm your identity with Google to modify your account.</p>
|
||||
<div class="inputWithButton">
|
||||
<input type="text" class="text" name="password" placeholder="Authenticate with Google..." disabled><!--
|
||||
--><button id="authenticateWithGoogleButton" class="yellow" type="button">Google</button>
|
||||
--><button id="authenticateWithGoogleButton" class="yellow" type="button"><i class="fa-brands fa-google"></i></button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<hr>
|
||||
<?php /* TODO: disabled for the time being, email modification should be implemented */ ?>
|
||||
<input type="email" class="text big fullWidth" name="email" placeholder="Email address" value="<?= $user['email'] ?>" disabled>
|
||||
<input type="password" class="text big fullWidth marginTop" name="password_new" placeholder="New password" minlength="6">
|
||||
<input type="password" class="text big fullWidth marginTop" name="password_new_confirm" placeholder="New password confirmation" minlength="6">
|
||||
<p class="formLabel">Email address</p>
|
||||
<input type="email" class="text big fullWidth" name="email" autocomplete="username" value="<?= $user['email'] ?>">
|
||||
<p class="formLabel marginTop">Username</p>
|
||||
<input type="text" class="text big fullWidth" name="username" value="<?= $user['username'] ?>">
|
||||
<p class="formLabel marginTop">New password</p>
|
||||
<input type="password" class="text big fullWidth" name="password_new" autocomplete="new-password" minlength="6">
|
||||
<p class="formLabel marginTop">New password confirmation</p>
|
||||
<input type="password" class="text big fullWidth" name="password_new_confirm" autocomplete="new-password" minlength="6">
|
||||
<hr>
|
||||
<p class="formLabel marginTop">Full name</p>
|
||||
<input type="text" class="text big fullWidth" name="full_name" value="<?= $user['full_name'] ?>" disabled>
|
||||
<p class="formLabel marginTop marginTop">Nickname</p>
|
||||
<input type="text" class="text big fullWidth" name="nickname" value="<?= $user['nickname'] ?>">
|
||||
<p class="formLabel marginTop marginTop">Phone</p>
|
||||
<input type="text" class="text big fullWidth" name="phone" value="<?= $user['phone'] ?>">
|
||||
<p class="formLabel marginTop marginTop">ID number</p>
|
||||
<input type="text" class="text big fullWidth" name="id_number" value="<?= $user['id_number'] ?>">
|
||||
<p id="accountFormError" class="formError justify marginTop"></p>
|
||||
<div class="right marginTop">
|
||||
<button type="submit" name="submit" disabled>Save</button>
|
||||
<button type="submit" name="submit_button" disabled><i class="fa-regular fa-floppy-disk"></i> Save</button>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="center">
|
||||
<?php if ($user['google_sub'] === null): ?>
|
||||
<a class="button yellow" href="<?= Container::$routeCollection->getRoute('account.googleConnect')->generateLink() ?>" title="Connect with Google"><i class="fa-solid fa-link"></i> Connect with Google</a>
|
||||
<?php else: ?>
|
||||
<?php if ($user['password'] !== null): ?>
|
||||
<a class="button yellow" href="<?= Container::$routeCollection->getRoute('account.googleDisconnect')->generateLink() ?>" title="Disconnect from Google"><i class="fa-solid fa-link-slash"></i> Disconnect from Google</a>
|
||||
<?php else: ?>
|
||||
<p class="bold small">Your account does not have a password. Please set a password if you want to disconnect your account from Google.</p>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section(pageScript)
|
||||
<script>
|
||||
var googleAuthenticateUrl = '<?= Container::$routeCollection->getRoute('account.googleAuthenticate')->generateLink() ?>';
|
||||
</script>
|
||||
@endsection
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user