Compare commits

...

39 Commits

Author SHA1 Message Date
6e1ee839ba
Merge pull request 'modernize-oauth' (!76) from modernize-oauth into master
All checks were successful
rvr-nextgen/pipeline/tag This commit looks good
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #76
2024-11-26 21:32:41 +01:00
cac57d7f71
decease session expiration time
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2024-11-26 21:11:27 +01:00
cde14ee779
modernize oauth token handling 2024-11-26 21:11:27 +01:00
dfcdd8dca7
update yarn.lock 2024-11-26 21:07:04 +01:00
a5286bf62f
add .well-known/openid-configuration to the root as well 2024-11-26 21:07:04 +01:00
ec4d3806ce
Merge pull request 'do not recreate docker runner group and user' (!75) from do-no-recreate-group-and-user into master
All checks were successful
rvr-nextgen/pipeline/tag This commit looks good
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #75
2024-03-11 00:15:07 +01:00
0c2334502d
do not recreate docker runner group and user
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2024-03-10 23:53:11 +01:00
6341072b0b
Merge pull request 'fix regex for tagging' (!74) from bugfix/fix-regex into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
rvr-nextgen/pipeline/tag This commit looks good
Reviewed-on: #74
2023-10-01 00:15:15 +02:00
1dbd813bef
fix regex for tagging
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-10-01 00:13:27 +02:00
14d83d24b4
Merge pull request 'push fixed version images' (!73) from feature/auto-release-fixed-tags into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #73
2023-10-01 00:07:52 +02:00
ba02b6d8cb
push fixed version images
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-10-01 00:06:38 +02:00
9b8fcaad9a
Merge pull request 'error reporting should always be E_ALL' (!72) from bugfix/fix-error-reporting into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #72
2023-09-30 23:37:24 +02:00
6bd6ede442
error reporting should always be E_ALL
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-09-30 23:18:42 +02:00
65dac4640a
Merge pull request 'define forwarded_scheme before use' (!71) from bugfix/pass-scheme-to-fastcgi into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
rvr-nextgen/pipeline/tag This commit looks good
Reviewed-on: #71
2023-09-28 15:00:21 +02:00
7bd12050f6
define forwarded_scheme before use
Some checks are pending
rvr-nextgen/pipeline/pr-master Build queued...
2023-09-28 14:59:55 +02:00
5041258de0
Merge pull request 'pass scheme to fastcgi' (!70) from bugfix/pass-scheme-to-fastcgi into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
rvr-nextgen/pipeline/tag This commit looks good
Reviewed-on: #70
2023-09-28 14:55:37 +02:00
47928d2d2b
pass scheme to fastcgi
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-09-28 14:43:30 +02:00
55cf2afde3
Merge pull request 'feature/update-to-ubuntu-2204' (!69) from feature/update-to-ubuntu-2204 into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #69
2023-09-28 14:42:43 +02:00
ed2b1c23ae
update soko-web
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-09-28 14:30:59 +02:00
6ef45b8b6a
generate composer.lock 2023-09-28 14:30:58 +02:00
b4c359e81d
update phpunit 2023-09-28 14:30:58 +02:00
495a2fe910
update ubuntu to 22.04 and php to 8.1 2023-09-28 14:30:58 +02:00
37ba0ec172
Merge pull request 'feature/create-working-docker-images' (!68) from feature/create-working-docker-images into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
rvr-nextgen/pipeline/tag This commit looks good
Reviewed-on: #68
2023-09-28 13:33:02 +02:00
9ffde6bccb
update readme
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-09-28 13:31:26 +02:00
0ce1c4f28a
update soko-web 2023-09-28 13:21:54 +02:00
0d1e4a3d1c
update docker-compose 2023-09-28 13:21:54 +02:00
cbf62d1c4a
add docker release stage to pipeline 2023-09-28 13:21:54 +02:00
5eeac18b4c
use the new dockerfile in pipeline 2023-09-28 13:21:54 +02:00
c4dce94f5e
delete deprecated dockerfiles 2023-09-28 13:21:54 +02:00
147b7690ac
add new dockerfile with dev and release stages 2023-09-28 13:21:54 +02:00
bdfa46b838
add entry point for dev docker 2023-09-28 13:21:54 +02:00
27fe883f59
add entry point for release docker 2023-09-28 13:21:54 +02:00
1718dfca9e
install base database in MigrateDatabaseCommand 2023-09-28 13:21:54 +02:00
6870d08c21
remove deprecated scripts 2023-09-28 13:21:54 +02:00
eba674bfbc
add cron for db:maintain 2023-09-28 13:21:54 +02:00
832170b1e1
add release generator script 2023-09-28 13:21:54 +02:00
8dfea0993b
add nodejs installer script 2023-09-28 13:21:54 +02:00
6146641eed
Merge pull request 'fix selectUpcomingAndRecent when used with other selects' (!67) from bugfix/upcoming-events-are-multiplicated into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #67
2023-08-06 22:09:33 +02:00
7cb406cc49
fix selectUpcomingAndRecent when used with other selects
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-08-06 22:06:42 +02:00
33 changed files with 1351 additions and 1160 deletions

View File

@ -21,3 +21,4 @@ RECAPTCHA_SITEKEY=your_recaptcha_sitekey
RECAPTCHA_SECRET=your_recaptcha_secret RECAPTCHA_SECRET=your_recaptcha_secret
JWT_RSA_PRIVATE_KEY=jwt-rsa256-private.pem JWT_RSA_PRIVATE_KEY=jwt-rsa256-private.pem
JWT_RSA_PUBLIC_KEY=jwt-rsa256-public.pem JWT_RSA_PUBLIC_KEY=jwt-rsa256-public.pem
JWT_KEY_KID=1

61
Jenkinsfile vendored
View File

@ -13,8 +13,9 @@ pipeline {
} }
agent { agent {
dockerfile { dockerfile {
filename 'docker/Dockerfile-test' filename 'docker/Dockerfile'
dir '.' dir '.'
additionalBuildArgs '--target rvr_base'
reuseNode true reuseNode true
} }
} }
@ -26,8 +27,9 @@ pipeline {
stage('Unit Testing') { stage('Unit Testing') {
agent { agent {
dockerfile { dockerfile {
filename 'docker/Dockerfile-test' filename 'docker/Dockerfile'
dir '.' dir '.'
additionalBuildArgs '--target rvr_base'
reuseNode true reuseNode true
} }
} }
@ -44,8 +46,9 @@ pipeline {
stage('Static Code Analysis') { stage('Static Code Analysis') {
agent { agent {
dockerfile { dockerfile {
filename 'docker/Dockerfile-test' filename 'docker/Dockerfile'
dir '.' dir '.'
additionalBuildArgs '--target rvr_base'
reuseNode true reuseNode true
} }
} }
@ -58,5 +61,57 @@ pipeline {
} }
} }
} }
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
View File

@ -2,81 +2,81 @@
[![Build Status](https://ci.esoko.eu/job/rvr-nextgen/job/master/badge/icon)](https://ci.esoko.eu/job/rvr-nextgen/job/master/) [![Build Status](https://ci.esoko.eu/job/rvr-nextgen/job/master/badge/icon)](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 ## 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. Execute the following command:
```bash
### 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:
```
docker compose up -d docker compose up -d
``` ```
Attach shell to the container of `rvr-nextgen-app`:
``` **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:
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:
``` ```
./rvr user:add EMAIL PASSWORD admin ./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.
--- ---

View File

@ -10,11 +10,11 @@
} }
], ],
"require": { "require": {
"esoko/soko-web": "0.13.1", "esoko/soko-web": "0.15",
"firebase/php-jwt": "^6.4" "firebase/php-jwt": "^6.4"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^9.6", "phpunit/phpunit": "^10.3",
"phpstan/phpstan": "^1.10" "phpstan/phpstan": "^1.10"
}, },
"autoload": { "autoload": {

964
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -2,12 +2,17 @@ version: '3'
services: services:
app: app:
build: build:
context: ./docker context: .
dockerfile: Dockerfile-app dockerfile: docker/Dockerfile
target: rvr_dev
depends_on:
mariadb:
condition: service_healthy
ports: ports:
- 80:80 - 80:80
volumes: volumes:
- .:/var/www/rvr - .:/var/www/rvr
working_dir: /var/www/rvr
mariadb: mariadb:
image: mariadb:10.3 image: mariadb:10.3
ports: ports:
@ -19,6 +24,13 @@ services:
MYSQL_DATABASE: 'rvr' MYSQL_DATABASE: 'rvr'
MYSQL_USER: 'rvr' MYSQL_USER: 'rvr'
MYSQL_PASSWORD: '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: adminer:
image: adminer:4.8.1-standalone image: adminer:4.8.1-standalone
ports: ports:

44
docker/Dockerfile Normal file
View 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

View File

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

View File

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

View File

@ -1,3 +1,9 @@
map $http_x_forwarded_proto $forwarded_scheme {
default $scheme;
http http;
https https;
}
server { server {
listen 80 default_server; listen 80 default_server;
listen [::]:80 default_server; listen [::]:80 default_server;
@ -14,7 +20,8 @@ server {
location ~ \.php$ { location ~ \.php$ {
include snippets/fastcgi-php.conf; 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 { location ~ /\.ht {

1
docker/scripts/cron Normal file
View File

@ -0,0 +1 @@
0 * * * * /var/www/rvr/rvr db:maintain

View 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
View 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 $?

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

View File

@ -2,6 +2,23 @@
# yarn lockfile v1 # 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: leaflet.markercluster@^1.4.1:
version "1.4.1" version "1.4.1"
resolved "https://registry.yarnpkg.com/leaflet.markercluster/-/leaflet.markercluster-1.4.1.tgz#b53f2c4f2ca7306ddab1dbb6f1861d5e8aa6c5e5" 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" version "1.6.0"
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.6.0.tgz#aecbb044b949ec29469eeb31c77a88e2f448f308" resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.6.0.tgz#aecbb044b949ec29469eeb31c77a88e2f448f308"
integrity sha512-CPkhyqWUKZKFJ6K8umN5/D2wrJ2+/8UIpXppY7QDnUZW5bZL5+SEI2J7GBpwh4LIupOKqbNSQXgqmrEJopHVNQ== 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"

View File

@ -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("----------------------------------------------")

View File

@ -1,32 +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 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

View File

@ -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 {} \;

View File

@ -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}';/" 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

View File

@ -1,26 +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 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

View File

@ -5,6 +5,8 @@ use SokoWeb\Database\Query\Modify;
use SokoWeb\Database\Query\Select; use SokoWeb\Database\Query\Select;
use SokoWeb\Interfaces\Database\IResultSet; use SokoWeb\Interfaces\Database\IResultSet;
use RVR\Repository\UserPasswordResetterRepository; use RVR\Repository\UserPasswordResetterRepository;
use RVR\Repository\OAuthTokenRepository;
use RVR\Repository\OAuthSessionRepository;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
@ -13,11 +15,17 @@ class MaintainDatabaseCommand extends Command
{ {
private UserPasswordResetterRepository $userPasswordResetterRepository; private UserPasswordResetterRepository $userPasswordResetterRepository;
private OAuthTokenRepository $oauthTokenRepository;
private OAuthSessionRepository $oauthSessionRepository;
public function __construct() public function __construct()
{ {
parent::__construct(); parent::__construct();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository(); $this->userPasswordResetterRepository = new UserPasswordResetterRepository();
$this->oauthTokenRepository = new OAuthTokenRepository();
$this->oauthSessionRepository = new OAuthSessionRepository();
} }
public function configure(): void public function configure(): void
@ -31,6 +39,8 @@ class MaintainDatabaseCommand extends Command
try { try {
$this->deleteExpiredPasswordResetters(); $this->deleteExpiredPasswordResetters();
$this->deleteExpiredSessions(); $this->deleteExpiredSessions();
$this->deleteExpiredOauthTokens();
$this->deleteExpiredOauthSessions();
} catch (\Exception $e) { } catch (\Exception $e) {
$output->writeln('<error>Maintenance failed!</error>'); $output->writeln('<error>Maintenance failed!</error>');
$output->writeln(''); $output->writeln('');
@ -59,7 +69,7 @@ class MaintainDatabaseCommand extends Command
//TODO: model may be used for sessions too //TODO: model may be used for sessions too
$select = new Select(\Container::$dbConnection, 'sessions'); $select = new Select(\Container::$dbConnection, 'sessions');
$select->columns(['id']); $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(); $result = $select->execute();
@ -69,4 +79,21 @@ class MaintainDatabaseCommand extends Command
$modify->delete(); $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);
}
}
} }

View File

@ -19,6 +19,8 @@ class MigrateDatabaseCommand extends Command
{ {
$db = \Container::$dbConnection; $db = \Container::$dbConnection;
$this->createBaseDb();
$db->startTransaction(); $db->startTransaction();
$success = []; $success = [];
@ -62,10 +64,8 @@ class MigrateDatabaseCommand extends Command
return 0; return 0;
} }
private function readDir(string $type): array private function createBaseDb()
{ {
$done = [];
$migrationTableExists = \Container::$dbConnection->query('SELECT count(*) $migrationTableExists = \Container::$dbConnection->query('SELECT count(*)
FROM information_schema.tables FROM information_schema.tables
WHERE table_schema = \'' . $_ENV['DB_NAME'] . '\' WHERE table_schema = \'' . $_ENV['DB_NAME'] . '\'
@ -73,16 +73,25 @@ class MigrateDatabaseCommand extends Command
->fetch(IResultSet::FETCH_NUM)[0]; ->fetch(IResultSet::FETCH_NUM)[0];
if ($migrationTableExists != 0) { if ($migrationTableExists != 0) {
$select = new Select(\Container::$dbConnection, 'migrations'); return;
$select->columns(['migration']); }
$select->where('type', '=', $type);
$select->orderBy('migration');
$result = $select->execute(); \Container::$dbConnection->multiQuery(file_get_contents(ROOT . '/database/rvr.sql'));
}
while ($migration = $result->fetch(IResultSet::FETCH_ASSOC)) { private function readDir(string $type): array
$done[] = $migration['migration']; {
} $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; $path = ROOT . '/database/migrations/' . $type;

View File

@ -1,79 +0,0 @@
<?php namespace RVR\Controller;
use DateTime;
use RVR\PersistentData\Model\OAuthToken;
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 OAuthAuthController implements IAuthenticationRequired
{
private OAuthClientRepository $oAuthClientRepository;
public function __construct()
{
$this->oAuthClientRepository = new OAuthClientRepository();
}
public function isAuthenticationRequired(): bool
{
return true;
}
public function auth()
{
$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'): '';
if (!$clientId || !$redirectUri || !$state) {
return new HtmlContent('oauth/oauth_error', ['error' => 'An invalid request was made. Please start authentication again.']);
}
$client = $this->oAuthClientRepository->getByClientId($clientId);
if ($client === null) {
return new HtmlContent('oauth/oauth_error', ['error' => 'Client is not authorized.']);
}
$redirectUriParsed = parse_url($redirectUri);
$redirectUriBase = $redirectUriParsed['scheme'] . '://' . $redirectUriParsed['host'] . $redirectUriParsed['path'];
$redirectUriQuery = [];
if (isset($redirectUriParsed['query'])) {
parse_str($redirectUriParsed['query'], $redirectUriQuery);
}
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));
$accessToken = bin2hex(random_bytes(16));
$token = new OAuthToken();
$token->setNonce($nonce);
$token->setScope($scope);
$token->setUser($user);
$token->setCode($code);
$token->setAccessToken($accessToken);
$token->setCreatedDate(new DateTime());
$token->setExpiresDate(new DateTime('+5 minutes'));
\Container::$persistentDataManager->saveToDb($token);
$redirectUriQuery = array_merge($redirectUriQuery, [
'state' => $state,
'code' => $code
]);
$finalRedirectUri = $redirectUriBase . '?' . http_build_query($redirectUriQuery);
return new Redirect($finalRedirectUri, IRedirect::TEMPORARY);
}
}

View File

@ -2,9 +2,16 @@
use DateTime; use DateTime;
use Firebase\JWT\JWT; 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\OAuthTokenRepository;
use RVR\Repository\UserRepository; use RVR\Repository\UserRepository;
use RVR\PersistentData\Model\User; use RVR\PersistentData\Model\User;
use RVR\PersistentData\Model\OAuthSession;
use RVR\PersistentData\Model\OAuthToken;
use RVR\Repository\OAuthClientRepository; use RVR\Repository\OAuthClientRepository;
use SokoWeb\Interfaces\Response\IContent; use SokoWeb\Interfaces\Response\IContent;
use SokoWeb\Response\JsonContent; use SokoWeb\Response\JsonContent;
@ -13,6 +20,8 @@ class OAuthController
{ {
private OAuthClientRepository $oAuthClientRepository; private OAuthClientRepository $oAuthClientRepository;
private OAuthSessionRepository $oAuthSessionRepository;
private OAuthTokenRepository $oAuthTokenRepository; private OAuthTokenRepository $oAuthTokenRepository;
private UserRepository $userRepository; private UserRepository $userRepository;
@ -20,60 +29,181 @@ class OAuthController
public function __construct() public function __construct()
{ {
$this->oAuthClientRepository = new OAuthClientRepository(); $this->oAuthClientRepository = new OAuthClientRepository();
$this->oAuthSessionRepository = new OAuthSessionRepository();
$this->oAuthTokenRepository = new OAuthTokenRepository(); $this->oAuthTokenRepository = new OAuthTokenRepository();
$this->userRepository = new UserRepository(); $this->userRepository = new UserRepository();
} }
public function getToken(): ?IContent public function generateToken(): ?IContent
{ {
$clientId = \Container::$request->post('client_id'); $credentials = $this->getClientCredentials();
$clientSecret = \Container::$request->post('client_secret');
$code = \Container::$request->post('code'); $code = \Container::$request->post('code');
$redirectUri = \Container::$request->post('redirect_uri');
if (!$clientId || !$clientSecret || !$code) { if (!$credentials['clientId'] || !$code || !$redirectUri) {
return new JsonContent([ return new JsonContent([
'error' => 'An invalid request was made.' 'error' => 'An invalid request was made.'
]); ]);
} }
$client = $this->oAuthClientRepository->getByClientId($clientId); $client = $this->oAuthClientRepository->getByClientId($credentials['clientId']);
if ($client === null || $client->getClientSecret() !== $clientSecret) { if ($client === null) {
return new JsonContent([ return new JsonContent([
'error' => 'Client is not authorized.' 'error' => 'Client is not found.'
]); ]);
} }
$token = $this->oAuthTokenRepository->getByCode($code); $redirectUriBase = explode('?', $redirectUri)[0];
if ($token === null || $token->getExpiresDate() < new DateTime()) { 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([ return new JsonContent([
'error' => 'The provided code is invalid.' 'error' => 'The provided code is invalid.'
]); ]);
} }
$payload = array_merge([ $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'], 'iss' => $_ENV['APP_URL'],
'iat' => (int)$token->getCreatedDate()->getTimestamp(), 'iat' => $token->getCreatedDate()->getTimestamp(),
'nbf' => (int)$token->getCreatedDate()->getTimestamp(), 'nbf' => $session->getCreatedDate()->getTimestamp(),
'exp' => (int)$token->getExpiresDate()->getTimestamp(), 'exp' => $token->getExpiresDate()->getTimestamp(),
'aud' => $clientId, 'aud' => $session->getClientId(),
'nonce' => $token->getNonce() 'nonce' => $session->getNonce()
], $this->getUserInfoInternal( ];
$this->userRepository->getById($token->getUserId()), $idTokenPayload = array_merge($commonPayload, $this->getUserInfoInternal(
$token->getScopeArray()) $this->userRepository->getById($session->getUserId()),
$session->getScopeArray())
); );
$accessTokenPayload = array_merge($commonPayload, [
'jti' => $token->getId(),
]);
$privateKey = file_get_contents(ROOT . '/' . $_ENV['JWT_RSA_PRIVATE_KEY']); $privateKey = file_get_contents(ROOT . '/' . $_ENV['JWT_RSA_PRIVATE_KEY']);
$jwt = JWT::encode($payload, $privateKey, 'RS256'); $idToken = JWT::encode($idTokenPayload, $privateKey, 'RS256', $_ENV['JWT_KEY_KID']);
$accessToken = JWT::encode($accessTokenPayload, $privateKey, 'RS256', $_ENV['JWT_KEY_KID']);
return new JsonContent([ return new JsonContent([
'access_token' => $token->getAccessToken(), 'access_token' => $accessToken,
'expires_in' => $token->getExpiresDate()->getTimestamp() - (new DateTime())->getTimestamp(), 'expires_in' => $token->getExpiresDate()->getTimestamp() - (new DateTime())->getTimestamp(),
'scope' => $token->getScope(), 'scope' => $session->getScope(),
'id_token' => $jwt, 'id_token' => $idToken,
'token_type' => 'Bearer' '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 public function getUserInfo() : IContent
{ {
$authorization = \Container::$request->header('Authorization'); $authorization = \Container::$request->header('Authorization');
@ -84,9 +214,8 @@ class OAuthController
} }
$accessToken = substr($authorization, strlen('Bearer ')); $accessToken = substr($authorization, strlen('Bearer '));
$token = $this->oAuthTokenRepository->getByAccessToken($accessToken); $tokenValidated = $this->validateTokenAndSession($accessToken, $token, $session);
if (!$tokenValidated) {
if ($token === null || $token->getExpiresDate() < new DateTime()) {
return new JsonContent([ return new JsonContent([
'error' => 'The provided access token is invalid.' 'error' => 'The provided access token is invalid.'
]); ]);
@ -94,8 +223,8 @@ class OAuthController
return new JsonContent( return new JsonContent(
$this->getUserInfoInternal( $this->getUserInfoInternal(
$this->userRepository->getById($token->getUserId()), $this->userRepository->getById($session->getUserId()),
$token->getScopeArray() $session->getScopeArray()
) )
); );
} }
@ -106,7 +235,10 @@ class OAuthController
'issuer' => $_ENV['APP_URL'], 'issuer' => $_ENV['APP_URL'],
'authorization_endpoint' => \Container::$request->getBase() . \Container::$routeCollection->getRoute('oauth.auth')->generateLink(), 'authorization_endpoint' => \Container::$request->getBase() . \Container::$routeCollection->getRoute('oauth.auth')->generateLink(),
'token_endpoint' => \Container::$request->getBase() . \Container::$routeCollection->getRoute('oauth.token')->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(), '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(), 'jwks_uri' => \Container::$request->getBase() . \Container::$routeCollection->getRoute('oauth.certs')->generateLink(),
'response_types_supported' => 'response_types_supported' =>
[ [
@ -128,6 +260,7 @@ class OAuthController
], ],
'token_endpoint_auth_methods_supported' => 'token_endpoint_auth_methods_supported' =>
[ [
'client_secret_basic',
'client_secret_post', 'client_secret_post',
], ],
'claims_supported' => 'claims_supported' =>
@ -167,13 +300,66 @@ class OAuthController
'kty' => 'RSA', 'kty' => 'RSA',
'alg' => 'RS256', 'alg' => 'RS256',
'use' => 'sig', 'use' => 'sig',
'kid' => '1', 'kid' => $_ENV['JWT_KEY_KID'],
'n' => str_replace(['+', '/'], ['-', '_'], base64_encode($keyInfo['rsa']['n'])), 'n' => str_replace(['+', '/'], ['-', '_'], base64_encode($keyInfo['rsa']['n'])),
'e' => str_replace(['+', '/'], ['-', '_'], base64_encode($keyInfo['rsa']['e'])), '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 private function getUserInfoInternal(User $user, array $scope): array
{ {
$userInfo = []; $userInfo = [];

View 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);
}
}

View 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;
}
}

View File

@ -7,61 +7,26 @@ class OAuthToken extends Model
{ {
protected static string $table = 'oauth_tokens'; protected static string $table = 'oauth_tokens';
protected static array $fields = ['scope', 'nonce', 'user_id', 'code', 'access_token', 'created', 'expires']; protected static array $fields = ['session_id', 'created', 'expires'];
protected static array $relations = ['user' => User::class]; protected static array $relations = ['session' => OAuthSession::class];
private static array $possibleScopeValues = ['openid', 'email', 'profile']; private ?OAuthSession $session = null;
private array $scope = []; private ?int $sessionId = null;
private string $nonce = '';
private ?User $user = null;
private ?int $userId = null;
private string $code = '';
private string $accessToken = '';
private DateTime $created; private DateTime $created;
private DateTime $expires; private DateTime $expires;
public function setScopeArray(array $scope): void public function setSession(OAuthSession $session): void
{ {
$this->scope = array_intersect($scope, self::$possibleScopeValues); $this->session = $session;
} }
public function setScope(string $scope): void public function setSessionId(int $sessionId): void
{ {
$this->setScopeArray(explode(' ', $scope)); $this->sessionId = $sessionId;
}
public function setNonce(string $nonce): void
{
$this->nonce = $nonce;
}
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 setAccessToken(string $accessToken): void
{
$this->accessToken = $accessToken;
} }
public function setCreatedDate(DateTime $created): void public function setCreatedDate(DateTime $created): void
@ -84,39 +49,14 @@ class OAuthToken extends Model
$this->expires = new DateTime($expires); $this->expires = new DateTime($expires);
} }
public function getScope(): string public function getSession(): ?OAuthSession
{ {
return implode(' ', $this->scope); return $this->session;
} }
public function getScopeArray(): array public function getSessionId(): ?int
{ {
return $this->scope; return $this->sessionId;
}
public function getNonce(): string
{
return $this->nonce;
}
public function getUser(): ?User
{
return $this->user;
}
public function getUserId(): ?int
{
return $this->userId;
}
public function getCode(): string
{
return $this->code;
}
public function getAccessToken(): string
{
return $this->accessToken;
} }
public function getCreatedDate(): DateTime public function getCreatedDate(): DateTime

View File

@ -23,19 +23,24 @@ class EventRepository
public function getAllByCommunity(Community $community, bool $useRelations = false, array $withRelations = []): Generator public function getAllByCommunity(Community $community, bool $useRelations = false, array $withRelations = []): Generator
{ {
$select = $this->selectAllByCommunity($community); $select = new Select(Container::$dbConnection, Event::getTable());
$this->selectAllByCommunity($select, $community);
yield from Container::$persistentDataManager->selectMultipleFromDb($select, Event::class, $useRelations, $withRelations); yield from Container::$persistentDataManager->selectMultipleFromDb($select, Event::class, $useRelations, $withRelations);
} }
public function countAllByCommunity(Community $community): int public function countAllByCommunity(Community $community): int
{ {
return $this->selectAllByCommunity($community)->count(); $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 public function getUpcomingAndRecentByCommunity(Community $community, DateTime $from, int $days, int $limit, bool $useRelations = false, array $withRelations = []): Generator
{ {
$select = $this->selectAllByCommunity($community); $select = new Select(Container::$dbConnection, Event::getTable());
$this->selectAllByCommunity($select, $community);
$this->selectUpcomingAndRecent($select, $from, $days); $this->selectUpcomingAndRecent($select, $from, $days);
$select->orderBy('start', 'DESC'); $select->orderBy('start', 'DESC');
$select->limit($limit, 0); $select->limit($limit, 0);
@ -45,7 +50,8 @@ class EventRepository
public function getUpcomingAndRecentByUser(User $user, DateTime $from, int $days, int $limit, bool $useRelations = false, array $withRelations = []): Generator public function getUpcomingAndRecentByUser(User $user, DateTime $from, int $days, int $limit, bool $useRelations = false, array $withRelations = []): Generator
{ {
$select = $this->selectAllByUser($user); $select = new Select(Container::$dbConnection, Event::getTable());
$this->selectAllByUser($select, $user);
$this->selectUpcomingAndRecent($select, $from, $days); $this->selectUpcomingAndRecent($select, $from, $days);
$select->orderBy('start', 'DESC'); $select->orderBy('start', 'DESC');
$select->limit($limit, 0); $select->limit($limit, 0);
@ -55,7 +61,8 @@ class EventRepository
public function getCurrentByUser(User $user, DateTime $from, int $days, bool $useRelations = false, array $withRelations = []): ?Event public function getCurrentByUser(User $user, DateTime $from, int $days, bool $useRelations = false, array $withRelations = []): ?Event
{ {
$select = $this->selectAllByUser($user); $select = new Select(Container::$dbConnection, Event::getTable());
$this->selectAllByUser($select, $user);
$this->selectUpcomingAndRecent($select, $from, $days); $this->selectUpcomingAndRecent($select, $from, $days);
$select->orderBy('start', 'DESC'); $select->orderBy('start', 'DESC');
$select->limit(1, 0); $select->limit(1, 0);
@ -65,7 +72,8 @@ class EventRepository
public function getPagedByCommunity(Community $community, int $page, int $itemsPerPage, bool $useRelations = false, array $withRelations = []): Generator public function getPagedByCommunity(Community $community, int $page, int $itemsPerPage, bool $useRelations = false, array $withRelations = []): Generator
{ {
$select = $this->selectAllByCommunity($community); $select = new Select(Container::$dbConnection, Event::getTable());
$this->selectAllByCommunity($select, $community);
$select->orderBy('start', 'DESC'); $select->orderBy('start', 'DESC');
$select->paginate($page, $itemsPerPage); $select->paginate($page, $itemsPerPage);
@ -74,7 +82,8 @@ class EventRepository
public function searchByTitle(Community $community, string $title): Generator public function searchByTitle(Community $community, string $title): Generator
{ {
$select = $this->selectAllByCommunity($community); $select = new Select(Container::$dbConnection, Event::getTable());
$this->selectAllByCommunity($select, $community);
$select->where('title', 'LIKE', '%' . $title . '%'); $select->where('title', 'LIKE', '%' . $title . '%');
$select->orderBy('start', 'DESC'); $select->orderBy('start', 'DESC');
$select->limit(10); $select->limit(10);
@ -82,31 +91,29 @@ class EventRepository
yield from Container::$persistentDataManager->selectMultipleFromDb($select, Event::class); yield from Container::$persistentDataManager->selectMultipleFromDb($select, Event::class);
} }
private function selectAllByCommunity(Community $community) private function selectAllByCommunity(Select $select, Community $community): void
{ {
$select = new Select(Container::$dbConnection, Event::getTable());
$select->where('community_id', '=', $community->getId()); $select->where('community_id', '=', $community->getId());
return $select;
} }
private function selectAllByUser(User $user) private function selectAllByUser(Select $select, User $user): void
{ {
$select = new Select(Container::$dbConnection, Event::getTable());
$select->innerJoin('communities', ['communities', 'id'], '=', ['events', 'community_id']); $select->innerJoin('communities', ['communities', 'id'], '=', ['events', 'community_id']);
$select->innerJoin('community_members', ['communities', 'id'], '=', ['community_members', 'community_id']); $select->innerJoin('community_members', ['communities', 'id'], '=', ['community_members', 'community_id']);
$select->where(['community_members', 'user_id'], '=', $user->getId()); $select->where(['community_members', 'user_id'], '=', $user->getId());
return $select;
} }
private function selectUpcomingAndRecent(Select $select, DateTime $from, int $days) 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(function (Select $select) use ($from, $days) {
$select->where('end', '>', $from->format('Y-m-d H:i:s')); $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->orWhere(function (Select $select) use ($from, $days) {
$select->where('start', '<', $from->format('Y-m-d H:i:s')); $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'));
});
}); });
} }
} }

View 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);
}
}

View File

@ -4,6 +4,7 @@ use DateTime;
use Generator; use Generator;
use SokoWeb\Database\Query\Select; use SokoWeb\Database\Query\Select;
use RVR\PersistentData\Model\OAuthToken; use RVR\PersistentData\Model\OAuthToken;
use RVR\PersistentData\Model\OAuthSession;
class OAuthTokenRepository class OAuthTokenRepository
{ {
@ -12,20 +13,16 @@ class OAuthTokenRepository
return \Container::$persistentDataManager->selectFromDbById($id, OAuthToken::class); return \Container::$persistentDataManager->selectFromDbById($id, OAuthToken::class);
} }
public function getByCode(string $code): ?OAuthToken public function getAllBySession(OAuthSession $session, bool $useRelations = false, array $withRelations = []): Generator
{ {
$select = new Select(\Container::$dbConnection); $select = $this->selectAllBySession($session);
$select->where('code', '=', $code);
return \Container::$persistentDataManager->selectFromDb($select, OAuthToken::class); yield from \Container::$persistentDataManager->selectMultipleFromDb($select, OAuthToken::class, $useRelations, $withRelations);
} }
public function getByAccessToken(string $accessToken): ?OAuthToken public function countAllBySession(OAuthSession $session): int
{ {
$select = new Select(\Container::$dbConnection); return $this->selectAllBySession($session)->count();
$select->where('access_token', '=', $accessToken);
return \Container::$persistentDataManager->selectFromDb($select, OAuthToken::class);
} }
public function getAllExpired(): Generator public function getAllExpired(): Generator
@ -35,4 +32,11 @@ class OAuthTokenRepository
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, OAuthToken::class); 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;
}
} }

14
web.php
View File

@ -7,7 +7,7 @@ use SokoWeb\Request\Request;
use SokoWeb\Request\Session; use SokoWeb\Request\Session;
use RVR\Controller\HomeController; use RVR\Controller\HomeController;
use RVR\Controller\LoginController; use RVR\Controller\LoginController;
use RVR\Controller\OAuthAuthController; use RVR\Controller\OAuthSessionController;
use RVR\Controller\OAuthController; use RVR\Controller\OAuthController;
use RVR\Controller\UserController; use RVR\Controller\UserController;
use RVR\Controller\UserSearchController; use RVR\Controller\UserSearchController;
@ -19,9 +19,8 @@ use RVR\Repository\UserRepository;
require 'app.php'; require 'app.php';
error_reporting(E_ALL);
if (!empty($_ENV['DEV'])) { if (!empty($_ENV['DEV'])) {
error_reporting(E_ALL);
ini_set('display_errors', '1'); ini_set('display_errors', '1');
} else { } else {
ini_set('display_errors', '0'); ini_set('display_errors', '0');
@ -30,6 +29,7 @@ if (!empty($_ENV['DEV'])) {
Container::$routeCollection = new RouteCollection(); Container::$routeCollection = new RouteCollection();
Container::$routeCollection->get('home', '', [HomeController::class, 'getHome']); Container::$routeCollection->get('home', '', [HomeController::class, 'getHome']);
Container::$routeCollection->get('oauth-config-root', '.well-known/openid-configuration', [OAuthController::class, 'getConfig']);
Container::$routeCollection->group('login', function (RouteCollection $routeCollection) { Container::$routeCollection->group('login', function (RouteCollection $routeCollection) {
$routeCollection->get('login', '', [LoginController::class, 'getLoginForm']); $routeCollection->get('login', '', [LoginController::class, 'getLoginForm']);
$routeCollection->post('login-action', '', [LoginController::class, 'login']); $routeCollection->post('login-action', '', [LoginController::class, 'login']);
@ -37,8 +37,10 @@ Container::$routeCollection->group('login', function (RouteCollection $routeColl
$routeCollection->get('login.google-action', 'google/code', [LoginController::class, 'loginWithGoogle']); $routeCollection->get('login.google-action', 'google/code', [LoginController::class, 'loginWithGoogle']);
}); });
Container::$routeCollection->group('oauth', function (RouteCollection $routeCollection) { Container::$routeCollection->group('oauth', function (RouteCollection $routeCollection) {
$routeCollection->get('oauth.auth', 'auth', [OAuthAuthController::class, 'auth']); $routeCollection->get('oauth.auth', 'auth', [OAuthSessionController::class, 'auth']);
$routeCollection->post('oauth.token', 'token', [OAuthController::class, 'getToken']); $routeCollection->post('oauth.token', 'token', [OAuthController::class, 'generateToken']);
$routeCollection->post('oauth.token.introspect', 'token/introspect', [OAuthController::class, 'introspectToken']);
$routeCollection->post('oauth.token.revoke', 'token/revoke', [OAuthController::class, 'revokeToken']);
$routeCollection->get('oauth.userinfo', 'userinfo', [OAuthController::class, 'getUserInfo']); $routeCollection->get('oauth.userinfo', 'userinfo', [OAuthController::class, 'getUserInfo']);
$routeCollection->get('oauth.config', '.well-known/openid-configuration', [OAuthController::class, 'getConfig']); $routeCollection->get('oauth.config', '.well-known/openid-configuration', [OAuthController::class, 'getConfig']);
$routeCollection->get('oauth.certs', 'certs', [OAuthController::class, 'getCerts']); $routeCollection->get('oauth.certs', 'certs', [OAuthController::class, 'getCerts']);
@ -116,7 +118,7 @@ Container::$routeCollection->group('communities', function (RouteCollection $rou
Container::$sessionHandler = new DatabaseSessionHandler( Container::$sessionHandler = new DatabaseSessionHandler(
Container::$dbConnection, Container::$dbConnection,
'sessions', 'sessions',
new DateTime('-7 days') new DateTime('-1 days')
); );
session_set_save_handler(Container::$sessionHandler, true); session_set_save_handler(Container::$sessionHandler, true);