diff --git a/alchemy.yml b/alchemy.yml index 325f2dc..6722e0e 100644 --- a/alchemy.yml +++ b/alchemy.yml @@ -42,6 +42,19 @@ refactor: php: '8.2' fluent-new-line: true import-names: true + sets: + - dead-code + - code-quality + - coding-style + - type-declarations + - type-declaration-docblocks + - privatization + # - naming + - named-args + - instanceof + - if + - early-return + - rector-preset analyse: level: 8 diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 93867b4..e69de29 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,19 +0,0 @@ -parameters: - ignoreErrors: - - - rawMessage: 'Method Leaf\Auth::db() should return Leaf\Db but returns Leaf\Db|null.' - identifier: return.type - count: 1 - path: src/Auth.php - - - - rawMessage: 'Parameter #1 $db of method Leaf\Auth\User::setDb() expects Leaf\Db, Leaf\Db|null given.' - identifier: argument.type - count: 1 - path: src/Auth.php - - - - rawMessage: 'Offset ''status'' might not exist on array|null.' - identifier: offsetAccess.notFound - count: 2 - path: src/Auth/User.php diff --git a/src/Auth.php b/src/Auth.php index 5f77c10..9eb3f13 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -1,5 +1,7 @@ - */ - protected $errorsArray = []; + /** All errors caught */ + protected array $errorsArray = []; - /** @var array Configured oauth clients */ - protected $oauthClients = []; + /** Configured oauth clients */ + protected array $oauthClients = []; public function __construct() { - if (class_exists('Leaf\App')) { - $this->middleware('auth.required', function () { + if (class_exists(App::class)) { + $this->middleware('auth.required', static function (): void { response()->redirect(Config::get('redirect.login') ?? '/auth/login'); }); - $this->middleware('auth.guest', function () { + $this->middleware('auth.guest', static function (): void { response()->redirect(Config::get('redirect.guest') ?? '/dashboard'); }); - $this->middleware('is', function ($role) { + $this->middleware('is', static function (): void { Pages::error( 'Page not found', 'The page you are looking for could not be found.', - 403 + 403, ); }); - $this->middleware('isNot', function () { + $this->middleware('isNot', static function (): void { Pages::error( 'Page not found', 'The page you are looking for could not be found.', - 403 + 403, ); }); - $this->middleware('can', function () { + $this->middleware('can', static function (): void { Pages::error( 'Page not found', 'The page you are looking for could not be found.', - 403 + 403, ); }); - $this->middleware('cannot', function () { + $this->middleware('cannot', static function (): void { Pages::error( 'Page not found', 'The page you are looking for could not be found.', - 403 + 403, ); }); - $this->middleware('auth.verified', function () { + $this->middleware('auth.verified', static function (): void { response()->redirect('/auth/verify'); }); - $this->middleware('auth.unverified', function () { + $this->middleware('auth.unverified', static function (): void { response()->redirect('/dashboard'); }); } @@ -111,9 +104,9 @@ public function __construct() 'name' => 'google', 'redirectUri' => $this->env( 'GOOGLE_AUTH_REDIRECT_URI', - $this->env('APP_URL') . '/auth/register/google' + $this->env('APP_URL') . '/auth/register/google', ), - ] + ], ); } } @@ -124,10 +117,8 @@ public function __construct() * reads happen once during setup (e.g. resolving the Google redirect * URI), so the uncached read costs nothing and stays honest when the * environment is set at runtime. - * @param mixed $default - * @return mixed */ - private function env(string $name, $default = false) + private function env(string $name, mixed $default = false): mixed { if (function_exists('_envUncached')) { return _envUncached($name, $default); @@ -147,9 +138,8 @@ private function env(string $name, $default = false) * dbtype?: string, * pdoOptions?: mixed[], * } $dbConfig Configuration for leaf db connection - * @return $this */ - public function connect($dbConfig = []) + public function connect($dbConfig = []): static { $this->db = new Db(); $this->db->connect($dbConfig); @@ -157,13 +147,8 @@ public function connect($dbConfig = []) return $this; } - /** - * Connect to database using environment variables - * - * @param mixed[] $pdoOptions Options for PDO connection - * @return $this - */ - public function autoConnect(array $pdoOptions = []) + /** Connect to database using environment variables */ + public function autoConnect(array $pdoOptions = []): static { $this->db = new Db(); $this->db->autoConnect($pdoOptions); @@ -171,13 +156,8 @@ public function autoConnect(array $pdoOptions = []) return $this; } - /** - * Pass in db connection instance directly - * - * @param PDO $connection A connection instance of your db - * @return $this - */ - public function dbConnection(PDO $connection) + /** Pass in db connection instance directly */ + public function dbConnection(PDO $connection): static { $this->db = new Db(); $this->db->connection($connection); @@ -186,29 +166,23 @@ public function dbConnection(PDO $connection) } /** - * Register a Google OAuth client - * --- - * Register a Google OAuth client to use with Leaf Auth, should be a league/oauth2-client compatible client. - * @param string $clientId - * @param string $clientSecret - * @param array{ - * name?: string, - * redirectUri?: string, - * } $options If `$options['redirectUri']` is not set, it will default to the value of `APP_URL` (as resolved by this class's environment helper) with `/auth/google/callback` appended; if `APP_URL` is unset or empty, the default will be `/auth/google/callback`. - * @return static + * Register a Google OAuth client to use with Leaf Auth, should be a + * league/oauth2-client compatible client. + * @param array{name?: string, redirectUri?: string} $options + * - If `$options['redirectUri']` is not set, it will default to the + * value of `APP_URL` (as resolved by this class's environment helper) + * with `/auth/google/callback` appended. */ public function withGoogle( string $clientId, string $clientSecret, - array $options = [] - ) { + array $options = [], + ): static { $clientName = $options['name'] ?? 'google'; unset($options['name']); - if (!isset($options['redirectUri'])) { - $options['redirectUri'] = $this->env('APP_URL') . '/auth/google/callback'; - } + $options['redirectUri'] ??= $this->env('APP_URL') . '/auth/google/callback'; $this->withProvider($clientName, new Google(array_merge([ 'clientId' => $clientId, @@ -220,24 +194,17 @@ public function withGoogle( } /** - * Register a generic OAuth client - * --- - * Register a generic OAuth client to use with Leaf Auth, should be a league/oauth2-client compatible client. - * @param string $clientName The name of the client to register + * Register a generic OAuth client to use with Leaf Auth, should be a + * league/oauth2-client compatible client. * @param AbstractProvider $client An instance of a league/oauth2-client compatible client - * @return static */ - public function withProvider(string $clientName, $client) + public function withProvider(string $clientName, $client): static { $this->oauthClients[$clientName] = $client; return $this; } - /** - * Return an oauth client - * @param string $clientName The name of the client to return - * @return AbstractProvider|null - */ + /** @return AbstractProvider|null An oauth client */ public function client(string $clientName) { return $this->oauthClients[$clientName] ?? null; @@ -245,12 +212,11 @@ public function client(string $clientName) /** * Get/Set Leaf Auth config - * - * @param string|array $config The auth config key or array of config + * @param string|array $config The auth config key or + * array of config * @param mixed $value The value if $config is a string - * @return mixed|void */ - public function config($config, $value = null) + public function config($config, mixed $value = null): mixed { if (is_string($config) && $value === null) { return Config::get($config); @@ -261,15 +227,16 @@ public function config($config, $value = null) ? [$config => $value] : $config ); + + return null; } /** * Create roles and permissions - * - * @param array $roles Array of roles and their permissions - * @return Auth + * @param array $roles Array of roles and their + * permissions */ - public function createRoles(array $roles) + public function createRoles(array $roles): static { Config::set([ 'roles' => $roles, @@ -278,24 +245,13 @@ public function createRoles(array $roles) return $this; } - /** - * Return all roles and their permissions - * - * @return array - */ - public function roles() + /** Return all roles and their permissions */ + public function roles(): ?array { return Config::get('roles'); } - /** - * Sign a user in - * --- - * Verify user credentials and sign them in with token or session - * - * @param array $credentials User credentials - * @return bool - */ + /** Verify user credentials and sign them in with token or session */ public function login(array $credentials): bool { $this->errorsArray = []; @@ -322,8 +278,8 @@ public function login(array $credentials): bool $this->errorsArray['auth'] = Config::get('messages.loginParamsError'); return false; } - } catch (Throwable $th) { - throw new Exception($th->getMessage()); + } catch (Throwable $throwable) { + throw new Exception($throwable->getMessage(), $throwable->getCode(), $throwable); } if ($passwordKey !== false) { @@ -344,14 +300,7 @@ public function login(array $credentials): bool return true; } - /** - * Register a new user - * --- - * Save a new user to the database - * - * @param array $userData User data - * @return bool - */ + /** Save a new user to the database */ public function register(array $userData): bool { // each operation owns its error state: stale errors from a @@ -393,8 +342,8 @@ public function register(array $userData): bool $this->errorsArray = array_merge($this->errorsArray, $this->db->errors()); return false; } - } catch (Throwable $th) { - throw new Exception($th->getMessage()); + } catch (Throwable $throwable) { + throw new Exception($throwable->getMessage(), $throwable->getCode(), $throwable); } $user = $this->db->select($table)->where($userData)->first(); @@ -409,14 +358,7 @@ public function register(array $userData): bool return true; } - /** - * Update user data - * --- - * Update user data in the database - * - * @param array $userData User data - * @return bool - */ + /** Update user data in the database */ public function update(array $userData): bool { // each operation owns its error state: stale errors from a @@ -467,8 +409,8 @@ public function update(array $userData): bool $this->errorsArray = array_merge($this->errorsArray, $this->db->errors()); return false; } - } catch (Throwable $th) { - throw new Exception($th->getMessage()); + } catch (Throwable $throwable) { + throw new Exception($throwable->getMessage(), $throwable->getCode(), $throwable); } if (Config::get('session')) { @@ -482,15 +424,7 @@ public function update(array $userData): bool return true; } - /** - * Update user password - * --- - * Update user password in the database - * - * @param string $oldPassword Old password - * @param string $newPassword New password - * @return bool - */ + /** Update user password in the database */ public function updatePassword(string $oldPassword, string $newPassword): bool { $this->checkDbConnection(); @@ -530,8 +464,8 @@ public function updatePassword(string $oldPassword, string $newPassword): bool $this->errorsArray = array_merge($this->errorsArray, $this->db->errors()); return false; } - } catch (Throwable $th) { - throw new Exception($th->getMessage()); + } catch (Throwable $throwable) { + throw new Exception($throwable->getMessage(), $throwable->getCode(), $throwable); } $this->user->{$passwordKey} = $newPassword; @@ -539,19 +473,13 @@ public function updatePassword(string $oldPassword, string $newPassword): bool return true; } - /** - * Create a new user from OAuth - * - * @param array $userData User data - * - * @return bool - */ + /** Create a new user from OAuth */ public function fromOAuth(array $userData): bool { $initialPassword = Config::get('password.key'); $this->checkDbConnection(); - $this->config('password.key', false); + $this->config('password.key', value: false); $user = $this->db->select(Config::get('db.table')) ->where([ @@ -575,14 +503,10 @@ public function fromOAuth(array $userData): bool } /** - * Find a user by id - * --- - * Select and return an existing user from db - * + * Select and return an existing user from db by id * @param string|int $id The id of the user to grab - * @return User|null */ - public function find($id) + public function find($id): ?User { $this->checkDbConnection(); @@ -592,19 +516,15 @@ public function find($id) return null; } - return (new User($userData, false))->setDb($this->db); + return (new User($userData, session: false))->setDb($this->db); } /** - * Create a new user - * --- * Create an account for another user - * * @param array $userData The user details to save - * @return User|false|never * @throws Exception If database connection is not established */ - public function createUserFor($userData) + public function createUserFor($userData): false|User { // each operation owns its error state: stale errors from a // previous call must never leak into this result @@ -645,8 +565,8 @@ public function createUserFor($userData) $this->errorsArray = array_merge($this->errorsArray, $this->db->errors()); return false; } - } catch (Throwable $th) { - throw new Exception($th->getMessage()); + } catch (Throwable $throwable) { + throw new Exception($throwable->getMessage(), $throwable->getCode(), $throwable); } $user = $this->db->select($table)->where($userData)->first(); @@ -656,25 +576,19 @@ public function createUserFor($userData) return false; } - return (new User($user, false))->setDb($this->db); + return (new User($user, session: false))->setDb($this->db); } - /** - * Get saved OAuth token - * @return mixed - */ - public function oauthToken() + /** Get saved OAuth token */ + public function oauthToken(): mixed { return Config::getUserCache('oauth-token'); } /** - * Sign a user out - * --- * Sign out the currently authenticated user - * - * @param string|mixed[]|callable|null $action Redirect to this url after logout - * @return bool + * @param string|mixed[]|callable|null $action Redirect to this url + * after logout */ public function logout($action = null): bool { @@ -697,22 +611,16 @@ public function logout($action = null): bool return true; } - /** - * Get the id of the currently authenticated user - * @return string|int - */ - public function id() + /** Get the id of the currently authenticated user */ + public function id(): string|int { return Config::get('session') ? $this->getFromSession('auth.id') : ($this->user ? $this->user->id() : ($this->parseToken()['user.id'] ?? null)); } - /** - * Get the currently authenticated user - * @return User|null - */ - public function user() + /** Get the currently authenticated user */ + public function user(): ?User { if (Config::get('session')) { $userId = $this->getFromSession('auth.id'); @@ -744,8 +652,8 @@ public function user() $this->errorsArray = $this->db->errors(); return null; } - } catch (Throwable $th) { - throw new Exception($th->getMessage()); + } catch (Throwable $throwable) { + throw new Exception($throwable->getMessage(), $throwable->getCode(), $throwable); } return $this->user = (new User( @@ -753,11 +661,8 @@ public function user() ))->setDb($this->db); } - /** - * Get data generated on user login - * @return object|null - */ - public function data() + /** Get data generated on user login */ + public function data(): ?object { $user = $this->user(); @@ -768,14 +673,8 @@ public function data() return $user->getAuthInfo(); } - /** - * Get generated access tokens - * @return array{ - * access?: string, - * refresh?: string, - * }|null - */ - public function tokens() + /** Get generated access tokens */ + public function tokens(): ?array { $user = $this->user(); @@ -790,17 +689,16 @@ public function tokens() * Register auth middleware for your Leaf apps * @param string $middleware The middleware to register * @param callable $callback The callback to run if middleware fails - * @return void|never * @throws Exception If not used with leafs/leaf installed */ - public function middleware(string $middleware, callable $callback) + public function middleware(string $middleware, callable $callback): void { if (!class_exists(App::class)) { throw new Exception('This feature is only available for Leaf apps'); } if ($middleware === 'auth.required') { - app()->registerMiddleware('auth.required', function () use ($callback) { + app()->registerMiddleware('auth.required', function () use ($callback): void { if (!$this->user()) { $callback(); exit; @@ -811,7 +709,7 @@ public function middleware(string $middleware, callable $callback) } if ($middleware === 'auth.guest') { - app()->registerMiddleware('auth.guest', function () use ($callback) { + app()->registerMiddleware('auth.guest', function () use ($callback): void { if ($this->user()) { $callback(); exit; @@ -824,7 +722,7 @@ public function middleware(string $middleware, callable $callback) } if ($middleware === 'is') { - app()->registerMiddleware('is', function ($role) use ($callback) { + app()->registerMiddleware('is', function ($role) use ($callback): void { if (!$this->user() || $this->user()->isNot($role)) { $callback($role); exit; @@ -835,7 +733,7 @@ public function middleware(string $middleware, callable $callback) } if ($middleware === 'isNot') { - app()->registerMiddleware('isNot', function ($role) use ($callback) { + app()->registerMiddleware('isNot', function ($role) use ($callback): void { if (!$this->user() || $this->user()->is($role)) { $callback($role); exit; @@ -846,7 +744,7 @@ public function middleware(string $middleware, callable $callback) } if ($middleware === 'can') { - app()->registerMiddleware('can', function ($role) use ($callback) { + app()->registerMiddleware('can', function ($role) use ($callback): void { if (!$this->user() || $this->user()->cannot($role)) { $callback($role); exit; @@ -857,7 +755,7 @@ public function middleware(string $middleware, callable $callback) } if ($middleware === 'cannot') { - app()->registerMiddleware('cannot', function ($role) use ($callback) { + app()->registerMiddleware('cannot', function ($role) use ($callback): void { if (!$this->user() || $this->user()->can($role)) { $callback($role); exit; @@ -868,7 +766,7 @@ public function middleware(string $middleware, callable $callback) } if ($middleware === 'auth.verified') { - app()->registerMiddleware('auth.verified', function () use ($callback) { + app()->registerMiddleware('auth.verified', function () use ($callback): void { if (!$this->user() || !$this->user()->isVerified()) { $callback(); exit; @@ -879,7 +777,7 @@ public function middleware(string $middleware, callable $callback) } if ($middleware === 'auth.unverified') { - app()->registerMiddleware('auth.unverified', function () use ($callback) { + app()->registerMiddleware('auth.unverified', function () use ($callback): void { if (!$this->user() || $this->user()->isVerified()) { $callback(); exit; @@ -892,11 +790,8 @@ public function middleware(string $middleware, callable $callback) app()->registerMiddleware($middleware, $callback); } - /** - * Parse the current user's token - * @return array|null - */ - public function parseToken() + /** Parse the current user's token */ + public function parseToken(): ?array { $bearerToken = $this->getTokenFromRequest(); @@ -909,18 +804,14 @@ public function parseToken() $bearerToken, new Key(Config::tokenSecret(), 'HS256') ); - } catch (Throwable $th) { - $this->errorsArray['token'] = $th->getMessage(); + } catch (Throwable $throwable) { + $this->errorsArray['token'] = $throwable->getMessage(); return null; } } - /** - * Verify a user's token - * @param string $token The token to verify - * @return User|null - */ - public function verifyToken(string $token, ?string $purpose = null) + /** Verify a user's token */ + public function verifyToken(string $token, ?string $purpose = null): ?User { try { $decodedToken = (array) JWT::decode( @@ -935,7 +826,7 @@ public function verifyToken(string $token, ?string $purpose = null) $user = $this->find($decodedToken['user.id']); - if (!$user) { + if (!$user instanceof User) { $this->errorsArray['token'] = 'User not found'; return null; } @@ -951,18 +842,14 @@ public function verifyToken(string $token, ?string $purpose = null) } return $user; - } catch (Throwable $th) { - $this->errorsArray['token'] = $th->getMessage(); + } catch (Throwable $throwable) { + $this->errorsArray['token'] = $throwable->getMessage(); return null; } } - /** - * Return the current db instance - * - * @return Db - */ - public function db() + /** Return the current db instance */ + public function db(): ?Db { return $this->db; } @@ -981,11 +868,8 @@ protected function checkDbConnection(): void } } - /** - * @param string|array $value - * @return ?mixed - */ - protected function getFromSession($value) + /** @param string|array $value */ + protected function getFromSession($value): mixed { if ($this->checkAndExpireSession()) { return null; @@ -994,11 +878,8 @@ protected function getFromSession($value) return Session::get($value); } - /** - * @return void|never - * @throws Exception If sessions are not enabled - */ - protected function sessionCheck() + /** @throws Exception If sessions are not enabled */ + protected function sessionCheck(): void { if (!Config::get('session')) { throw new Exception('Turn on sessions to use this feature.'); @@ -1022,8 +903,7 @@ protected function checkAndExpireSession(): bool return $isSessionExpired; } - /** @return ?string */ - protected function getTokenFromRequest() + protected function getTokenFromRequest(): ?string { $headers = null; @@ -1034,7 +914,7 @@ protected function getTokenFromRequest() } elseif (function_exists('apache_request_headers')) { $requestHeaders = apache_request_headers(); // Server-side fix for bug in old Android versions (a nice side-effect of this fix means we don't care about capitalization for Authorization) - $requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders)); + $requestHeaders = array_combine(array_map(ucwords(...), array_keys($requestHeaders)), array_values($requestHeaders)); if (isset($requestHeaders['Authorization'])) { $headers = trim($requestHeaders['Authorization']); @@ -1052,25 +932,18 @@ protected function getTokenFromRequest() return null; } - /** @return ?mixed */ - protected function getTokenFromSession() + protected function getTokenFromSession(): mixed { return Session::get('auth.token'); } - /** - * Clear all errors caught - * @return void - */ - public function clearErrors() + /** Clear all errors caught */ + public function clearErrors(): void { $this->errorsArray = []; } - /** - * Return all errors caught - * @return array - */ + /** Return all errors caught */ public function errors(): array { return $this->errorsArray; diff --git a/src/Auth/Config.php b/src/Auth/Config.php index fb561d1..941c8e2 100644 --- a/src/Auth/Config.php +++ b/src/Auth/Config.php @@ -1,7 +1,11 @@ Configuration for Leaf Auth */ + /** Configuration for Leaf Auth */ protected static array $config = [ 'id.key' => 'id', 'db.table' => 'users', @@ -80,7 +83,7 @@ public static function tokenSecret(): string 'messages.loginPasswordError' => 'Password is incorrect!', ]; - /** @var array Additional user information for cache */ + /** Additional user information for cache */ protected static array $userCache = []; /** @@ -118,9 +121,8 @@ public static function get($key = null) /** * Set user cache * @param string $key - * @param mixed $value */ - public static function setUserCache($key, $value): void + public static function setUserCache($key, mixed $value): void { if (isset($_SESSION)) { $_SESSION[$key] = $value; diff --git a/src/Auth/Model.php b/src/Auth/Model.php index c5cc61d..036dfb3 100644 --- a/src/Auth/Model.php +++ b/src/Auth/Model.php @@ -1,5 +1,7 @@ User data to save */ + /** User data to save */ protected array $dataToSave = []; - /** - * @param array{db: Db, user: User, table: string} $data - */ + /** @param array{db: Db, user: User, table: string} $data */ public function __construct($data) { $this->db = $data['db']; @@ -46,10 +40,7 @@ public function __construct($data) /** * Create a new model resource - * * @param array $data Data to be inserted - * - * @return PDOStatement|null */ public function create(array $data): ?PDOStatement { @@ -66,10 +57,7 @@ public function create(array $data): ?PDOStatement /** * Update a model resource - * * @param array $data Data to be updated - * - * @return Db */ public function update(array $data): Db { @@ -80,11 +68,7 @@ public function update(array $data): Db ->where('user_id', $this->user->id()); } - /** - * Delete a model resource - * - * @return Db - */ + /** Delete a model resource */ public function delete(): Db { return $this->db->delete($this->table) @@ -93,10 +77,7 @@ public function delete(): Db /** * Get a model resource - * * @param string $columns Columns to search by separated by commas or * - * - * @return Db */ public function table($columns = '*'): Db { @@ -104,11 +85,7 @@ public function table($columns = '*'): Db ->where('user_id', $this->user->id()); } - /** - * Save a model resource - * - * @return PDOStatement|null - */ + /** Save a model resource */ public function save(): ?PDOStatement { $success = $this->create($this->dataToSave); @@ -118,11 +95,8 @@ public function save(): ?PDOStatement return $success; } - /** - * @param string $name - * @param mixed $value - */ - public function __set($name, $value) + /** @param string $name */ + public function __set($name, mixed $value) { $this->dataToSave[$name] = $value; } @@ -130,9 +104,8 @@ public function __set($name, $value) /** * @param string $name * @param mixed[] $arguments - * @return mixed */ - public function __call($name, $arguments) + public function __call($name, $arguments): mixed { return $this->table()->$name(...$arguments); } diff --git a/src/Auth/User.php b/src/Auth/User.php index 63a60d8..5742cd2 100644 --- a/src/Auth/User.php +++ b/src/Auth/User.php @@ -1,10 +1,13 @@ User Information */ + /** User Information */ protected array $data = []; - /** - * @var array{ - * access?: string, - * refresh?: string, - * } User Tokens - */ + /** @var array{access?: string, refresh?: string} User Tokens */ protected array $tokens = []; - /** - * Expiry used when tokens are lazily minted - * @var int - */ + /** @var int Expiry used when tokens are lazily minted */ protected $tokenLifetime; - /** - * All errors caught - * @var array{ - * password?: string, - * } | array - */ - protected $errorsArray = []; + /** All errors caught */ + protected array $errorsArray = []; /** * @param array $data @@ -119,34 +103,22 @@ public function __construct($data, $session = true) } if ($data[Config::get('roles.key')] ?? null) { - $this->setRolesAndPermissions(json_decode($data[Config::get('roles.key')], true)); + $this->setRolesAndPermissions(json_decode($data[Config::get('roles.key')], associative: true)); } } - /** - * Return the id of current user - * @return string|int - */ - public function id() + /** Return the id of current user */ + public function id(): null|string|int { return $this->data[Config::get('id.key')] ?? null; } - /** - * Update user data - * --- - * Update user data in the database - * - * @param array{ - * email?: string, - * } | array $userData User data - * @return bool - */ + /** Update user data in the database */ public function update(array $userData): bool { $user = $this->get(); - if (!$user) { + if ($user === []) { return false; } @@ -186,8 +158,8 @@ public function update(array $userData): bool $this->errorsArray = array_merge($this->errorsArray, $this->db->errors()); return false; } - } catch (Throwable $th) { - throw new Exception($th->getMessage()); + } catch (Throwable $throwable) { + throw new Exception($throwable->getMessage(), $throwable->getCode(), $throwable); } if (Config::get('session')) { @@ -201,20 +173,12 @@ public function update(array $userData): bool return true; } - /** - * Update user password - * --- - * Update user password in the database - * - * @param string $oldPassword Old password - * @param string $newPassword New password - * @return bool - */ + /** Update user password in the database */ public function updatePassword(string $oldPassword, string $newPassword): bool { $user = $this->get(); - if (!$user) { + if ($user === []) { return false; } @@ -247,8 +211,8 @@ public function updatePassword(string $oldPassword, string $newPassword): bool $this->errorsArray = array_merge($this->errorsArray, $this->db->errors()); return false; } - } catch (Throwable $th) { - throw new Exception($th->getMessage()); + } catch (Throwable $throwable) { + throw new Exception($throwable->getMessage(), $throwable->getCode(), $throwable); } $this->data[$passwordKey] = $newPassword; @@ -256,19 +220,12 @@ public function updatePassword(string $oldPassword, string $newPassword): bool return true; } - /** - * Reset user password - * --- - * Reset user password in the database - * - * @param string $newPassword New password - * @return bool - */ + /** Reset user password in the database */ public function resetPassword(string $newPassword): bool { $user = $this->get(); - if (!$user) { + if ($user === []) { return false; } @@ -289,8 +246,8 @@ public function resetPassword(string $newPassword): bool $this->errorsArray = array_merge($this->errorsArray, $this->db->errors()); return false; } - } catch (Throwable $th) { - throw new Exception($th->getMessage()); + } catch (Throwable $throwable) { + throw new Exception($throwable->getMessage(), $throwable->getCode(), $throwable); } $this->data[$passwordKey] = $newPassword; @@ -298,10 +255,7 @@ public function resetPassword(string $newPassword): bool return true; } - /** - * Get auth information to be sent to the client - * @return object - */ + /** Get auth information to be sent to the client */ public function getAuthInfo(): object { $tokens = $this->tokens(); @@ -323,13 +277,7 @@ public function getAuthInfo(): object return $dataToReturn; } - /** - * Return generated tokens - * @return array{ - * access?: string, - * refresh?: string, - * } - */ + /** Return generated tokens */ public function tokens(): array { if (!isset($this->tokens['access'])) { @@ -343,7 +291,6 @@ public function tokens(): array /** * Generate a new JWT for the user * @param int $tokenLifetime - * @return string */ public function generateToken($tokenLifetime): string { @@ -363,8 +310,6 @@ public function generateToken($tokenLifetime): string /** * Generate a verification token for the user * @param mixed $expiresIn Token expiration time - * @param string|null $purpose Purpose of the token - * @return string */ public function generateVerificationToken($expiresIn = null, ?string $purpose = null): string { @@ -386,19 +331,13 @@ public function generateVerificationToken($expiresIn = null, ?string $purpose = return JWT::encode($payload, $secretPhrase, 'HS256'); } - /** - * Check if email is verified - * @return bool - */ + /** Check if email is verified */ public function isVerified(): bool { - return !!($this->data['email_verified_at'] ?? false); + return (bool) ($this->data['email_verified_at'] ?? false); } - /** - * Verify user's email - * @return bool - */ + /** Verify user's email */ public function verifyEmail(): bool { if ($this->isVerified()) { @@ -418,13 +357,12 @@ public function verifyEmail(): bool ->execute(); return true; - } catch (Throwable $th) { + } catch (Throwable) { return false; } } - /** @return array */ - public function get() + public function get(): array { $userData = $this->data; @@ -434,19 +372,17 @@ public function get() unset($userData[$passwordKey], $userData['remember_token']); - if (count($hidden) > 0) { - foreach ($hidden as $item) { - if (array_key_exists($item, $userData)) { - unset($userData[$item]); - } + foreach ($hidden as $item) { + if (array_key_exists($item, $userData)) { + unset($userData[$item]); + } - if ($item === 'field.id' && array_key_exists($idKey, $userData)) { - unset($userData[$idKey]); - } + if ($item === 'field.id' && array_key_exists($idKey, $userData)) { + unset($userData[$idKey]); + } - if ($item === 'field.password' && array_key_exists($passwordKey, $userData)) { - unset($userData[$passwordKey]); - } + if ($item === 'field.password' && array_key_exists($passwordKey, $userData)) { + unset($userData[$passwordKey]); } } @@ -456,37 +392,27 @@ public function get() /** * Set user db instance * @param Db $db - * @return User */ - public function setDb($db) + public function setDb($db): static { $this->db = $db; return $this; } - /** - * Get user errors - * @return array{ - * password?: string, - * } | array - */ - public function errors() + /** Get user errors */ + public function errors(): array { return $this->errorsArray; } - #[ReturnTypeWillChange] - public function __toString() + public function __toString(): string { return json_encode($this->get()) ?: ''; } - /** - * @param string $name - * @return ?mixed - */ - public function __get($name) + /** @param string $name */ + public function __get($name): mixed { // using data instead of get() here because // we want people to be able to user()->get hidden fields @@ -494,53 +420,37 @@ public function __get($name) return $this->data[$name] ?? null; } - /** - * @param string $name - * @param mixed $value - * @return void - */ - public function __set($name, $value) + /** @param string $name */ + public function __set($name, mixed $value): void { $this->data[$name] = $value; } - /** - * @param string $name - * @return bool - */ - public function __isset($name) + /** @param string $name */ + public function __isset($name): bool { return isset($this->data[$name]); } - /** - * @param string $name - * @return void - */ - public function __unset($name) + /** @param string $name */ + public function __unset($name): void { unset($this->data[$name]); } /** * Get a "user to many" table relation - * - * + * ``` * auth()->user()->orders()->all(); * auth()->user()->transactions()->where('amount', '>', 100)->get(); * auth()->user()->notes()->where('title', 'like', '%important%')->get(); * auth()->user()->posts()->where('published', true)->all(); - * - * - * @param mixed $method The table to relate to - * @param mixed $args - * @throws Exception - * - * @return Model + * ``` + * @throws Exception If Leaf App class does not exist */ - public function __call($method, $args) + public function __call(mixed $method, mixed $args): Model { - if (!class_exists('Leaf\App')) { + if (!class_exists(App::class)) { throw new Exception('Relations are only available in Leaf apps.'); } diff --git a/src/Auth/UsesRoles.php b/src/Auth/UsesRoles.php index 8353304..d0cfe05 100644 --- a/src/Auth/UsesRoles.php +++ b/src/Auth/UsesRoles.php @@ -1,5 +1,7 @@ data)) { try { $this->db->query('ALTER TABLE ' . Config::get('db.table') . " ADD COLUMN $roleKey TEXT")->execute(); - } catch (Throwable $th) { + } catch (Throwable) { // column already exists in the table, it just wasn't selected } } @@ -59,7 +59,7 @@ public function assign($role): bool ]) ->where(Config::get('id.key'), $this->data[Config::get('id.key')]) ->execute(); - } catch (Throwable $th) { + } catch (Throwable) { return false; } @@ -69,7 +69,6 @@ public function assign($role): bool /** * Check if user has a permission * @param string|string[] $permission The permission(s) to check - * @return bool */ public function can($permission): bool { @@ -92,7 +91,6 @@ public function cannot($permission): bool /** * Check if user has a role * @param string|string[] $role The role(s) to check - * @return bool */ public function is($role): bool { @@ -112,19 +110,13 @@ public function isNot($role): bool return !$this->is($role); } - /** - * Return the user's roles - * @return string[] - */ + /** Return the user's roles */ public function roles(): array { return $this->roles; } - /** - * Return the user's permissions - * @return string[] - */ + /** Return the user's permissions */ public function permissions(): array { return $this->permissions; @@ -154,9 +146,7 @@ public function unassign($role): void /** * Return any of the given roles that haven't been registered - * * @param string|string[] $roles The role(s) to check - * @return string[] */ protected function unknownRoles($roles): array { @@ -164,15 +154,12 @@ protected function unknownRoles($roles): array return array_values(array_filter( is_string($roles) ? [$roles] : $roles, - function ($role) use ($registered) { - return !array_key_exists($role, $registered); - } + fn ($role): bool => !array_key_exists($role, $registered) )); } /** * Set the roles and permissions for a user - * * @param string|string[] $roles The role(s) to set */ protected function setRolesAndPermissions($roles): void @@ -198,9 +185,7 @@ protected function setRolesAndPermissions($roles): void /** * Get the permissions for a role - * * @param string|string[] $roles - * @return string[] */ protected function getRolePermissions($roles): array { diff --git a/src/Auth/UsesSubscriptions.php b/src/Auth/UsesSubscriptions.php index 726ab9e..c70ccc9 100644 --- a/src/Auth/UsesSubscriptions.php +++ b/src/Auth/UsesSubscriptions.php @@ -1,7 +1,10 @@ |null - */ - protected $subscription = null; + /** @var ?array User Subscription */ + protected $subscription; /** * Get current subscription * * The most recent subscription is returned, so a resubscribed user * gets their new subscription rather than an older cancelled one. - * - * @return mixed[]|null */ public function subscription(): ?array { @@ -45,13 +43,10 @@ public function subscription(): ?array $this->subscription['tier'] = billing()->tier($this->subscription['plan_id']); } - return $this->subscription ? $this->subscription : null; + return $this->subscription ?: null; } - /** - * Check if user has a subscription - * @return bool - */ + /** Check if user has a subscription */ public function hasSubscription(): bool { return $this->subscription() && $this->subscription['status'] !== Subscription::STATUS_CANCELLED; @@ -62,8 +57,6 @@ public function hasSubscription(): bool * * Users on a trial, and users who cancelled but are still inside the * period they paid for (grace period), still count as active. - * - * @return bool */ public function hasActiveSubscription(): bool { @@ -86,9 +79,7 @@ public function hasActiveSubscription(): bool * Matches on the subscription name or on the billing tier's name or id, * so `isSubscribedTo('Starter')` works whether you named the * subscription after the tier or not. - * * @param string $plan The plan name or tier id to check - * @return bool */ public function isSubscribedTo(string $plan): bool { @@ -104,7 +95,7 @@ public function isSubscribedTo(string $plan): bool $tier = $subscription['tier'] ?? null; - if ($tier instanceof \Leaf\Billing\Tier) { + if ($tier instanceof Tier) { $tier = $tier->toArray(); } @@ -115,10 +106,7 @@ public function isSubscribedTo(string $plan): bool return ($tier['name'] ?? null) === $plan || ($tier['id'] ?? null) === $plan; } - /** - * Check if user is on a trial - * @return bool - */ + /** Check if user is on a trial */ public function onTrial(): bool { if (!$this->subscription()) { @@ -134,10 +122,7 @@ public function onTrial(): bool return $trialEndsAt && strtotime($trialEndsAt) > time(); } - /** - * Check if user cancelled but still has access until the period ends - * @return bool - */ + /** Check if user cancelled but still has access until the period ends */ public function onGracePeriod(): bool { if (!$this->subscription()) { @@ -153,7 +138,6 @@ public function onGracePeriod(): bool /** * Check if subscription payment is past due (renewal failed, in dunning) - * @return bool */ public function hasPastDueSubscription(): bool { @@ -166,9 +150,6 @@ public function hasPastDueSubscription(): bool * By default the subscription is cancelled at the end of the current * billing period, so the user keeps access to what they paid for. * Pass false to cancel and revoke access immediately. - * - * @param bool $atPeriodEnd Cancel at period end instead of immediately - * @return bool */ public function cancelSubscription(bool $atPeriodEnd = true): bool { @@ -184,8 +165,6 @@ public function cancelSubscription(bool $atPeriodEnd = true): bool /** * Resume a subscription that was cancelled at period end * but has not run out yet. - * - * @return bool */ public function resumeSubscription(): bool { diff --git a/src/functions.php b/src/functions.php index fca0b34..3d62d4c 100644 --- a/src/functions.php +++ b/src/functions.php @@ -1,20 +1,17 @@ new Auth()); } return Config::get('auth'); diff --git a/tests/Pest.php b/tests/Pest.php index 8111b01..67d3a46 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -66,7 +66,7 @@ function authInstance(): Auth return $auth; } -function deleteUser(string $username, $table = 'users') +function deleteUser(string $username, string $table = 'users'): void { $db = dbInstance(); @@ -79,9 +79,8 @@ function createTableForUsers($table = 'users', bool $uuid = false): void $idColumn = $uuid ? 'id TEXT PRIMARY KEY' : null; try { - switch ($_ENV['DB_CONNECTION']) { - case 'sqlite': - $sql = "CREATE TABLE IF NOT EXISTS $table ( + $sql = match ($_ENV['DB_CONNECTION']) { + 'sqlite' => "CREATE TABLE IF NOT EXISTS $table ( " . ($idColumn ?? 'id INTEGER PRIMARY KEY AUTOINCREMENT') . ", username TEXT NOT NULL, email TEXT NOT NULL, @@ -90,11 +89,8 @@ function createTableForUsers($table = 'users', bool $uuid = false): void roles TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - )"; - break; - - case 'mysql': - $sql = "CREATE TABLE IF NOT EXISTS $table ( + )", + 'mysql' => "CREATE TABLE IF NOT EXISTS $table ( " . ($idColumn ?? 'id SERIAL PRIMARY KEY') . ", username VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, @@ -103,11 +99,8 @@ function createTableForUsers($table = 'users', bool $uuid = false): void roles JSON, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - )"; - break; - - default: - $sql = "CREATE TABLE IF NOT EXISTS $table ( + )", + default => "CREATE TABLE IF NOT EXISTS $table ( " . ($idColumn ?? 'id SERIAL PRIMARY KEY') . ", username VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, @@ -116,11 +109,11 @@ function createTableForUsers($table = 'users', bool $uuid = false): void roles JSONB, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - )"; - } + )", + }; $db->query($sql)->execute(); - } catch (Throwable $th) { - throw new Exception('Failed to create table for users: ' . $th->getMessage()); + } catch (Throwable $throwable) { + throw new Exception('Failed to create table for users: ' . $throwable->getMessage(), $throwable->getCode(), $throwable); } } diff --git a/tests/google.test.php b/tests/google.test.php index a857434..1723f8c 100644 --- a/tests/google.test.php +++ b/tests/google.test.php @@ -47,10 +47,6 @@ function expect($value = null): Expectation $clientSecret = $reflectionClass->getProperty('clientSecret'); $redirectUri = $reflectionClass->getProperty('redirectUri'); - $clientId->setAccessible(true); - $clientSecret->setAccessible(true); - $redirectUri->setAccessible(true); - expect($clientId->getValue($google)) ->toBe($_ENV['GOOGLE_AUTH_CLIENT_ID']); @@ -77,8 +73,6 @@ function expect($value = null): Expectation $reflectionClass = new ReflectionClass(Google::class); $redirectUri = $reflectionClass->getProperty('redirectUri'); - $redirectUri->setAccessible(true); - expect($redirectUri->getValue($google)) ->toBe("{$_ENV['APP_URL']}/auth/register/google"); }); @@ -97,8 +91,6 @@ function expect($value = null): Expectation $reflectionClass = new ReflectionClass(Google::class); $redirectUri = $reflectionClass->getProperty('redirectUri'); - $redirectUri->setAccessible(true); - expect($redirectUri->getValue($google)) ->toBe("{$_ENV['APP_URL']}/auth/google/callback"); }); diff --git a/tests/login.test.php b/tests/login.test.php index 1b7a6a8..fc1413a 100644 --- a/tests/login.test.php +++ b/tests/login.test.php @@ -2,28 +2,24 @@ use Leaf\Auth\User; -beforeAll(function () { +beforeAll(function (): void { createTableForUsers(); - try { - dbInstance() - ->insert('users') - ->params([ - 'username' => 'test-user', - 'email' => 'test-user@example.com', - 'password' => password_hash('password', PASSWORD_BCRYPT) - ]) - ->execute(); - } catch (Throwable $th) { - throw $th; - } + dbInstance() + ->insert('users') + ->params([ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => password_hash('password', PASSWORD_BCRYPT) + ]) + ->execute(); }); -afterAll(function () { +afterAll(function (): void { dbInstance()->delete('users')->execute(); }); -test('user can login', function () { +test('user can login', function (): void { $auth = authInstance(); $testUser = [ @@ -39,7 +35,7 @@ expect($auth->user()->username)->toBe($testUser['username']); }); -test('login generates tokens on success', function () { +test('login generates tokens on success', function (): void { $auth = authInstance(); $testUser = [ @@ -56,7 +52,7 @@ expect($auth->data()->refreshToken)->toBeString(); }); -test('login fails with incorrect password', function () { +test('login fails with incorrect password', function (): void { $auth = authInstance(); $userData = [ @@ -73,7 +69,7 @@ expect($auth->errors()['password'] ?? null)->toBe($loginPasswordError); }); -test('login should fail if user does not exist', function () { +test('login should fail if user does not exist', function (): void { $auth = authInstance(); $userData = [ @@ -90,10 +86,10 @@ expect($auth->errors()['auth'] ?? null)->toBe($loginParamsError); }); -test('login should work without password is password.key is false', function () { +test('login should work without password is password.key is false', function (): void { $auth = authInstance(); - $auth->config('password.key', false); + $auth->config('password.key', value: false); $userData = [ 'username' => 'test-user' diff --git a/tests/register.test.php b/tests/register.test.php index d87cb91..9ca5591 100644 --- a/tests/register.test.php +++ b/tests/register.test.php @@ -2,16 +2,16 @@ use Leaf\Auth\User; -beforeAll(function () { +beforeAll(function (): void { createTableForUsers(); dbInstance()->delete('users')->execute(); }); -afterEach(function () { +afterEach(function (): void { dbInstance()->delete('users')->execute(); }); -test('user can register an account', function () { +test('user can register an account', function (): void { $auth = authInstance(); $auth->config(['db.table' => 'users']); @@ -32,7 +32,7 @@ expect($auth->user()->username)->toBe($userData['username']); }); -test('user can login after registering', function () { +test('user can login after registering', function (): void { $auth = authInstance(); $auth->config(['db.table' => 'users']); @@ -53,7 +53,7 @@ expect($auth->user()->username)->toBe($userData['username']); }); -test('user can only sign up once', function () { +test('user can only sign up once', function (): void { $auth = authInstance(); $auth->config(['db.table' => 'users']); @@ -81,7 +81,7 @@ ]); }); -test('register passwords are encrypted', function () { +test('register passwords are encrypted', function (): void { $auth = authInstance(); $auth->config(['db.table' => 'users']); diff --git a/tests/roles.test.php b/tests/roles.test.php index ab09752..60f19b6 100644 --- a/tests/roles.test.php +++ b/tests/roles.test.php @@ -1,5 +1,6 @@ delete('role_users')->execute(); }); -beforeEach(function () { +beforeEach(function (): void { deleteUser('role-user', 'role_users'); $this->auth = roleAuthInstance(); @@ -41,11 +42,11 @@ function roleAuthInstance(): Auth ]); }); -test('createRoles registers roles and their permissions', function () { +test('createRoles registers roles and their permissions', function (): void { expect($this->auth->roles())->toBe(ROLES); }); -test('a fresh user has no roles or permissions', function () { +test('a fresh user has no roles or permissions', function (): void { $user = $this->auth->user(); expect($user->roles())->toBe([]); @@ -54,7 +55,7 @@ function roleAuthInstance(): Auth expect($user->can('view-ticket'))->toBeFalse(); }); -test('assign gives a user a role and its permissions', function () { +test('assign gives a user a role and its permissions', function (): void { $user = $this->auth->user(); expect($user->assign('admin'))->toBeTrue(); @@ -65,7 +66,7 @@ function roleAuthInstance(): Auth expect($user->cannot('nonexistent-permission'))->toBeTrue(); }); -test('assign accepts multiple roles and merges permissions without duplicates', function () { +test('assign accepts multiple roles and merges permissions without duplicates', function (): void { $user = $this->auth->user(); $user->assign(['support', 'guest']); @@ -74,11 +75,11 @@ function roleAuthInstance(): Auth expect($user->permissions())->toBe(['view-ticket', 'edit-ticket']); }); -test('assigning an unknown role fails loudly instead of silently doing nothing', function () { +test('assigning an unknown role fails loudly instead of silently doing nothing', function (): void { $user = $this->auth->user(); $errors = []; - set_error_handler(function ($errno, $errstr) use (&$errors) { + set_error_handler(function ($errno, $errstr) use (&$errors): true { $errors[] = $errstr; return true; }); @@ -94,10 +95,10 @@ function roleAuthInstance(): Auth expect($user->permissions())->toBe([]); }); -test('a known role mixed with an unknown one assigns nothing', function () { +test('a known role mixed with an unknown one assigns nothing', function (): void { $user = $this->auth->user(); - set_error_handler(fn () => true); + set_error_handler(fn (): true => true); $result = $user->assign(['admin', 'nope']); restore_error_handler(); @@ -105,7 +106,7 @@ function roleAuthInstance(): Auth expect($user->roles())->toBe([]); }); -test('is and can accept arrays and match any entry', function () { +test('is and can accept arrays and match any entry', function (): void { $user = $this->auth->user(); $user->assign('support'); @@ -115,7 +116,7 @@ function roleAuthInstance(): Auth expect($user->can(['delete-ticket']))->toBeFalse(); }); -test('roles persist and rehydrate on a fresh login', function () { +test('roles persist and rehydrate on a fresh login', function (): void { $this->auth->user()->assign('support'); $freshAuth = roleAuthInstance(); @@ -131,7 +132,7 @@ function roleAuthInstance(): Auth expect($user->can('delete-ticket'))->toBeFalse(); }); -test('unassign revokes a role and recalculates permissions', function () { +test('unassign revokes a role and recalculates permissions', function (): void { $user = $this->auth->user(); $user->assign(['admin', 'guest']); @@ -155,14 +156,14 @@ function roleAuthInstance(): Auth * request was allowed through. */ -class MiddlewareDenied extends \Exception +class MiddlewareDenied extends Exception { } -function registerRoleMiddleware(Leaf\Auth $auth): void +function registerRoleMiddleware(Auth $auth): void { foreach (['is', 'isNot', 'can', 'cannot'] as $name) { - $auth->middleware($name, function () use ($name) { + $auth->middleware($name, function () use ($name): void { throw new MiddlewareDenied($name); }); } @@ -170,27 +171,26 @@ function registerRoleMiddleware(Leaf\Auth $auth): void function runRoleMiddleware(string $name, $args) { - $registered = (new ReflectionClass(Leaf\Router::class))->getProperty('namedMiddleware'); - $registered->setAccessible(true); + $registered = (new ReflectionClass(Router::class))->getProperty('namedMiddleware'); return $registered->getValue()[$name](is_array($args) ? $args : [$args]); } -test('is middleware allows a user holding the role', function () { +test('is middleware allows a user holding the role', function (): void { $this->auth->user()->assign('admin'); registerRoleMiddleware($this->auth); expect(fn () => runRoleMiddleware('is', ['admin']))->not->toThrow(MiddlewareDenied::class); }); -test('is middleware blocks a user without the role', function () { +test('is middleware blocks a user without the role', function (): void { $this->auth->user()->assign('guest'); registerRoleMiddleware($this->auth); expect(fn () => runRoleMiddleware('is', ['admin']))->toThrow(MiddlewareDenied::class); }); -test('is middleware allows any role in a piped list', function () { +test('is middleware allows any role in a piped list', function (): void { $this->auth->user()->assign('support'); registerRoleMiddleware($this->auth); @@ -198,28 +198,28 @@ function runRoleMiddleware(string $name, $args) expect(fn () => runRoleMiddleware('is', ['admin', 'support']))->not->toThrow(MiddlewareDenied::class); }); -test('can middleware allows a permission the role grants', function () { +test('can middleware allows a permission the role grants', function (): void { $this->auth->user()->assign('support'); registerRoleMiddleware($this->auth); expect(fn () => runRoleMiddleware('can', ['edit-ticket']))->not->toThrow(MiddlewareDenied::class); }); -test('can middleware blocks a permission the role does not grant', function () { +test('can middleware blocks a permission the role does not grant', function (): void { $this->auth->user()->assign('support'); registerRoleMiddleware($this->auth); expect(fn () => runRoleMiddleware('can', ['delete-ticket']))->toThrow(MiddlewareDenied::class); }); -test('isNot middleware blocks a user holding the role', function () { +test('isNot middleware blocks a user holding the role', function (): void { $this->auth->user()->assign('admin'); registerRoleMiddleware($this->auth); expect(fn () => runRoleMiddleware('isNot', ['admin']))->toThrow(MiddlewareDenied::class); }); -test('cannot middleware blocks a user holding the permission', function () { +test('cannot middleware blocks a user holding the permission', function (): void { $this->auth->user()->assign('admin'); registerRoleMiddleware($this->auth); diff --git a/tests/session.test.php b/tests/session.test.php index 9eefe62..2ff2eea 100644 --- a/tests/session.test.php +++ b/tests/session.test.php @@ -2,23 +2,23 @@ use Leaf\Auth\User; -beforeAll(function () { +beforeAll(function (): void { createTableForUsers(); dbInstance()->delete('users')->execute(); }); -afterEach(function () { +afterEach(function (): void { if (session_status() === PHP_SESSION_ACTIVE) { $_SESSION = []; session_destroy(); } }); -afterAll(function () { +afterAll(function (): void { dbInstance()->delete('users')->execute(); }); -test('register should create a new session when session => true', function () { +test('register should create a new session when session => true', function (): void { $auth = authInstance(); $auth->config(['session' => true]); $auth->config(['db.table' => 'users']); @@ -39,7 +39,7 @@ expect($_SESSION['auth']['user']['username'] ?? null)->toBe($userData['username']); }); -test('register should not create a new session when session => false', function () { +test('register should not create a new session when session => false', function (): void { $auth = authInstance(); $auth->config(['session' => false]); $auth->config(['db.table' => 'users']); @@ -59,7 +59,7 @@ expect($_SESSION['auth']['user']['username'] ?? null)->toBeNull(); }); -test('login should create session when session => true', function () { +test('login should create session when session => true', function (): void { $auth = authInstance(); $auth->config(['session' => true]); $auth->config(['db.table' => 'users']); @@ -80,7 +80,7 @@ expect($_SESSION['auth']['user']['username'] ?? null)->toBe($userData['username']); }); -test('session should create auth.ttl when session.lifetime is not 0', function () { +test('session should create auth.ttl when session.lifetime is not 0', function (): void { $auth = authInstance(); $auth->config(['session' => true, 'session.lifetime' => 2]); $auth->config(['db.table' => 'users']); @@ -105,7 +105,7 @@ expect($_SESSION['auth']['ttl'])->toBeGreaterThan($timeBeforeLogin); }); -test('session should not create auth.ttl when session.lifetime is 0', function () { +test('session should not create auth.ttl when session.lifetime is 0', function (): void { $auth = authInstance(); $auth->config(['session' => true, 'session.lifetime' => 0]); $auth->config(['db.table' => 'users']); @@ -128,7 +128,7 @@ expect($_SESSION['auth']['ttl'] ?? null)->toBeNull(); }); -test('session should expire after session.lifetime', function () { +test('session should expire after session.lifetime', function (): void { $auth = authInstance(); $auth->config(['session' => true, 'session.lifetime' => 2]); $auth->config(['db.table' => 'users']); @@ -154,7 +154,7 @@ expect($auth->user())->toBeNull(); }); -test('login should regenerate session id when session => true and session is already active', function () { +test('login should regenerate session id when session => true and session is already active', function (): void { $auth = authInstance(); $auth->config(['session' => true]); $auth->config(['db.table' => 'users']); @@ -183,7 +183,7 @@ expect($newSessionId)->not()->toBe($sessionId); }); -test('logout should remove auth info from session when session => true', function () { +test('logout should remove auth info from session when session => true', function (): void { $auth = authInstance(); $auth->config(['session' => true]); $auth->config(['db.table' => 'users']); diff --git a/tests/subscriptions.test.php b/tests/subscriptions.test.php index 1b6ce3b..6741a2e 100644 --- a/tests/subscriptions.test.php +++ b/tests/subscriptions.test.php @@ -1,5 +1,7 @@ config(['session' => false]); @@ -85,7 +84,7 @@ function seedUserSubscription(int $userId, array $overrides = []): void ], $overrides))->execute(); } -beforeAll(function () { +beforeAll(function (): void { // point the db() singleton (leafs/db ships it since leaf core is a // transitive dev dep) at the shared test connection db()->connection(dbInstance()->connection()); @@ -111,14 +110,14 @@ function seedUserSubscription(int $userId, array $overrides = []): void createTableForUsers(); }); -beforeEach(function () { +beforeEach(function (): void { dbInstance()->delete('subscriptions')->execute(); deleteUser('sub-user'); FakeBillingDriver::$cancelled = []; FakeBillingDriver::$resumed = []; }); -test('a user with no subscription rows has no subscription', function () { +test('a user with no subscription rows has no subscription', function (): void { $user = subscriptionUser(); expect($user->subscription())->toBeNull(); @@ -126,7 +125,7 @@ function seedUserSubscription(int $userId, array $overrides = []): void expect($user->hasActiveSubscription())->toBeFalse(); }); -test('an active subscription is returned with its tier attached', function () { +test('an active subscription is returned with its tier attached', function (): void { $user = subscriptionUser(); seedUserSubscription($user->id()); @@ -134,7 +133,7 @@ function seedUserSubscription(int $userId, array $overrides = []): void expect($user->subscription()['tier']['name'])->toBe('Basic'); }); -test('the latest subscription wins after a resubscribe', function () { +test('the latest subscription wins after a resubscribe', function (): void { $user = subscriptionUser(); seedUserSubscription($user->id(), [ 'subscription_id' => 'sub_old', @@ -147,7 +146,7 @@ function seedUserSubscription(int $userId, array $overrides = []): void expect($user->hasActiveSubscription())->toBeTrue(); }); -test('a cancelled subscription keeps access through the grace period', function () { +test('a cancelled subscription keeps access through the grace period', function (): void { $user = subscriptionUser(); seedUserSubscription($user->id(), [ 'status' => Subscription::STATUS_CANCELLED, @@ -159,7 +158,7 @@ function seedUserSubscription(int $userId, array $overrides = []): void expect($user->hasSubscription())->toBeFalse(); }); -test('a cancelled subscription past its end date has no access', function () { +test('a cancelled subscription past its end date has no access', function (): void { $user = subscriptionUser(); seedUserSubscription($user->id(), [ 'status' => Subscription::STATUS_CANCELLED, @@ -170,7 +169,7 @@ function seedUserSubscription(int $userId, array $overrides = []): void expect($user->hasActiveSubscription())->toBeFalse(); }); -test('trial state is reported from status or trial_ends_at', function () { +test('trial state is reported from status or trial_ends_at', function (): void { $user = subscriptionUser(); seedUserSubscription($user->id(), [ 'status' => Subscription::STATUS_TRIAL, @@ -181,7 +180,7 @@ function seedUserSubscription(int $userId, array $overrides = []): void expect($user->hasActiveSubscription())->toBeTrue(); }); -test('past due subscriptions lose access but are flagged for dunning', function () { +test('past due subscriptions lose access but are flagged for dunning', function (): void { $user = subscriptionUser(); seedUserSubscription($user->id(), ['status' => Subscription::STATUS_PAST_DUE]); @@ -189,18 +188,18 @@ function seedUserSubscription(int $userId, array $overrides = []): void expect($user->hasActiveSubscription())->toBeFalse(); }); -test('cancelSubscription defaults to period end and can cancel immediately', function () { +test('cancelSubscription defaults to period end and can cancel immediately', function (): void { $user = subscriptionUser(); seedUserSubscription($user->id()); expect($user->cancelSubscription())->toBeTrue(); expect(FakeBillingDriver::$cancelled)->toBe([['sub_1', true]]); - expect($user->cancelSubscription(false))->toBeTrue(); + expect($user->cancelSubscription(atPeriodEnd: false))->toBeTrue(); expect(FakeBillingDriver::$cancelled[1])->toBe(['sub_1', false]); }); -test('resumeSubscription only works during the grace period', function () { +test('resumeSubscription only works during the grace period', function (): void { $user = subscriptionUser(); seedUserSubscription($user->id(), [ 'status' => Subscription::STATUS_CANCELLED, @@ -211,7 +210,7 @@ function seedUserSubscription(int $userId, array $overrides = []): void expect(FakeBillingDriver::$resumed)->toBe(['sub_1']); }); -test('resumeSubscription refuses when the subscription is fully over', function () { +test('resumeSubscription refuses when the subscription is fully over', function (): void { $user = subscriptionUser(); seedUserSubscription($user->id(), [ 'status' => Subscription::STATUS_CANCELLED, diff --git a/tests/table.test.php b/tests/table.test.php index 4429e7c..2649676 100644 --- a/tests/table.test.php +++ b/tests/table.test.php @@ -1,17 +1,17 @@ delete('myusers')->execute(); }); -test('register should save user in user defined table', function () { +test('register should save user in user defined table', function (): void { $auth = authInstance(); $auth->config(['session' => false, 'db.table' => 'myusers']); @@ -28,7 +28,7 @@ expect($auth->user()->username)->toBe('test-user'); }); -test('login should work with user defined table', function () { +test('login should work with user defined table', function (): void { $auth = authInstance(); $auth->config(['session' => false, 'db.table' => 'myusers']); @@ -45,7 +45,7 @@ expect($auth->user()->username)->toBe('test-user'); }); -test('update should work with user defined table', function () { +test('update should work with user defined table', function (): void { $auth = authInstance(); $auth->config(['session' => true, 'db.table' => 'myusers', 'session.lifetime' => '1 day']); @@ -72,7 +72,7 @@ expect($auth->user()->email)->toBe('test-user55@example.com'); }); -test('user table can use uuid as id', function () { +test('user table can use uuid as id', function (): void { createTableForUsers('uuid_users', uuid: true); $auth = authInstance(); diff --git a/tests/tokens.test.php b/tests/tokens.test.php index 71fa289..cf9f543 100644 --- a/tests/tokens.test.php +++ b/tests/tokens.test.php @@ -1,5 +1,6 @@ null, 'token.lifetime' => 60 * 60 * 24 * 365]); unset($_ENV['APP_KEY'], $_ENV['AUTH_TOKEN_SECRET']); }); -test('the default token lifetime is a year, not zero', function () { +test('the default token lifetime is a year, not zero', function (): void { expect(Config::get('token.lifetime'))->toBe(60 * 60 * 24 * 365); }); -test('tokens are minted with a future expiry on default config', function () { +test('tokens are minted with a future expiry on default config', function (): void { $auth = authInstance(); $auth->createUserFor([ 'username' => 'token-check', @@ -36,7 +37,7 @@ expect($login)->toBeTrue(); $token = $auth->data()->accessToken; - $payload = json_decode(base64_decode(explode('.', $token)[1]), true); + $payload = json_decode(base64_decode(explode('.', $token)[1]), associative: true); // the born-expired bug: exp equalled iat on default config expect($payload['exp'])->toBeGreaterThan($payload['iat'] + 60 * 60 * 24 * 364); @@ -44,7 +45,7 @@ deleteUser('token-check'); }); -test('the token secret derives from APP_KEY when nothing else is set', function () { +test('the token secret derives from APP_KEY when nothing else is set', function (): void { Config::set(['token.secret' => null]); $_ENV['APP_KEY'] = 'base64:test-app-key'; @@ -55,7 +56,7 @@ ->and(strlen($secret))->toBe(64); // long enough for HS256 everywhere }); -test('AUTH_TOKEN_SECRET beats the APP_KEY derivation', function () { +test('AUTH_TOKEN_SECRET beats the APP_KEY derivation', function (): void { Config::set(['token.secret' => null]); $_ENV['APP_KEY'] = 'base64:test-app-key'; $_ENV['AUTH_TOKEN_SECRET'] = 'explicit-secret-that-is-long-enough-for-hs256'; @@ -63,15 +64,15 @@ expect(Config::tokenSecret())->toBe('explicit-secret-that-is-long-enough-for-hs256'); }); -test('token operations throw clearly when no secret can be resolved', function () { +test('token operations throw clearly when no secret can be resolved', function (): void { Config::set(['token.secret' => null]); unset($_ENV['APP_KEY'], $_ENV['AUTH_TOKEN_SECRET']); - expect(fn () => Config::tokenSecret()) + expect(fn (): string => Config::tokenSecret()) ->toThrow(RuntimeException::class, 'No auth token secret'); }); -test('each operation starts with a clean error state', function () { +test('each operation starts with a clean error state', function (): void { $auth = authInstance(); $auth->createUserFor([ 'username' => 'error-state', @@ -102,28 +103,28 @@ deleteUser('error-state'); }); -test('a present-but-null roles column is hidden from get()', function () { +test('a present-but-null roles column is hidden from get()', function (): void { $auth = authInstance(); - $user = new \Leaf\Auth\User([ + $user = new User([ 'id' => 1, 'email' => 'null-roles@example.com', Config::get('roles.key') => null, - ], false); + ], session: false); expect($user->get())->not->toHaveKey(Config::get('roles.key')); }); -test('the password hash never leaks from get(), whatever hidden says', function () { +test('the password hash never leaks from get(), whatever hidden says', function (): void { // following the old docs (literal 'password') with a custom // password.key put the bcrypt hash in the session and Inertia props Config::set(['password.key' => 'pass', 'hidden' => ['password', 'remember_token']]); - $user = new \Leaf\Auth\User([ + $user = new User([ 'id' => 1, 'email' => 'leak-check@example.com', 'pass' => '$2y$10$fakehashfakehashfakehash', 'remember_token' => 'session-equivalent-secret', - ], false); + ], session: false); expect($user->get())->not->toHaveKey('pass') ->and($user->get())->not->toHaveKey('remember_token'); @@ -131,21 +132,21 @@ Config::set(['password.key' => 'password', 'hidden' => ['field.id', 'field.password', 'remember_token']]); }); -test('session-only users need no token secret until tokens are read', function () { +test('session-only users need no token secret until tokens are read', function (): void { Config::set(['token.secret' => null]); unset($_ENV['APP_KEY'], $_ENV['AUTH_TOKEN_SECRET']); // constructing a user must not mint JWTs — session apps never read them - $user = new \Leaf\Auth\User(['id' => 5, 'email' => 'lazy@example.com'], false); + $user = new User(['id' => 5, 'email' => 'lazy@example.com'], session: false); expect($user->get())->toHaveKey('email'); // reading tokens is the moment the secret becomes required - expect(fn () => $user->tokens()) + expect(fn (): array => $user->tokens()) ->toThrow(RuntimeException::class, 'No auth token secret'); }); -test('middleware redirect targets are configurable', function () { +test('middleware redirect targets are configurable', function (): void { expect(Config::get('redirect.login'))->toBe('/auth/login') ->and(Config::get('redirect.guest'))->toBe('/dashboard'); diff --git a/tests/update.test.php b/tests/update.test.php index 76b2286..5fb7373 100644 --- a/tests/update.test.php +++ b/tests/update.test.php @@ -1,6 +1,6 @@ config(['db.table' => 'users']); @@ -19,11 +19,11 @@ sleep(1); }); -afterAll(function () { +afterAll(function (): void { dbInstance()->delete('users')->execute(); }); -test('update should update user data', function () { +test('update should update user data', function (): void { $auth = authInstance(); $auth->config(['db.table' => 'users']); @@ -49,7 +49,7 @@ expect($auth->user()->username)->toBe($updateData['username']); }); -test('update should fail if user already exists', function () { +test('update should fail if user already exists', function (): void { $auth = authInstance(); $auth->config(['db.table' => 'users']); @@ -72,7 +72,7 @@ expect($auth->errors()['username'])->toBe('username already exists'); }); -test('updatePassword should update user password', function () { +test('updatePassword should update user password', function (): void { $auth = authInstance(); $auth->config(['unique' => ['username']]); $auth->config(['db.table' => 'users']); @@ -104,7 +104,7 @@ expect($auth->user()->{$auth->config('password.key')})->not()->toBe($oldPassword); }); -test('update should regenerate session id if session => true', function () { +test('update should regenerate session id if session => true', function (): void { $auth = authInstance(); $auth->config(['session' => true]); $auth->config(['db.table' => 'users']); diff --git a/tests/user.test.php b/tests/user.test.php index 2283c83..f76a703 100644 --- a/tests/user.test.php +++ b/tests/user.test.php @@ -3,29 +3,25 @@ use Leaf\Auth; use Leaf\Auth\User; -beforeAll(function () { +beforeAll(function (): void { createTableForUsers(); dbInstance()->delete('users')->execute(); - try { - dbInstance() - ->insert('users') - ->params([ - 'username' => 'test-user', - 'email' => 'test-user@example.com', - 'password' => password_hash('password', PASSWORD_BCRYPT) - ]) - ->execute(); - } catch (Throwable $th) { - throw $th; - } + dbInstance() + ->insert('users') + ->params([ + 'username' => 'test-user', + 'email' => 'test-user@example.com', + 'password' => password_hash('password', PASSWORD_BCRYPT) + ]) + ->execute(); }); -afterAll(function () { +afterAll(function (): void { dbInstance()->delete('users')->execute(); }); -test('auth user is instance of Leaf\Auth\User', function () { +test('auth user is instance of Leaf\Auth\User', function (): void { $auth = authInstance(); $auth->config(['db.table' => 'users']); @@ -46,7 +42,7 @@ expect($auth->user()->username)->toBe($testUser['username']); }); -test('logout can use logout callback to run custom action', function () { +test('logout can use logout callback to run custom action', function (): void { $auth = authInstance(); $auth->config(['db.table' => 'users']); @@ -66,7 +62,7 @@ expect($auth->user())->toBeInstanceOf(User::class); expect($auth->user()->username)->toBe($testUser['username']); - $auth->logout(function ($auth) use ($testUser) { + $auth->logout(function ($auth): void { expect($auth)->toBeInstanceOf(Auth::class); });