Rewrote tracker authentication to use OpenID Connect.
This commit is contained in:
parent
98cf112002
commit
da83e85735
9 changed files with 862 additions and 110 deletions
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"require": {
|
||||
"flashwave/index": "^0.2503",
|
||||
"flashii/apii": "^0.5",
|
||||
"erusev/parsedown": "^1.7",
|
||||
"sentry/sdk": "^4.0"
|
||||
"sentry/sdk": "^4.0",
|
||||
"guzzlehttp/guzzle": "~7.9"
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
|
|
47
composer.lock
generated
47
composer.lock
generated
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "7a7507e696bc63c5c529fcd1db2542d2",
|
||||
"content-hash": "551e2a9180a3d744f8feb737da6afb74",
|
||||
"packages": [
|
||||
{
|
||||
"name": "erusev/parsedown",
|
||||
|
@ -56,52 +56,13 @@
|
|||
},
|
||||
"time": "2019-12-30T22:54:17+00:00"
|
||||
},
|
||||
{
|
||||
"name": "flashii/apii",
|
||||
"version": "v0.5.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://patchii.net/flashii/apii-php.git",
|
||||
"reference": "69c0a1fa2780f4dda72e1eb4c50599abab9230f8"
|
||||
},
|
||||
"require": {
|
||||
"guzzlehttp/guzzle": "~7.9",
|
||||
"php": ">=8.1",
|
||||
"psr/http-client": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "~2.1",
|
||||
"phpunit/phpunit": "~10.5"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Flashii\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"bsd-3-clause-clear"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "flashwave",
|
||||
"email": "packagist@flash.moe",
|
||||
"homepage": "https://flash.moe",
|
||||
"role": "mom"
|
||||
}
|
||||
],
|
||||
"description": "Client library for the Flashii.net API.",
|
||||
"homepage": "https://api.flashii.net",
|
||||
"time": "2025-03-25T11:42:32+00:00"
|
||||
},
|
||||
{
|
||||
"name": "flashwave/index",
|
||||
"version": "v0.2503.230355",
|
||||
"version": "v0.2503.251852",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://patchii.net/flash/index.git",
|
||||
"reference": "2372a113d26380176994f64ab99c42aaf2e9d98e"
|
||||
"reference": "60c21301824719551c995e004288f3bfcd1a2509"
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
|
@ -150,7 +111,7 @@
|
|||
],
|
||||
"description": "Composer package for the common library for my projects.",
|
||||
"homepage": "https://railgun.sh/index",
|
||||
"time": "2025-03-23T03:45:47+00:00"
|
||||
"time": "2025-03-25T18:53:17+00:00"
|
||||
},
|
||||
{
|
||||
"name": "guzzlehttp/guzzle",
|
||||
|
|
|
@ -1,32 +1,11 @@
|
|||
<?php
|
||||
namespace Seria;
|
||||
|
||||
use RuntimeException;
|
||||
use Flashii\{FlashiiClient,FlashiiUrls};
|
||||
use Flashii\Credentials\MisuzuCredentials;
|
||||
use Seria\Users\UserInfo;
|
||||
|
||||
require_once __DIR__ . '/../seria.php';
|
||||
|
||||
$authToken = (string)filter_input(INPUT_COOKIE, 'msz_auth');
|
||||
$flashii = new FlashiiClient('Seria', new MisuzuCredentials($authToken), new FlashiiUrls(
|
||||
$cfg->getString('apii:url', FlashiiUrls::PROD_URL),
|
||||
));
|
||||
|
||||
try {
|
||||
$authInfo = $flashii->v1()->me();
|
||||
if($authInfo !== null) {
|
||||
$seria->usersCtx->users->syncApiUser($authInfo);
|
||||
$sUserInfo = $seria->usersCtx->users->getUser($authInfo->getId(), 'id');
|
||||
$seria->authInfo->userInfo = $sUserInfo;
|
||||
}
|
||||
} catch(RuntimeException $ex) {
|
||||
$authInfo = $sUserInfo = null;
|
||||
}
|
||||
|
||||
$seria->initCsrfToken(
|
||||
$cfg->getString('csrfp:secret', 'mewow'),
|
||||
$authInfo === null ? (string)filter_input(INPUT_SERVER, 'REMOTE_ADDR') : $authToken
|
||||
(string)filter_input(INPUT_SERVER, 'REMOTE_ADDR')
|
||||
);
|
||||
|
||||
$seria->startTemplating();
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
namespace Seria;
|
||||
|
||||
use Index\Cache\CacheBackends;
|
||||
use Index\Config\Fs\FsConfig;
|
||||
use Index\Db\DbBackends;
|
||||
|
||||
|
@ -35,4 +36,6 @@ if($cfg->hasValues('sentry:dsn'))
|
|||
$db = DbBackends::create($cfg->getString('database:dsn', 'null:'));
|
||||
$db->execute('SET SESSION time_zone = \'+00:00\', sql_mode = \'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION\';');
|
||||
|
||||
$seria = new SeriaContext($db, $cfg);
|
||||
$cache = CacheBackends::create($cfg->getString('cache:dsn', 'null:'));
|
||||
|
||||
$seria = new SeriaContext($db, $cache, $cfg);
|
||||
|
|
785
src/Auth/AuthRoutes.php
Normal file
785
src/Auth/AuthRoutes.php
Normal file
|
@ -0,0 +1,785 @@
|
|||
<?php
|
||||
namespace Seria\Auth;
|
||||
|
||||
use RuntimeException;
|
||||
use UnexpectedValueException;
|
||||
use GuzzleHttp\Client as GuzzleHttpClient;
|
||||
use Index\UriBase64;
|
||||
use Index\Cache\CacheProvider;
|
||||
use Index\Config\Config;
|
||||
use Index\Http\{HttpResponseBuilder,HttpRequest,HttpUri};
|
||||
use Index\Http\Routing\{RouteHandler,RouteHandlerCommon};
|
||||
use Index\Http\Routing\Filters\PrefixFilter;
|
||||
use Index\Http\Routing\Routes\ExactRoute;
|
||||
use Seria\GitInfo;
|
||||
use Seria\Users\UsersContext;
|
||||
|
||||
class AuthRoutes implements RouteHandler {
|
||||
use RouteHandlerCommon;
|
||||
|
||||
private GuzzleHttpClient $client;
|
||||
|
||||
public function __construct(
|
||||
private CacheProvider $cache,
|
||||
private Config $config,
|
||||
private UsersContext $usersCtx,
|
||||
private AuthInfo $authInfo,
|
||||
) {
|
||||
$this->client = new GuzzleHttpClient([
|
||||
'allow_redirects' => true,
|
||||
'timeout' => 10,
|
||||
'headers' => [
|
||||
'User-Agent' => sprintf('Seria/%s', GitInfo::version()),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T of object
|
||||
* @param callable(): T $constructor
|
||||
* @return T
|
||||
*/
|
||||
private function cached(string $key, callable $constructor, int $lifetime = 300): object {
|
||||
$value = $this->cache->get($key);
|
||||
if(is_object($value))
|
||||
return $value;
|
||||
|
||||
$value = $constructor();
|
||||
$this->cache->set($key, $value, $lifetime);
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object{
|
||||
* access_token: string,
|
||||
* expires_in?: int,
|
||||
* refresh_token?: string,
|
||||
* } $tokenInfo
|
||||
*/
|
||||
private function applyCookies(HttpResponseBuilder $response, HttpRequest $request, object $tokenInfo): void {
|
||||
$response->addCookie(
|
||||
'seria_access',
|
||||
$tokenInfo->access_token,
|
||||
expires: time() + ($tokenInfo->expires_in ?? 3600),
|
||||
path: '/',
|
||||
secure: $request->secure,
|
||||
httpOnly: true,
|
||||
sameSiteStrict: true,
|
||||
);
|
||||
if(!empty($tokenInfo->refresh_token))
|
||||
$response->addCookie(
|
||||
'seria_refresh',
|
||||
$tokenInfo->refresh_token,
|
||||
expires: strtotime('1 year'),
|
||||
path: '/',
|
||||
secure: $request->secure,
|
||||
httpOnly: true,
|
||||
sameSiteStrict: true,
|
||||
);
|
||||
}
|
||||
|
||||
#[PrefixFilter('/')]
|
||||
public function filterAuth(HttpResponseBuilder $response, HttpRequest $request): void {
|
||||
$resourceInfo = $openIdConfig = $userInfo = null;
|
||||
|
||||
if($request->hasCookie('seria_access')) {
|
||||
try {
|
||||
$resourceInfo ??= $this->getOAuth2ProtectedResource();
|
||||
$openIdConfig ??= $this->getOpenIdConfiguration($resourceInfo->authorization_servers[array_rand($resourceInfo->authorization_servers)]);
|
||||
$userInfo = $this->getOpenIdUserInfo(
|
||||
$openIdConfig->userinfo_endpoint,
|
||||
(string)$request->getCookie('seria_access')
|
||||
);
|
||||
} catch(RuntimeException|UnexpectedValueException $ex) {
|
||||
$userInfo = null;
|
||||
}
|
||||
}
|
||||
|
||||
if($userInfo === null && $request->hasCookie('seria_refresh')) {
|
||||
try {
|
||||
$resourceInfo ??= $this->getOAuth2ProtectedResource();
|
||||
$openIdConfig ??= $this->getOpenIdConfiguration($resourceInfo->authorization_servers[array_rand($resourceInfo->authorization_servers)]);
|
||||
|
||||
$tokenInfo = $this->postTokenWithRefreshToken(
|
||||
$openIdConfig->token_endpoint,
|
||||
(string)$request->getCookie('seria_refresh'),
|
||||
in_array('client_secret_basic', $openIdConfig->token_endpoint_auth_methods_supported),
|
||||
);
|
||||
$this->applyCookies($response, $request, $tokenInfo);
|
||||
|
||||
$userInfo = $this->getOpenIdUserInfo(
|
||||
$openIdConfig->userinfo_endpoint,
|
||||
$tokenInfo->access_token
|
||||
);
|
||||
} catch(RuntimeException|UnexpectedValueException $ex) {
|
||||
$userInfo = null;
|
||||
}
|
||||
}
|
||||
|
||||
if($userInfo !== null) {
|
||||
$this->usersCtx->users->syncApiUser($userInfo);
|
||||
$this->authInfo->userInfo = $this->usersCtx->users->getUser($userInfo->sub, 'id');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RuntimeException
|
||||
* @throws UnexpectedValueException
|
||||
* @return object{
|
||||
* sub: string.
|
||||
* name: string,
|
||||
* nickname: string,
|
||||
* preferred_username: string,
|
||||
* profile: string,
|
||||
* picture: string,
|
||||
* zoneinfo: string,
|
||||
* birthdate?: string,
|
||||
* website?: string,
|
||||
* 'http://railgun.sh/country_code': string,
|
||||
* 'http://railgun.sh/colour_raw': ?int,
|
||||
* 'http://railgun.sh/colour_css': string,
|
||||
* 'http://railgun.sh/rank': int,
|
||||
* 'http://railgun.sh/roles'?: string[],
|
||||
* 'http://railgun.sh/banned'?: bool,
|
||||
* 'http://railgun.sh/is_super'?: bool,
|
||||
* }
|
||||
*/
|
||||
public function getOpenIdUserInfo(string $userInfoEndpoint, string $accessToken): object {
|
||||
return $this->cached(
|
||||
sprintf('seria:getOpenIdUserInfo:%s#%s', $userInfoEndpoint, $accessToken),
|
||||
function() use ($userInfoEndpoint, $accessToken) {
|
||||
$payload = json_decode((string)$this->client->get($userInfoEndpoint, [
|
||||
'headers' => [
|
||||
'Authorization' => sprintf('Bearer %s', $accessToken),
|
||||
],
|
||||
])->getBody());
|
||||
|
||||
if(!is_object($payload))
|
||||
throw new UnexpectedValueException('payload was not a json object');
|
||||
|
||||
if(property_exists($payload, 'error')) {
|
||||
$error = is_string($payload->error) ? $payload->error : 'unknown';
|
||||
if(property_exists($payload, 'error_description')
|
||||
&& is_string($payload->error_description))
|
||||
$error = sprintf('[%s] %s', $error, $payload->error_description);
|
||||
|
||||
throw new RuntimeException($error);
|
||||
}
|
||||
|
||||
if(!property_exists($payload, 'sub')
|
||||
|| !is_string($payload->sub))
|
||||
throw new UnexpectedValueException('payload->sub must be a string');
|
||||
|
||||
if(!property_exists($payload, 'name')
|
||||
|| !is_string($payload->name))
|
||||
throw new UnexpectedValueException('payload->name must be a string');
|
||||
|
||||
if(!property_exists($payload, 'nickname')
|
||||
|| !is_string($payload->nickname))
|
||||
throw new UnexpectedValueException('payload->nickname must be a string');
|
||||
|
||||
if(!property_exists($payload, 'preferred_username')
|
||||
|| !is_string($payload->preferred_username))
|
||||
throw new UnexpectedValueException('payload->preferred_username must be a string');
|
||||
|
||||
if(!property_exists($payload, 'profile')
|
||||
|| !is_string($payload->profile))
|
||||
throw new UnexpectedValueException('payload->profile must be a string');
|
||||
|
||||
if(!property_exists($payload, 'picture')
|
||||
|| !is_string($payload->picture))
|
||||
throw new UnexpectedValueException('payload->picture must be a string');
|
||||
|
||||
if(!property_exists($payload, 'zoneinfo')
|
||||
|| !is_string($payload->zoneinfo))
|
||||
throw new UnexpectedValueException('payload->zoneinfo must be a string');
|
||||
|
||||
if(property_exists($payload, 'birthdate')
|
||||
&& !is_string($payload->birthdate))
|
||||
throw new UnexpectedValueException('payload->birthdate must be a string, if present');
|
||||
|
||||
if(property_exists($payload, 'website')
|
||||
&& !is_string($payload->website))
|
||||
throw new UnexpectedValueException('payload->website must be a string, if present');
|
||||
|
||||
if(!property_exists($payload, 'http://railgun.sh/country_code')
|
||||
|| !is_string($payload->{'http://railgun.sh/country_code'}))
|
||||
throw new UnexpectedValueException('payload->http://railgun.sh/country_code must be a string');
|
||||
|
||||
if(!property_exists($payload, 'http://railgun.sh/colour_raw')
|
||||
|| ($payload->{'http://railgun.sh/colour_raw'} !== null && !is_int($payload->{'http://railgun.sh/colour_raw'})))
|
||||
throw new UnexpectedValueException('payload->http://railgun.sh/colour_raw must be an integer or null');
|
||||
|
||||
if(!property_exists($payload, 'http://railgun.sh/colour_css')
|
||||
|| !is_string($payload->{'http://railgun.sh/colour_css'}))
|
||||
throw new UnexpectedValueException('payload->http://railgun.sh/colour_css must be a string');
|
||||
|
||||
if(!property_exists($payload, 'http://railgun.sh/rank')
|
||||
|| !is_int($payload->{'http://railgun.sh/rank'}))
|
||||
throw new UnexpectedValueException('payload->http://railgun.sh/rank must be an integer');
|
||||
|
||||
if(property_exists($payload, 'http://railgun.sh/roles')
|
||||
&& (
|
||||
!is_array($payload->{'http://railgun.sh/roles'})
|
||||
|| !array_all($payload->{'http://railgun.sh/roles'}, fn($value) => is_string($value))
|
||||
))
|
||||
throw new UnexpectedValueException('payload->http://railgun.sh/roles must be an array or strings');
|
||||
|
||||
if(property_exists($payload, 'http://railgun.sh/banned')
|
||||
&& !is_bool($payload->{'http://railgun.sh/banned'}))
|
||||
throw new UnexpectedValueException('payload->http://railgun.sh/banned must be a boolean, if present');
|
||||
|
||||
if(property_exists($payload, 'http://railgun.sh/is_super')
|
||||
&& !is_bool($payload->{'http://railgun.sh/is_super'}))
|
||||
throw new UnexpectedValueException('payload->http://railgun.sh/is_super must be a boolean, if present');
|
||||
|
||||
return $payload;
|
||||
},
|
||||
lifetime: 30
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws UnexpectedValueException
|
||||
* @throws RuntimeException
|
||||
* @return object{
|
||||
* access_token: string,
|
||||
* token_type?: string,
|
||||
* expires_in?: int,
|
||||
* scope?: string,
|
||||
* refresh_token?: string,
|
||||
* id_token?: string,
|
||||
* }
|
||||
*/
|
||||
public function verifyTokenResponse(mixed $payload): object {
|
||||
if(!is_object($payload))
|
||||
throw new UnexpectedValueException('payload was not a json object');
|
||||
|
||||
if(property_exists($payload, 'error')) {
|
||||
$error = is_string($payload->error) ? $payload->error : 'unknown';
|
||||
if(property_exists($payload, 'error_description')
|
||||
&& is_string($payload->error_description))
|
||||
$error = sprintf('[%s] %s', $error, $payload->error_description);
|
||||
|
||||
throw new RuntimeException($error);
|
||||
}
|
||||
|
||||
if(!property_exists($payload, 'access_token')
|
||||
|| !is_string($payload->access_token))
|
||||
throw new UnexpectedValueException('payload->access_token must be a string');
|
||||
|
||||
if(property_exists($payload, 'token_type')
|
||||
&& (!is_string($payload->token_type) || strcasecmp($payload->token_type, 'bearer') !== 0))
|
||||
throw new UnexpectedValueException('payload->token_type must be equals to "Bearer", if present');
|
||||
|
||||
if(property_exists($payload, 'expires_in')
|
||||
&& !is_int($payload->expires_in))
|
||||
throw new UnexpectedValueException('payload->expires_in must be an integer, if present');
|
||||
|
||||
if(property_exists($payload, 'scope')
|
||||
&& !is_string($payload->scope))
|
||||
throw new UnexpectedValueException('payload->scope must be a string, if present');
|
||||
|
||||
if(property_exists($payload, 'refresh_token')
|
||||
&& !is_string($payload->refresh_token))
|
||||
throw new UnexpectedValueException('payload->refresh_token must be a string, if present');
|
||||
|
||||
if(property_exists($payload, 'id_token')
|
||||
&& !is_string($payload->id_token))
|
||||
throw new UnexpectedValueException('payload->id_token must be a string, if present');
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RuntimeException
|
||||
* @throws UnexpectedValueException
|
||||
* @return object{
|
||||
* access_token: string,
|
||||
* token_type?: string,
|
||||
* expires_in?: int,
|
||||
* scope?: string,
|
||||
* refresh_token?: string,
|
||||
* id_token?: string,
|
||||
* }
|
||||
*/
|
||||
public function postTokenWithAuthorizationCode(string $tokenEndpoint, string $code, string $codeVerifier, bool $useBasicAuth): object {
|
||||
$headers = [
|
||||
'Content-Type' => 'application/x-www-form-urlencoded',
|
||||
];
|
||||
$body = [
|
||||
'grant_type' => 'authorization_code',
|
||||
'code' => $code,
|
||||
'code_verifier' => $codeVerifier,
|
||||
];
|
||||
|
||||
if($useBasicAuth) {
|
||||
$headers['Authorization'] = sprintf('Basic %s', base64_encode(sprintf(
|
||||
'%s:%s',
|
||||
$this->config->getString('client_id'),
|
||||
$this->config->getString('client_secret'),
|
||||
)));
|
||||
} else {
|
||||
$body['client_id'] = $this->config->getString('client_id');
|
||||
$body['client_secret'] = $this->config->getString('client_secret');
|
||||
}
|
||||
|
||||
return $this->verifyTokenResponse(json_decode((string)$this->client->post($tokenEndpoint, [
|
||||
'headers' => $headers,
|
||||
'body' => HttpUri::buildQueryString($body),
|
||||
])->getBody()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RuntimeException
|
||||
* @throws UnexpectedValueException
|
||||
* @return object{
|
||||
* access_token: string,
|
||||
* token_type?: string,
|
||||
* expires_in?: int,
|
||||
* scope?: string,
|
||||
* refresh_token?: string,
|
||||
* id_token?: string,
|
||||
* }
|
||||
*/
|
||||
public function postTokenWithRefreshToken(string $tokenEndpoint, string $refreshToken, bool $useBasicAuth): object {
|
||||
$headers = [
|
||||
'Content-Type' => 'application/x-www-form-urlencoded',
|
||||
];
|
||||
$body = [
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $refreshToken,
|
||||
];
|
||||
|
||||
if($useBasicAuth) {
|
||||
$headers['Authorization'] = sprintf('Basic %s', base64_encode(sprintf(
|
||||
'%s:%s',
|
||||
$this->config->getString('client_id'),
|
||||
$this->config->getString('client_secret'),
|
||||
)));
|
||||
} else {
|
||||
$body['client_id'] = $this->config->getString('client_id');
|
||||
$body['client_secret'] = $this->config->getString('client_secret');
|
||||
}
|
||||
|
||||
return $this->verifyTokenResponse(json_decode((string)$this->client->post($tokenEndpoint, [
|
||||
'headers' => $headers,
|
||||
'body' => HttpUri::buildQueryString($body),
|
||||
])->getBody()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws UnexpectedValueException
|
||||
* @return object{
|
||||
* resource: string,
|
||||
* authorization_servers: string[],
|
||||
* scopes_supported: string[],
|
||||
* bearer_methods_supported: string[],
|
||||
* }
|
||||
*/
|
||||
public function getOAuth2ProtectedResource(): object {
|
||||
return $this->cached(
|
||||
sprintf('seria:getOAuth2ProtectedResource:%s', $this->config->getString('base_url')),
|
||||
function() {
|
||||
$payload = json_decode((string)$this->client->get($this->config->getString('base_url') . '/.well-known/oauth-protected-resource')->getBody());
|
||||
|
||||
if(!is_object($payload))
|
||||
throw new UnexpectedValueException('payload was not a json object');
|
||||
|
||||
if(!property_exists($payload, 'resource')
|
||||
|| !is_string($payload->resource))
|
||||
throw new UnexpectedValueException('payload->resource was is string');
|
||||
|
||||
if(!property_exists($payload, 'authorization_servers')
|
||||
|| !is_array($payload->authorization_servers)
|
||||
|| empty($payload->authorization_servers)
|
||||
|| !array_all($payload->authorization_servers, fn($value) => is_string($value)))
|
||||
throw new UnexpectedValueException('payload->authorization_servers must be a non-empty array of strings');
|
||||
|
||||
if(!property_exists($payload, 'scopes_supported')
|
||||
|| !is_array($payload->scopes_supported)
|
||||
|| !array_all($payload->scopes_supported, fn($value) => is_string($value)))
|
||||
throw new UnexpectedValueException('payload->scopes_supported must be an array of strings');
|
||||
|
||||
if(!property_exists($payload, 'bearer_methods_supported')
|
||||
|| !is_array($payload->bearer_methods_supported)
|
||||
|| !array_all($payload->bearer_methods_supported, fn($value) => is_string($value)))
|
||||
throw new UnexpectedValueException('payload->bearer_methods_supported must be an array of strings');
|
||||
|
||||
return $payload;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return object{
|
||||
* issuer: string,
|
||||
* authorization_endpoint: string,
|
||||
* token_endpoint: string,
|
||||
* jwks_uri: string,
|
||||
* protected_resources: string[],
|
||||
* scopes_supported: string[],
|
||||
* response_types_supported: string[],
|
||||
* response_modes_supported: string[],
|
||||
* grant_types_supported: string[],
|
||||
* token_endpoint_auth_methods_supported: string[],
|
||||
* code_challenge_methods_supported: string[],
|
||||
* }
|
||||
*/
|
||||
public function getOAuth2AuthorizationServer(string $server): object {
|
||||
return $this->cached(
|
||||
sprintf('seria:getOAuth2AuthorizationServer:%s', $server),
|
||||
function() use ($server) {
|
||||
$payload = json_decode((string)$this->client->get($server . '/.well-known/oauth-authorization-server')->getBody());
|
||||
|
||||
if(!is_object($payload))
|
||||
throw new UnexpectedValueException('payload was not a json object');
|
||||
|
||||
if(!property_exists($payload, 'issuer')
|
||||
|| !is_string($payload->issuer))
|
||||
throw new UnexpectedValueException('payload->issuer must be a string');
|
||||
|
||||
if(!property_exists($payload, 'authorization_endpoint')
|
||||
|| !is_string($payload->authorization_endpoint))
|
||||
throw new UnexpectedValueException('payload->authorization_endpoint must be a string');
|
||||
|
||||
if(!property_exists($payload, 'token_endpoint')
|
||||
|| !is_string($payload->token_endpoint))
|
||||
throw new UnexpectedValueException('payload->token_endpoint must be a string');
|
||||
|
||||
if(!property_exists($payload, 'jwks_uri')
|
||||
|| !is_string($payload->jwks_uri))
|
||||
throw new UnexpectedValueException('payload->jwks_uri must be a string');
|
||||
|
||||
if(!property_exists($payload, 'protected_resources')
|
||||
|| !is_array($payload->protected_resources)
|
||||
|| !array_all($payload->protected_resources, fn($value) => is_string($value)))
|
||||
throw new UnexpectedValueException('payload->protected_resources must be an array of strings');
|
||||
|
||||
if(!property_exists($payload, 'scopes_supported')
|
||||
|| !is_array($payload->scopes_supported)
|
||||
|| !array_all($payload->scopes_supported, fn($value) => is_string($value)))
|
||||
throw new UnexpectedValueException('payload->scopes_supported must be an array of strings');
|
||||
|
||||
if(!property_exists($payload, 'response_types_supported')
|
||||
|| !is_array($payload->response_types_supported)
|
||||
|| !array_all($payload->response_types_supported, fn($value) => is_string($value)))
|
||||
throw new UnexpectedValueException('payload->response_types_supported must be an array of strings');
|
||||
|
||||
if(!property_exists($payload, 'response_modes_supported')
|
||||
|| !is_array($payload->response_modes_supported)
|
||||
|| !array_all($payload->response_modes_supported, fn($value) => is_string($value)))
|
||||
throw new UnexpectedValueException('payload->response_modes_supported must be an array of strings');
|
||||
|
||||
if(!property_exists($payload, 'grant_types_supported')
|
||||
|| !is_array($payload->grant_types_supported)
|
||||
|| !array_all($payload->grant_types_supported, fn($value) => is_string($value)))
|
||||
throw new UnexpectedValueException('payload->grant_types_supported must be an array of strings');
|
||||
|
||||
if(!property_exists($payload, 'token_endpoint_auth_methods_supported')
|
||||
|| !is_array($payload->token_endpoint_auth_methods_supported)
|
||||
|| !array_all($payload->token_endpoint_auth_methods_supported, fn($value) => is_string($value)))
|
||||
throw new UnexpectedValueException('payload->token_endpoint_auth_methods_supported must be an array of strings');
|
||||
|
||||
if(!property_exists($payload, 'code_challenge_methods_supported')
|
||||
|| !is_array($payload->code_challenge_methods_supported)
|
||||
|| !array_all($payload->code_challenge_methods_supported, fn($value) => is_string($value)))
|
||||
throw new UnexpectedValueException('payload->code_challenge_methods_supported must be an array of strings');
|
||||
|
||||
return $payload;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return object{
|
||||
* issuer: string,
|
||||
* authorization_endpoint: string,
|
||||
* token_endpoint: string,
|
||||
* userinfo_endpoint: string,
|
||||
* jwks_uri: string,
|
||||
* response_types_supported: string[],
|
||||
* response_modes_supported: string[],
|
||||
* grant_types_supported: string[],
|
||||
* subject_types_supported: string[],
|
||||
* id_token_signing_alg_values_supported: string[],
|
||||
* token_endpoint_auth_methods_supported: string[],
|
||||
* }
|
||||
*/
|
||||
public function getOpenIdConfiguration(string $server): object {
|
||||
return $this->cached(
|
||||
sprintf('seria:getOpenIdConfiguration:%s', $server),
|
||||
function() use ($server) {
|
||||
$payload = json_decode((string)$this->client->get($server . '/.well-known/openid-configuration')->getBody());
|
||||
|
||||
if(!is_object($payload))
|
||||
throw new UnexpectedValueException('payload was not a json object');
|
||||
|
||||
if(!property_exists($payload, 'issuer')
|
||||
|| !is_string($payload->issuer))
|
||||
throw new UnexpectedValueException('payload->issuer must be a string');
|
||||
|
||||
if(!property_exists($payload, 'authorization_endpoint')
|
||||
|| !is_string($payload->authorization_endpoint))
|
||||
throw new UnexpectedValueException('payload->authorization_endpoint must be a string');
|
||||
|
||||
if(!property_exists($payload, 'token_endpoint')
|
||||
|| !is_string($payload->token_endpoint))
|
||||
throw new UnexpectedValueException('payload->token_endpoint must be a string');
|
||||
|
||||
if(!property_exists($payload, 'userinfo_endpoint')
|
||||
|| !is_string($payload->userinfo_endpoint))
|
||||
throw new UnexpectedValueException('payload->userinfo_endpoint must be a string');
|
||||
|
||||
if(!property_exists($payload, 'jwks_uri')
|
||||
|| !is_string($payload->jwks_uri))
|
||||
throw new UnexpectedValueException('payload->jwks_uri must be a string');
|
||||
|
||||
if(!property_exists($payload, 'response_types_supported')
|
||||
|| !is_array($payload->response_types_supported)
|
||||
|| !array_all($payload->response_types_supported, fn($value) => is_string($value)))
|
||||
throw new UnexpectedValueException('payload->response_types_supported must be an array of strings');
|
||||
|
||||
if(!property_exists($payload, 'response_modes_supported')
|
||||
|| !is_array($payload->response_modes_supported)
|
||||
|| !array_all($payload->response_modes_supported, fn($value) => is_string($value)))
|
||||
throw new UnexpectedValueException('payload->response_modes_supported must be an array of strings');
|
||||
|
||||
if(!property_exists($payload, 'grant_types_supported')
|
||||
|| !is_array($payload->grant_types_supported)
|
||||
|| !array_all($payload->grant_types_supported, fn($value) => is_string($value)))
|
||||
throw new UnexpectedValueException('payload->grant_types_supported must be an array of strings');
|
||||
|
||||
if(!property_exists($payload, 'subject_types_supported')
|
||||
|| !is_array($payload->subject_types_supported)
|
||||
|| !array_all($payload->subject_types_supported, fn($value) => is_string($value)))
|
||||
throw new UnexpectedValueException('payload->subject_types_supported must be an array of strings');
|
||||
|
||||
if(!property_exists($payload, 'id_token_signing_alg_values_supported')
|
||||
|| !is_array($payload->id_token_signing_alg_values_supported)
|
||||
|| !array_all($payload->id_token_signing_alg_values_supported, fn($value) => is_string($value)))
|
||||
throw new UnexpectedValueException('payload->id_token_signing_alg_values_supported must be an array of strings');
|
||||
|
||||
if(!property_exists($payload, 'token_endpoint_auth_methods_supported')
|
||||
|| !is_array($payload->token_endpoint_auth_methods_supported)
|
||||
|| !array_all($payload->token_endpoint_auth_methods_supported, fn($value) => is_string($value)))
|
||||
throw new UnexpectedValueException('payload->token_endpoint_auth_methods_supported must be an array of strings');
|
||||
|
||||
return $payload;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[ExactRoute('GET', '/auth/authorize')]
|
||||
public function getAuthorize(HttpResponseBuilder $response): int {
|
||||
try {
|
||||
$resourceInfo = $this->getOAuth2ProtectedResource();
|
||||
$authzServer = $this->getOAuth2AuthorizationServer($resourceInfo->authorization_servers[array_rand($resourceInfo->authorization_servers)]);
|
||||
} catch(RuntimeException|UnexpectedValueException $ex) {
|
||||
return 500;
|
||||
}
|
||||
|
||||
if(!in_array('authorization_code', $authzServer->grant_types_supported))
|
||||
return 500;
|
||||
|
||||
if(!in_array('query', $authzServer->response_modes_supported))
|
||||
return 500;
|
||||
|
||||
if(!in_array('openid', $authzServer->scopes_supported)
|
||||
|| !in_array('profile', $authzServer->scopes_supported))
|
||||
return 500;
|
||||
|
||||
if(!in_array('client_secret_basic', $authzServer->token_endpoint_auth_methods_supported)
|
||||
|| !in_array('client_secret_post', $authzServer->token_endpoint_auth_methods_supported))
|
||||
return 500;
|
||||
|
||||
$scope = 'openid profile';
|
||||
if(in_array('offline_access', $authzServer->scopes_supported)
|
||||
&& in_array('refresh_token', $authzServer->grant_types_supported))
|
||||
$scope .= ' offline_access';
|
||||
|
||||
$verifier = random_bytes(20);
|
||||
$time = pack('J', time());
|
||||
$state = $time . $verifier;
|
||||
$state = hash_hmac('sha256', $state, $this->config->getString('state_secret'), true) . $state;
|
||||
|
||||
$params = [
|
||||
'redirect_uri' => ($this->config->getString('redirect_base_url') . '/auth/callback'),
|
||||
'client_id' => $this->config->getString('client_id'),
|
||||
'scope' => $scope,
|
||||
'state' => UriBase64::encode($state),
|
||||
];
|
||||
|
||||
if(in_array('code id_token', $authzServer->response_types_supported))
|
||||
$params['response_type'] = 'code id_token';
|
||||
else return 500;
|
||||
|
||||
if(in_array('S256', $authzServer->code_challenge_methods_supported)) {
|
||||
$params['code_challenge_method'] = 'S256';
|
||||
$verifier = hash('sha256', $verifier, true);
|
||||
} elseif(in_array('plain', $authzServer->code_challenge_methods_supported)) {
|
||||
$params['code_challenge_method'] = 'plain';
|
||||
} else return 500;
|
||||
|
||||
$params['code_challenge'] = UriBase64::encode($verifier);
|
||||
|
||||
$response->redirect($authzServer->authorization_endpoint . '?' . HttpUri::buildQueryString($params));
|
||||
return 302;
|
||||
}
|
||||
|
||||
#[ExactRoute('GET', '/auth/callback')]
|
||||
public function getCallback(HttpResponseBuilder $response, HttpRequest $request) {
|
||||
$state = $request->getParam('state');
|
||||
if(empty($state))
|
||||
return 400;
|
||||
|
||||
$state = UriBase64::decode($state);
|
||||
if(strlen($state) < 60)
|
||||
return 400;
|
||||
|
||||
$signature = hash_hmac('sha256', substr($state, 32), $this->config->getString('state_secret'), true);
|
||||
if(!hash_equals($signature, substr($state, 0, 32)))
|
||||
return 403;
|
||||
|
||||
$state = unpack('a32hash/Jtime/a20verifier', $state);
|
||||
if(empty($state))
|
||||
return 500;
|
||||
|
||||
if($state['time'] < strtotime('-15 minutes') || $state['time'] > strtotime('+15 minutes'))
|
||||
return 403;
|
||||
|
||||
$error = $request->getParam('error');
|
||||
if(!empty($error)) {
|
||||
$text = $request->getParam('error_description');
|
||||
if(empty($text))
|
||||
$text = match($error) {
|
||||
'access_denied' => 'You cancelled the authorization request.',
|
||||
'invalid_request' => 'The authorization server did not understand the request, please report this!',
|
||||
'invalid_scope' => 'The authorization server did not understand the requested scope, please report this!',
|
||||
'server_error' => 'Something went wrong on the authorization server, please try again later.',
|
||||
default => sprintf('An unexpected error occurred: %s', $error),
|
||||
};
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
$code = $request->getParam('code');
|
||||
$idToken = $request->getParam('id_token');
|
||||
if(empty($code) || empty($idToken))
|
||||
return 400;
|
||||
|
||||
try {
|
||||
$resourceInfo = $this->getOAuth2ProtectedResource();
|
||||
} catch(RuntimeException|UnexpectedValueException $ex) {
|
||||
return 500;
|
||||
}
|
||||
|
||||
// todo: verify id_token signature, need to turn the JWT code in Misuzu into a library
|
||||
|
||||
$idToken = explode('.', $idToken, 3);
|
||||
if(count($idToken) < 3)
|
||||
return 400;
|
||||
|
||||
$idTokenHead = json_decode(UriBase64::decode($idToken[0]));
|
||||
if(!property_exists($idTokenHead, 'typ')
|
||||
|| !is_string($idTokenHead->typ)
|
||||
|| $idTokenHead->typ !== 'JWT')
|
||||
return 400;
|
||||
|
||||
if(!property_exists($idTokenHead, 'alg')
|
||||
|| !is_string($idTokenHead->alg))
|
||||
return 400;
|
||||
|
||||
if(property_exists($idTokenHead, 'kid')
|
||||
&& !is_string($idTokenHead->kid))
|
||||
return 400;
|
||||
|
||||
$idTokenPayload = json_decode(UriBase64::decode($idToken[1]));
|
||||
if(!property_exists($idTokenPayload, 'iss')
|
||||
|| !is_string($idTokenPayload->iss)
|
||||
|| !in_array($idTokenPayload->iss, $resourceInfo->authorization_servers))
|
||||
return 400;
|
||||
|
||||
if(!property_exists($idTokenPayload, 'aud')
|
||||
|| !is_string($idTokenPayload->aud)
|
||||
|| $idTokenPayload->aud !== $this->config->getString('client_id'))
|
||||
return 400;
|
||||
|
||||
if(!property_exists($idTokenPayload, 'iat')
|
||||
|| !is_int($idTokenPayload->iat)
|
||||
|| $idTokenPayload->iat >= time())
|
||||
return 400;
|
||||
|
||||
if(!property_exists($idTokenPayload, 'exp')
|
||||
|| !is_int($idTokenPayload->exp)
|
||||
|| $idTokenPayload->exp < time())
|
||||
return 403;
|
||||
|
||||
try {
|
||||
$authzServer = $this->getOAuth2AuthorizationServer($idTokenPayload->iss);
|
||||
} catch(RuntimeException|UnexpectedValueException $ex) {
|
||||
return 500;
|
||||
}
|
||||
|
||||
if(!in_array($resourceInfo->resource, $authzServer->protected_resources))
|
||||
return 500;
|
||||
|
||||
if(!in_array('authorization_code', $authzServer->grant_types_supported))
|
||||
return 500;
|
||||
|
||||
if(!in_array('client_secret_basic', $authzServer->token_endpoint_auth_methods_supported)
|
||||
|| !in_array('client_secret_post', $authzServer->token_endpoint_auth_methods_supported))
|
||||
return 500;
|
||||
|
||||
try {
|
||||
$openIdConfig = $this->getOpenIdConfiguration($idTokenPayload->iss);
|
||||
} catch(RuntimeException|UnexpectedValueException $ex) {
|
||||
return 500;
|
||||
}
|
||||
|
||||
if(!in_array('public', $openIdConfig->subject_types_supported))
|
||||
return 500;
|
||||
|
||||
if(!in_array($idTokenHead->alg, $openIdConfig->id_token_signing_alg_values_supported))
|
||||
return 400;
|
||||
|
||||
try {
|
||||
$tokenInfo = $this->postTokenWithAuthorizationCode(
|
||||
$authzServer->token_endpoint,
|
||||
$code,
|
||||
$state['verifier'],
|
||||
in_array('client_secret_basic', $authzServer->token_endpoint_auth_methods_supported),
|
||||
);
|
||||
} catch(UnexpectedValueException $ex) {
|
||||
return 500;
|
||||
} catch(RuntimeException $ex) {
|
||||
return 400;
|
||||
}
|
||||
|
||||
$this->applyCookies($response, $request, $tokenInfo);
|
||||
|
||||
$response->redirect('/');
|
||||
return 302;
|
||||
}
|
||||
|
||||
#[ExactRoute('GET', '/auth/terminate')]
|
||||
public function getTerminate(HttpResponseBuilder $response, HttpRequest $request): int {
|
||||
$response->removeCookie(
|
||||
'seria_access',
|
||||
path: '/',
|
||||
secure: $request->secure,
|
||||
httpOnly: true,
|
||||
sameSiteStrict: true,
|
||||
);
|
||||
$response->removeCookie(
|
||||
'seria_refresh',
|
||||
path: '/',
|
||||
secure: $request->secure,
|
||||
httpOnly: true,
|
||||
sameSiteStrict: true,
|
||||
);
|
||||
$response->redirect('/');
|
||||
return 302;
|
||||
}
|
||||
}
|
|
@ -2,7 +2,8 @@
|
|||
namespace Seria;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Index\CsrfToken;
|
||||
use Index\{CsrfToken,Dependencies};
|
||||
use Index\Cache\CacheProvider;
|
||||
use Index\Config\Config;
|
||||
use Index\Db\DbConnection;
|
||||
use Index\Db\Migration\{DbMigrationManager,DbMigrationRepo,FsDbMigrationRepo};
|
||||
|
@ -10,29 +11,37 @@ use Index\Http\Content\FormContent;
|
|||
use Index\Http\Routing\HandlerContext;
|
||||
use Index\Http\Routing\Processors\ProcessorInfo;
|
||||
use Index\Templating\TplEnvironment;
|
||||
use Seria\Auth\AuthInfo;
|
||||
use Seria\Torrents\TorrentsContext;
|
||||
use Seria\Users\UsersContext;
|
||||
|
||||
final class SeriaContext {
|
||||
public private(set) AuthInfo $authInfo;
|
||||
public private(set) Dependencies $deps;
|
||||
|
||||
public private(set) SiteInfo $siteInfo;
|
||||
public private(set) Auth\AuthInfo $authInfo;
|
||||
|
||||
public private(set) CsrfToken $csrf;
|
||||
public private(set) ?TplEnvironment $templating = null;
|
||||
|
||||
public private(set) TorrentsContext $torrentsCtx;
|
||||
public private(set) UsersContext $usersCtx;
|
||||
public private(set) Torrents\TorrentsContext $torrentsCtx;
|
||||
public private(set) Users\UsersContext $usersCtx;
|
||||
|
||||
public function __construct(
|
||||
private DbConnection $dbConn,
|
||||
private Config $config
|
||||
public private(set) DbConnection $dbConn,
|
||||
public private(set) CacheProvider $cache,
|
||||
public private(set) Config $config,
|
||||
) {
|
||||
$this->authInfo = new AuthInfo;
|
||||
$this->siteInfo = new SiteInfo($config->scopeTo('site'));
|
||||
$this->deps = new Dependencies;
|
||||
$this->deps->register($this);
|
||||
$this->deps->register($this->deps);
|
||||
|
||||
$this->torrentsCtx = new TorrentsContext($dbConn);
|
||||
$this->usersCtx = new UsersContext($dbConn);
|
||||
$this->deps->register($this->dbConn);
|
||||
$this->deps->register($this->cache);
|
||||
$this->deps->register($this->config);
|
||||
|
||||
$this->deps->register($this->siteInfo = $this->deps->constructLazy(SiteInfo::class, config: $config->scopeTo('site')));
|
||||
$this->deps->register($this->authInfo = $this->deps->constructLazy(Auth\AuthInfo::class));
|
||||
|
||||
$this->deps->register($this->torrentsCtx = $this->deps->constructLazy(Torrents\TorrentsContext::class));
|
||||
$this->deps->register($this->usersCtx = $this->deps->constructLazy(Users\UsersContext::class));
|
||||
}
|
||||
|
||||
public function getDbQueryCount(): int {
|
||||
|
@ -66,6 +75,7 @@ final class SeriaContext {
|
|||
);
|
||||
$this->templating->addExtension(new TemplatingExtension($this, $this->siteInfo));
|
||||
$this->templating->addGlobal('globals', $globals);
|
||||
$this->deps->register($this->templating);
|
||||
}
|
||||
|
||||
public function createRouting(): RoutingContext {
|
||||
|
@ -101,19 +111,20 @@ final class SeriaContext {
|
|||
}
|
||||
}));
|
||||
|
||||
$routing->register(new HomeRoutes($this->templating));
|
||||
$routing->register(new Users\ProfileRoutes($this->authInfo, $this->torrentsCtx, $this->usersCtx, $this->templating));
|
||||
$routing->register(new Users\SettingsRoutes($this->authInfo, $this->usersCtx, $this->templating));
|
||||
$routing->register(new Torrents\AnnounceRoutes($this->torrentsCtx, $this->usersCtx));
|
||||
$routing->register(new Torrents\TorrentCreateRoutes($this->dbConn, $this->authInfo, $this->torrentsCtx, $this->templating));
|
||||
$routing->register(new Torrents\TorrentInfoRoutes(
|
||||
$this->config->scopeTo('announce'),
|
||||
$this->authInfo,
|
||||
$this->torrentsCtx,
|
||||
$this->usersCtx,
|
||||
$this->templating
|
||||
$routing->register($this->deps->constructLazy(HomeRoutes::class));
|
||||
$routing->register($this->deps->constructLazy(
|
||||
Auth\AuthRoutes::class,
|
||||
config: $this->config->scopeTo('oidc'),
|
||||
));
|
||||
$routing->register(new Torrents\TorrentListRoutes($this->authInfo, $this->torrentsCtx, $this->usersCtx, $this->templating));
|
||||
$routing->register($this->deps->constructLazy(Users\ProfileRoutes::class));
|
||||
$routing->register($this->deps->constructLazy(Users\SettingsRoutes::class));
|
||||
$routing->register($this->deps->constructLazy(Torrents\AnnounceRoutes::class));
|
||||
$routing->register($this->deps->constructLazy(Torrents\TorrentCreateRoutes::class));
|
||||
$routing->register($this->deps->constructLazy(
|
||||
Torrents\TorrentInfoRoutes::class,
|
||||
config: $this->config->scopeTo('announce'),
|
||||
));
|
||||
$routing->register($this->deps->constructLazy(Torrents\TorrentListRoutes::class));
|
||||
|
||||
return $routing;
|
||||
}
|
||||
|
|
|
@ -19,10 +19,6 @@ class SiteInfo {
|
|||
get => $this->config->getString('parent');
|
||||
}
|
||||
|
||||
public string $loginUrl {
|
||||
get => $this->config->getString('login');
|
||||
}
|
||||
|
||||
public function getProfileUrl(UserInfo|string $userInfo): string {
|
||||
if($userInfo instanceof UserInfo)
|
||||
$userInfo = $userInfo->id;
|
||||
|
|
|
@ -30,15 +30,19 @@ final class TemplatingExtension extends AbstractExtension {
|
|||
public function getHeaderMenu(): array {
|
||||
$menu = [];
|
||||
|
||||
if($this->ctx->authInfo->loggedIn)
|
||||
if($this->ctx->authInfo->loggedIn) {
|
||||
$menu[] = [
|
||||
'text' => 'Log out',
|
||||
'url' => '/auth/terminate',
|
||||
];
|
||||
$menu[] = [
|
||||
'text' => 'Settings',
|
||||
'url' => '/settings',
|
||||
];
|
||||
else
|
||||
} else
|
||||
$menu[] = [
|
||||
'text' => 'Log in',
|
||||
'url' => $this->siteInfo->loginUrl,
|
||||
'url' => '/auth/authorize',
|
||||
];
|
||||
|
||||
$menu[] = [
|
||||
|
|
|
@ -3,7 +3,6 @@ namespace Seria\Users;
|
|||
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
use Flashii\V1\Users\V1User;
|
||||
use Index\XString;
|
||||
use Index\Colour\Colour;
|
||||
use Index\Db\{DbConnection,DbStatementCache};
|
||||
|
@ -19,15 +18,29 @@ class Users {
|
|||
return XString::random(48);
|
||||
}
|
||||
|
||||
public function syncApiUser(V1User $authInfo): void {
|
||||
$stmt = $this->cache->get('INSERT INTO ser_users (user_id, user_name, user_colour, user_rank, user_permissions) VALUES (?, ?, ?, ?, 0) ON DUPLICATE KEY UPDATE user_name = ?, user_colour = ?, user_rank = ?');
|
||||
$stmt->nextParameter($authInfo->getId());
|
||||
$stmt->nextParameter($authInfo->getName());
|
||||
$stmt->nextParameter($authInfo->hasColourRaw() ? $authInfo->getColourRaw() : null);
|
||||
$stmt->nextParameter($authInfo->getRank());
|
||||
$stmt->nextParameter($authInfo->getName());
|
||||
$stmt->nextParameter($authInfo->hasColourRaw() ? $authInfo->getColourRaw() : null);
|
||||
$stmt->nextParameter($authInfo->getRank());
|
||||
/**
|
||||
* @param object{
|
||||
* sub: string,
|
||||
* preferred_username: string,
|
||||
* 'http://railgun.sh/colour_raw': ?int,
|
||||
* 'http://railgun.sh/rank': int,
|
||||
* } $authInfo
|
||||
*/
|
||||
public function syncApiUser(object $authInfo): void {
|
||||
$stmt = $this->cache->get(<<<SQL
|
||||
INSERT INTO ser_users (
|
||||
user_id, user_name, user_colour, user_rank, user_permissions
|
||||
) VALUES (?, ?, ?, ?, 0)
|
||||
ON DUPLICATE KEY
|
||||
UPDATE user_name = ?, user_colour = ?, user_rank = ?
|
||||
SQL);
|
||||
$stmt->nextParameter($authInfo->sub);
|
||||
$stmt->nextParameter($authInfo->preferred_username);
|
||||
$stmt->nextParameter($authInfo->{'http://railgun.sh/colour_raw'});
|
||||
$stmt->nextParameter($authInfo->{'http://railgun.sh/rank'});
|
||||
$stmt->nextParameter($authInfo->preferred_username);
|
||||
$stmt->nextParameter($authInfo->{'http://railgun.sh/colour_raw'});
|
||||
$stmt->nextParameter($authInfo->{'http://railgun.sh/rank'});
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue