You've already forked php-auth
generated from siteworxpro/Php-Template
Add audit logging functionality with database schema and event handling
Some checks failed
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m23s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m35s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m25s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 2m39s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Failing after 2m26s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Failing after 1m5s
Some checks failed
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m23s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m35s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m25s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 2m39s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Failing after 2m26s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Failing after 1m5s
This commit is contained in:
@@ -83,9 +83,9 @@ services:
|
|||||||
- "traefik.http.routers.api.rule=Host(`localhost`) || Host(`127.0.0.1`)"
|
- "traefik.http.routers.api.rule=Host(`localhost`) || Host(`127.0.0.1`)"
|
||||||
- "traefik.http.routers.api.tls=true"
|
- "traefik.http.routers.api.tls=true"
|
||||||
- "traefik.http.routers.api.service=api"
|
- "traefik.http.routers.api.service=api"
|
||||||
- "traefik.http.services.api.loadbalancer.healthcheck.path=/healthz"
|
# - "traefik.http.services.api.loadbalancer.healthcheck.path=/healthz"
|
||||||
- "traefik.http.services.api.loadbalancer.healthcheck.interval=5s"
|
# - "traefik.http.services.api.loadbalancer.healthcheck.interval=5s"
|
||||||
- "traefik.http.services.api.loadbalancer.healthcheck.timeout=60s"
|
# - "traefik.http.services.api.loadbalancer.healthcheck.timeout=60s"
|
||||||
- "traefik.tcp.services.api.loadbalancer.server.port=9001"
|
- "traefik.tcp.services.api.loadbalancer.server.port=9001"
|
||||||
- "traefik.http.services.api.loadbalancer.server.port=9501"
|
- "traefik.http.services.api.loadbalancer.server.port=9501"
|
||||||
- "traefik.tcp.routers.grpc.entrypoints=grpc"
|
- "traefik.tcp.routers.grpc.entrypoints=grpc"
|
||||||
|
|||||||
1
db/migrations/000002_create_audit_table.down.sql
Normal file
1
db/migrations/000002_create_audit_table.down.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
drop table if exists audit_logs;
|
||||||
14
db/migrations/000002_create_audit_table.up.sql
Normal file
14
db/migrations/000002_create_audit_table.up.sql
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
create table audit_logs
|
||||||
|
(
|
||||||
|
id VARCHAR(26) not null
|
||||||
|
constraint audit_logs_pkey
|
||||||
|
primary key,
|
||||||
|
user_id integer,
|
||||||
|
action varchar(255) not null,
|
||||||
|
timestamp timestamptz default current_timestamp,
|
||||||
|
details jsonb
|
||||||
|
);
|
||||||
|
|
||||||
|
create index idx_audit_logs_action on audit_logs (action);
|
||||||
|
create index idx_audit_logs_user_id on audit_logs (user_id);
|
||||||
|
create index idx_audit_logs_timestamp on audit_logs (timestamp);
|
||||||
@@ -7,6 +7,7 @@ namespace Siteworxpro\App\Cli\Commands\OAuth;
|
|||||||
use Siteworxpro\App\Cli\ClimateOutput;
|
use Siteworxpro\App\Cli\ClimateOutput;
|
||||||
use Siteworxpro\App\CommandBus\Commands\CreateClient as CreateClientCommand;
|
use Siteworxpro\App\CommandBus\Commands\CreateClient as CreateClientCommand;
|
||||||
use Siteworxpro\App\CommandBus\Exceptions\CommandHandlerException;
|
use Siteworxpro\App\CommandBus\Exceptions\CommandHandlerException;
|
||||||
|
use Siteworxpro\App\Models\Enums\ClientGrant as ClientGrantAlias;
|
||||||
use Siteworxpro\App\OAuth\Entities\Client;
|
use Siteworxpro\App\OAuth\Entities\Client;
|
||||||
use Siteworxpro\App\Services\Facades\CommandBus;
|
use Siteworxpro\App\Services\Facades\CommandBus;
|
||||||
use Symfony\Component\Console\Attribute\AsCommand;
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
@@ -37,8 +38,13 @@ class CreateClient extends \Siteworxpro\App\Cli\Commands\Command
|
|||||||
$question->setMultiselect(true);
|
$question->setMultiselect(true);
|
||||||
|
|
||||||
$grants = $this->helper->ask($input, $output, $question);
|
$grants = $this->helper->ask($input, $output, $question);
|
||||||
|
$grantsEnum = [];
|
||||||
|
|
||||||
$command = new CreateClientCommand($clientName, $grants, $clientDescription);
|
foreach ($grants as $grant) {
|
||||||
|
$grantsEnum[] = ClientGrantAlias::from($grant);
|
||||||
|
}
|
||||||
|
|
||||||
|
$command = new CreateClientCommand($clientName, $grantsEnum, $clientDescription);
|
||||||
try {
|
try {
|
||||||
/** @var Client $client */
|
/** @var Client $client */
|
||||||
$client = CommandBus::handle($command);
|
$client = CommandBus::handle($command);
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ namespace Siteworxpro\App\Cli\Commands\User;
|
|||||||
|
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
use Siteworxpro\App\Cli\Commands\Command;
|
use Siteworxpro\App\Cli\Commands\Command;
|
||||||
|
use Siteworxpro\App\CommandBus\Commands\CreateUser;
|
||||||
use Siteworxpro\App\Models\ClientUser;
|
use Siteworxpro\App\Models\ClientUser;
|
||||||
use Siteworxpro\App\Models\User;
|
use Siteworxpro\App\Models\User;
|
||||||
use Siteworxpro\App\OAuth\Entities\Client;
|
use Siteworxpro\App\OAuth\Entities\Client;
|
||||||
|
use Siteworxpro\App\Services\Facades\CommandBus;
|
||||||
use Symfony\Component\Console\Attribute\AsCommand;
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
use Symfony\Component\Console\Command\Command as SCommand;
|
use Symfony\Component\Console\Command\Command as SCommand;
|
||||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||||
@@ -76,19 +78,16 @@ class Add extends Command
|
|||||||
$lastNameQuestion = new QuestionInput('Enter the user\'s last name: ');
|
$lastNameQuestion = new QuestionInput('Enter the user\'s last name: ');
|
||||||
$lastName = $helper->ask($input, $output, $lastNameQuestion);
|
$lastName = $helper->ask($input, $output, $lastNameQuestion);
|
||||||
|
|
||||||
$user = new User();
|
$createUserCommand = new CreateUser($client, $email, $password, $firstName, $lastName);
|
||||||
$user->email = $email;
|
|
||||||
$user->password = $password;
|
|
||||||
$user->first_name = $firstName;
|
|
||||||
$user->last_name = $lastName;
|
|
||||||
$user->save();
|
|
||||||
|
|
||||||
$clientUser = new ClientUser();
|
/** @var User $user */
|
||||||
$clientUser->client_id = $client->id;
|
$user = CommandBus::handle($createUserCommand);
|
||||||
$clientUser->user_id = $user->id;
|
|
||||||
$clientUser->save();
|
|
||||||
|
|
||||||
$output->green('User added and associated with the client successfully.');
|
$output->green('User added and associated with the client successfully.');
|
||||||
|
$output->info('User Details:');
|
||||||
|
$output->out("ID: $user->id");
|
||||||
|
$output->out("Email: $user->email");
|
||||||
|
$output->out("Name: $user->first_name $user->last_name");
|
||||||
|
|
||||||
return SCommand::SUCCESS;
|
return SCommand::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,24 +2,32 @@
|
|||||||
|
|
||||||
namespace Siteworxpro\App\CommandBus\Commands;
|
namespace Siteworxpro\App\CommandBus\Commands;
|
||||||
|
|
||||||
|
use Siteworxpro\App\Models\Enums\ClientGrant;
|
||||||
|
|
||||||
readonly class CreateClient extends Command
|
readonly class CreateClient extends Command
|
||||||
{
|
{
|
||||||
private const array VALID_GRANTS = [
|
/**
|
||||||
'authorization_code',
|
* @param string $clientName
|
||||||
'password',
|
* @param array<ClientGrant> $clientGrants
|
||||||
'client_credentials',
|
* @param string $clientDescription
|
||||||
'refresh_token',
|
*/
|
||||||
'implicit',
|
|
||||||
];
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private string $clientName,
|
private string $clientName,
|
||||||
private array $clientGrants = [],
|
private array $clientGrants = [],
|
||||||
private string $clientDescription = ''
|
private string $clientDescription = ''
|
||||||
) {
|
) {
|
||||||
foreach ($this->clientGrants as $grant) {
|
foreach ($this->clientGrants as $grant) {
|
||||||
if (!in_array($grant, self::VALID_GRANTS, true)) {
|
if ($grant instanceof ClientGrant === false) {
|
||||||
throw new \InvalidArgumentException("Invalid grant type: $grant");
|
throw new \InvalidArgumentException(
|
||||||
|
sprintf(
|
||||||
|
'Invalid client grant provided: %s. Valid grants are: %s',
|
||||||
|
is_string($grant) ? $grant : gettype($grant),
|
||||||
|
implode(', ', array_map(
|
||||||
|
fn(ClientGrant $validGrant) => $validGrant->value,
|
||||||
|
ClientGrant::getValidGrants()
|
||||||
|
))
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
51
src/CommandBus/Commands/CreateUser.php
Normal file
51
src/CommandBus/Commands/CreateUser.php
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\CommandBus\Commands;
|
||||||
|
|
||||||
|
use Siteworxpro\App\OAuth\Entities\Client;
|
||||||
|
|
||||||
|
readonly class CreateUser extends Command
|
||||||
|
{
|
||||||
|
private string $email;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private Client $client,
|
||||||
|
string $email,
|
||||||
|
private string $password,
|
||||||
|
private string $firstName,
|
||||||
|
private string $lastName,
|
||||||
|
) {
|
||||||
|
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
throw new \InvalidArgumentException('Invalid email address.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->email = $email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEmail(): string
|
||||||
|
{
|
||||||
|
return $this->email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPassword(): string
|
||||||
|
{
|
||||||
|
return $this->password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFirstName(): string
|
||||||
|
{
|
||||||
|
return $this->firstName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLastName(): string
|
||||||
|
{
|
||||||
|
return $this->lastName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getClient(): Client
|
||||||
|
{
|
||||||
|
return $this->client;
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/CommandBus/Handlers/CreateUserHandler.php
Normal file
31
src/CommandBus/Handlers/CreateUserHandler.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\CommandBus\Handlers;
|
||||||
|
|
||||||
|
use Siteworxpro\App\Attributes\CommandBus\HandlesCommand;
|
||||||
|
use Siteworxpro\App\CommandBus\Commands\CreateUser;
|
||||||
|
use Siteworxpro\App\Models\ClientUser;
|
||||||
|
use Siteworxpro\App\Models\User;
|
||||||
|
|
||||||
|
#[HandlesCommand(CreateUser::class)]
|
||||||
|
class CreateUserHandler
|
||||||
|
{
|
||||||
|
public function __invoke(CreateUser $command): User
|
||||||
|
{
|
||||||
|
$user = User::create([
|
||||||
|
'email' => $command->getEmail(),
|
||||||
|
'password' => password_hash($command->getPassword(), PASSWORD_BCRYPT),
|
||||||
|
'first_name' => $command->getFirstName(),
|
||||||
|
'last_name' => $command->getLastName(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$clientUser = new ClientUser();
|
||||||
|
$clientUser->client_id = $command->getClient()->id;
|
||||||
|
$clientUser->user_id = $user->id;
|
||||||
|
$clientUser->save();
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ use Siteworxpro\App\CommandBus\Commands\Command;
|
|||||||
use Siteworxpro\App\CommandBus\Exceptions\CommandHandlerException;
|
use Siteworxpro\App\CommandBus\Exceptions\CommandHandlerException;
|
||||||
use Siteworxpro\App\CommandBus\Handlers\CommandHandler;
|
use Siteworxpro\App\CommandBus\Handlers\CommandHandler;
|
||||||
use Siteworxpro\App\OAuth\Entities\Client;
|
use Siteworxpro\App\OAuth\Entities\Client;
|
||||||
|
use Siteworxpro\App\OAuth\Entities\ClientCapabilities;
|
||||||
|
|
||||||
#[HandlesCommand(\Siteworxpro\App\CommandBus\Commands\CreateClient::class)]
|
#[HandlesCommand(\Siteworxpro\App\CommandBus\Commands\CreateClient::class)]
|
||||||
class CreateClient extends CommandHandler
|
class CreateClient extends CommandHandler
|
||||||
@@ -24,6 +25,7 @@ class CreateClient extends CommandHandler
|
|||||||
$client->name = $command->getClientName();
|
$client->name = $command->getClientName();
|
||||||
$client->description = $command->getClientDescription();
|
$client->description = $command->getClientDescription();
|
||||||
$client->grant_types = new Collection($command->getClientGrants()); // @phpstan-ignore-line assign.propertyType
|
$client->grant_types = new Collection($command->getClientGrants()); // @phpstan-ignore-line assign.propertyType
|
||||||
|
$client->capabilities = new ClientCapabilities();
|
||||||
|
|
||||||
$client->save();
|
$client->save();
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,14 @@ namespace Siteworxpro\App\Controllers;
|
|||||||
use Defuse\Crypto\Exception\BadFormatException;
|
use Defuse\Crypto\Exception\BadFormatException;
|
||||||
use Defuse\Crypto\Exception\EnvironmentIsBrokenException;
|
use Defuse\Crypto\Exception\EnvironmentIsBrokenException;
|
||||||
use League\OAuth2\Server\Exception\OAuthServerException;
|
use League\OAuth2\Server\Exception\OAuthServerException;
|
||||||
|
use Nyholm\Psr7\Response;
|
||||||
use Nyholm\Psr7\ServerRequest;
|
use Nyholm\Psr7\ServerRequest;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Siteworxpro\App\Events\AccessToken\Issued;
|
||||||
use Siteworxpro\App\Http\JsonResponseFactory;
|
use Siteworxpro\App\Http\JsonResponseFactory;
|
||||||
use Siteworxpro\App\Http\Responses\GenericResponse;
|
use Siteworxpro\App\Http\Responses\GenericResponse;
|
||||||
use Siteworxpro\App\OAuth\Entities\Client;
|
use Siteworxpro\App\OAuth\Entities\Client;
|
||||||
|
use Siteworxpro\App\Services\Facades\Dispatcher;
|
||||||
use Siteworxpro\HttpStatus\CodesEnum;
|
use Siteworxpro\HttpStatus\CodesEnum;
|
||||||
|
|
||||||
final class AccessTokenController extends Controller
|
final class AccessTokenController extends Controller
|
||||||
@@ -34,9 +37,14 @@ final class AccessTokenController extends Controller
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $client
|
/** @var Response $response */
|
||||||
|
$response = $client
|
||||||
->getAuthorizationServer()
|
->getAuthorizationServer()
|
||||||
->respondToAccessTokenRequest($request, JsonResponseFactory::createJsonResponse([]));
|
->respondToAccessTokenRequest($request, JsonResponseFactory::createJsonResponse([]));
|
||||||
|
|
||||||
|
Dispatcher::push(new Issued($response));
|
||||||
|
|
||||||
|
return $response;
|
||||||
} catch (OAuthServerException $e) {
|
} catch (OAuthServerException $e) {
|
||||||
return JsonResponseFactory::createJsonResponse(
|
return JsonResponseFactory::createJsonResponse(
|
||||||
$e->getPayload(),
|
$e->getPayload(),
|
||||||
|
|||||||
@@ -12,10 +12,14 @@ use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
|
|||||||
use Nyholm\Psr7\Response;
|
use Nyholm\Psr7\Response;
|
||||||
use Nyholm\Psr7\ServerRequest;
|
use Nyholm\Psr7\ServerRequest;
|
||||||
use Nyholm\Psr7\Stream;
|
use Nyholm\Psr7\Stream;
|
||||||
|
use Siteworxpro\App\Events\Login\LoginAttempt;
|
||||||
|
use Siteworxpro\App\Events\Login\LoginFailed;
|
||||||
|
use Siteworxpro\App\Events\Login\LoginSuccess;
|
||||||
use Siteworxpro\App\Helpers\Rand;
|
use Siteworxpro\App\Helpers\Rand;
|
||||||
use Siteworxpro\App\Http\JsonResponseFactory;
|
use Siteworxpro\App\Http\JsonResponseFactory;
|
||||||
use Siteworxpro\App\Http\Responses\ServerErrorResponse;
|
use Siteworxpro\App\Http\Responses\ServerErrorResponse;
|
||||||
use Siteworxpro\App\OAuth\Entities\Client;
|
use Siteworxpro\App\OAuth\Entities\Client;
|
||||||
|
use Siteworxpro\App\Services\Facades\Dispatcher;
|
||||||
use Siteworxpro\App\Services\Facades\Logger;
|
use Siteworxpro\App\Services\Facades\Logger;
|
||||||
use Siteworxpro\App\Services\Facades\Redis;
|
use Siteworxpro\App\Services\Facades\Redis;
|
||||||
use Siteworxpro\HttpStatus\CodesEnum;
|
use Siteworxpro\HttpStatus\CodesEnum;
|
||||||
@@ -31,6 +35,8 @@ final class AuthorizeController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function post(ServerRequest $request): Response
|
public function post(ServerRequest $request): Response
|
||||||
{
|
{
|
||||||
|
Dispatcher::push(new LoginAttempt($request));
|
||||||
|
|
||||||
$s = $request->getCookieParams()['s'] ?? '';
|
$s = $request->getCookieParams()['s'] ?? '';
|
||||||
|
|
||||||
$password = $request->getParsedBody()['password'] ?? '';
|
$password = $request->getParsedBody()['password'] ?? '';
|
||||||
@@ -63,6 +69,8 @@ final class AuthorizeController extends Controller
|
|||||||
$user = $client->loginUser($email, $password);
|
$user = $client->loginUser($email, $password);
|
||||||
|
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
|
Dispatcher::push(new LoginFailed($request));
|
||||||
|
|
||||||
return JsonResponseFactory::createJsonResponse([
|
return JsonResponseFactory::createJsonResponse([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'reason' => 'login failed'
|
'reason' => 'login failed'
|
||||||
@@ -76,6 +84,8 @@ final class AuthorizeController extends Controller
|
|||||||
|
|
||||||
Redis::del('session:' . $s);
|
Redis::del('session:' . $s);
|
||||||
|
|
||||||
|
Dispatcher::push(new LoginSuccess($request, $user));
|
||||||
|
|
||||||
return JsonResponseFactory::createJsonResponse([
|
return JsonResponseFactory::createJsonResponse([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'location' => $response->getHeader('Location')[0]
|
'location' => $response->getHeader('Location')[0]
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Siteworxpro\App\Events\Listeners;
|
namespace Siteworxpro\App\EventListeners;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Listener
|
* Class Listener
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Siteworxpro\App\Events\Listeners;
|
namespace Siteworxpro\App\EventListeners;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface ListenerInterface
|
* Interface ListenerInterface
|
||||||
40
src/EventListeners/LoginFailed.php
Normal file
40
src/EventListeners/LoginFailed.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\EventListeners;
|
||||||
|
|
||||||
|
use Siteworxpro\App\Attributes\Events\ListensFor;
|
||||||
|
use Siteworxpro\App\Models\AuditLog;
|
||||||
|
use Siteworxpro\App\Models\Enums\AuditLogAction;
|
||||||
|
|
||||||
|
#[ListensFor(\Siteworxpro\App\Events\Login\LoginFailed::class)]
|
||||||
|
class LoginFailed extends Listener
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle the event.
|
||||||
|
*
|
||||||
|
* @param \Siteworxpro\App\Events\Login\LoginFailed $event
|
||||||
|
* @param array $payload
|
||||||
|
* @return AuditLog|null
|
||||||
|
*/
|
||||||
|
public function __invoke(mixed $event, array $payload = []): ?AuditLog
|
||||||
|
{
|
||||||
|
if (is_string($event)) {
|
||||||
|
$event = $payload[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$event instanceof \Siteworxpro\App\Events\Login\LoginFailed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return AuditLog::create([
|
||||||
|
'user_id' => $event->getUser()?->id,
|
||||||
|
'action' => AuditLogAction::LOGIN_FAIL,
|
||||||
|
'details' => [
|
||||||
|
'username_attempted' => $event->getUsernameAttempted(),
|
||||||
|
'ip_address' => $event->getRequestIp(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/Events/AccessToken/Issued.php
Normal file
19
src/Events/AccessToken/Issued.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\Events\AccessToken;
|
||||||
|
|
||||||
|
use Nyholm\Psr7\Response;
|
||||||
|
|
||||||
|
readonly class Issued
|
||||||
|
{
|
||||||
|
public function __construct(private Response $response)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getResponse(): Response
|
||||||
|
{
|
||||||
|
return $this->response;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,7 +37,7 @@ class Dispatcher implements DispatcherContract, Arrayable
|
|||||||
/**
|
/**
|
||||||
* @var string LISTENERS_NAMESPACE The namespace where listeners are located
|
* @var string LISTENERS_NAMESPACE The namespace where listeners are located
|
||||||
*/
|
*/
|
||||||
private const string LISTENERS_NAMESPACE = 'Siteworxpro\\App\\Events\\Listeners\\';
|
private const string LISTENERS_NAMESPACE = 'Siteworxpro\\App\\EventListeners\\';
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
@@ -63,7 +63,7 @@ class Dispatcher implements DispatcherContract, Arrayable
|
|||||||
private function registerListeners(): void
|
private function registerListeners(): void
|
||||||
{
|
{
|
||||||
// traverse the Listeners directory and register all listeners
|
// traverse the Listeners directory and register all listeners
|
||||||
$listenersPath = __DIR__ . '/Listeners';
|
$listenersPath = __DIR__ . '/../EventListeners';
|
||||||
$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($listenersPath));
|
$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($listenersPath));
|
||||||
|
|
||||||
foreach ($iterator as $file) {
|
foreach ($iterator as $file) {
|
||||||
@@ -199,6 +199,11 @@ class Dispatcher implements DispatcherContract, Arrayable
|
|||||||
*/
|
*/
|
||||||
public function push($event, $payload = []): void
|
public function push($event, $payload = []): void
|
||||||
{
|
{
|
||||||
|
if (!is_string($event)) {
|
||||||
|
$payload = [$event];
|
||||||
|
$event = get_class($event);
|
||||||
|
}
|
||||||
|
|
||||||
$this->pushed->put($event, $payload);
|
$this->pushed->put($event, $payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Siteworxpro\App\Events\Listeners\Database;
|
|
||||||
|
|
||||||
use Illuminate\Database\Events\ConnectionEstablished;
|
|
||||||
use Illuminate\Database\Events\ConnectionEvent;
|
|
||||||
use Siteworxpro\App\Attributes\Events\ListensFor;
|
|
||||||
use Siteworxpro\App\Events\Listeners\Listener;
|
|
||||||
use Siteworxpro\App\Services\Facades\Logger;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class Connected
|
|
||||||
* @package Siteworxpro\App\Events\Listeners\Database
|
|
||||||
*/
|
|
||||||
#[ListensFor(ConnectionEstablished::class)]
|
|
||||||
class Connected extends Listener
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param mixed $event
|
|
||||||
* @param array $payload
|
|
||||||
* @return null
|
|
||||||
*/
|
|
||||||
public function __invoke(mixed $event, array $payload = []): null
|
|
||||||
{
|
|
||||||
if (!($event instanceof ConnectionEvent)) {
|
|
||||||
throw new \TypeError("Invalid event type passed to listener " . static::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
20
src/Events/Login/LoginAttempt.php
Normal file
20
src/Events/Login/LoginAttempt.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\Events\Login;
|
||||||
|
|
||||||
|
use Nyholm\Psr7\ServerRequest;
|
||||||
|
|
||||||
|
readonly class LoginAttempt
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private ServerRequest $request,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEmail(): string
|
||||||
|
{
|
||||||
|
return $this->request->getParsedBody()['email'] ?? '';
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/Events/Login/LoginFailed.php
Normal file
50
src/Events/Login/LoginFailed.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\Events\Login;
|
||||||
|
|
||||||
|
use Nyholm\Psr7\ServerRequest;
|
||||||
|
use Siteworxpro\App\Models\User;
|
||||||
|
|
||||||
|
readonly class LoginFailed
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private ServerRequest $request,
|
||||||
|
private ?User $user = null,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRequestIp(): string
|
||||||
|
{
|
||||||
|
if ($this->request->getHeader('X-Forwarded-For')) {
|
||||||
|
$ipAddresses = explode(',', $this->request->getHeaderLine('X-Forwarded-For'));
|
||||||
|
return trim($ipAddresses[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->request->getHeader('X-Real-IP')) {
|
||||||
|
return $this->request->getHeaderLine('X-Real-IP');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->request->getServerParams()['HTTP_CLIENT_IP'] ?? false) {
|
||||||
|
return $this->request->getServerParams()['HTTP_CLIENT_IP'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->request->getServerParams()['HTTP_X_FORWARDED_FOR'] ?? false) {
|
||||||
|
$ipAddresses = explode(',', $this->request->getServerParams()['HTTP_X_FORWARDED_FOR']);
|
||||||
|
return trim($ipAddresses[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->request->getServerParams()['REMOTE_ADDR'] ?? 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUsernameAttempted(): string
|
||||||
|
{
|
||||||
|
return $this->request->getParsedBody()['email'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUser(): ?User
|
||||||
|
{
|
||||||
|
return $this->user;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/Events/Login/LoginSuccess.php
Normal file
27
src/Events/Login/LoginSuccess.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\Events\Login;
|
||||||
|
|
||||||
|
use Nyholm\Psr7\ServerRequest;
|
||||||
|
use Siteworxpro\App\Models\User;
|
||||||
|
|
||||||
|
readonly class LoginSuccess
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private ServerRequest $serverRequest,
|
||||||
|
private User $user
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRequest(): ServerRequest
|
||||||
|
{
|
||||||
|
return $this->serverRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUser(): User
|
||||||
|
{
|
||||||
|
return $this->user;
|
||||||
|
}
|
||||||
|
}
|
||||||
55
src/Models/AuditLog.php
Normal file
55
src/Models/AuditLog.php
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\Models;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Siteworxpro\App\Models\Enums\AuditLogAction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class AuditLog
|
||||||
|
*
|
||||||
|
* @package Siteworxpro\App\Models
|
||||||
|
*
|
||||||
|
* @property int $id
|
||||||
|
* @property int|null $user_id
|
||||||
|
* @property AuditLogAction $action
|
||||||
|
* @property-read Carbon $timestamp
|
||||||
|
* @property array $details
|
||||||
|
*/
|
||||||
|
class AuditLog extends Model
|
||||||
|
{
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'details' => 'array',
|
||||||
|
'timestamp' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'action',
|
||||||
|
'details',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function getActionAttribute(string $value): AuditLogAction
|
||||||
|
{
|
||||||
|
return AuditLogAction::from($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setActionAttribute(AuditLogAction $value): void
|
||||||
|
{
|
||||||
|
$this->attributes['action'] = $value->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDetailsAttribute(array $value): void
|
||||||
|
{
|
||||||
|
$this->attributes['details'] = json_encode($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDetailsAttribute($value): array
|
||||||
|
{
|
||||||
|
return json_decode($value, true) ?? [];
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/Models/Enums/AuditLogAction.php
Normal file
12
src/Models/Enums/AuditLogAction.php
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\Models\Enums;
|
||||||
|
|
||||||
|
enum AuditLogAction: string
|
||||||
|
{
|
||||||
|
case LOGIN_SUCCESS = 'login_success';
|
||||||
|
case LOGIN_FAIL = 'login_fail';
|
||||||
|
case LOGOUT = 'logout';
|
||||||
|
}
|
||||||
37
src/Models/Enums/ClientGrant.php
Normal file
37
src/Models/Enums/ClientGrant.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\Models\Enums;
|
||||||
|
|
||||||
|
enum ClientGrant: string
|
||||||
|
{
|
||||||
|
case AUTHORIZATION_CODE = 'authorization_code';
|
||||||
|
case PASSWORD = 'password';
|
||||||
|
case CLIENT_CREDENTIALS = 'client_credentials';
|
||||||
|
case REFRESH_TOKEN = 'refresh_token';
|
||||||
|
case IMPLICIT = 'implicit';
|
||||||
|
|
||||||
|
public static function fromString(string $grant): ?ClientGrant
|
||||||
|
{
|
||||||
|
return match ($grant) {
|
||||||
|
'authorization_code' => self::AUTHORIZATION_CODE,
|
||||||
|
'password' => self::PASSWORD,
|
||||||
|
'client_credentials' => self::CLIENT_CREDENTIALS,
|
||||||
|
'refresh_token' => self::REFRESH_TOKEN,
|
||||||
|
'implicit' => self::IMPLICIT,
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getValidGrants(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::AUTHORIZATION_CODE,
|
||||||
|
self::PASSWORD,
|
||||||
|
self::CLIENT_CREDENTIALS,
|
||||||
|
self::REFRESH_TOKEN,
|
||||||
|
self::IMPLICIT,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ use Siteworxpro\App\Helpers\Ulid;
|
|||||||
* @method static static|null find(string $id, array $columns = ['*'])
|
* @method static static|null find(string $id, array $columns = ['*'])
|
||||||
* @method static Builder where(string $column, string $operator = null, string $value = null, string $boolean = 'and')
|
* @method static Builder where(string $column, string $operator = null, string $value = null, string $boolean = 'and')
|
||||||
* @method static Builder whereJsonContains(string $column, mixed $value, string $boolean = 'and', bool $not = false)
|
* @method static Builder whereJsonContains(string $column, mixed $value, string $boolean = 'and', bool $not = false)
|
||||||
|
* @method static static create(array $attributes = [])
|
||||||
*/
|
*/
|
||||||
abstract class Model extends ORM
|
abstract class Model extends ORM
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ use Siteworxpro\App\OAuth\ScopeRepository;
|
|||||||
* @property Collection $grant_types
|
* @property Collection $grant_types
|
||||||
* @property bool $confidential
|
* @property bool $confidential
|
||||||
*
|
*
|
||||||
* @property-read ClientCapabilities $capabilities
|
* @property ClientCapabilities $capabilities
|
||||||
* @property-read Collection<ClientRedirectUri> $clientRedirectUris
|
* @property-read Collection<ClientRedirectUri> $clientRedirectUris
|
||||||
* @property-read Scope[]|Collection $scopes
|
* @property-read Scope[]|Collection $scopes
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class ClientCapabilities implements Arrayable
|
|||||||
'logoUrl' => null,
|
'logoUrl' => null,
|
||||||
];
|
];
|
||||||
|
|
||||||
public function __construct(array $capabilities)
|
public function __construct(array $capabilities = [])
|
||||||
{
|
{
|
||||||
if (isset($capabilities['userPass'])) {
|
if (isset($capabilities['userPass'])) {
|
||||||
$this->userPass = (bool)$capabilities['userPass'];
|
$this->userPass = (bool)$capabilities['userPass'];
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use Illuminate\Database\Events\ConnectionEstablished;
|
|||||||
use Psr\Container\ContainerExceptionInterface;
|
use Psr\Container\ContainerExceptionInterface;
|
||||||
use Psr\Container\NotFoundExceptionInterface;
|
use Psr\Container\NotFoundExceptionInterface;
|
||||||
use Psr\Log\LogLevel;
|
use Psr\Log\LogLevel;
|
||||||
use Siteworxpro\App\Events\Listeners\Database\Connected;
|
use Siteworxpro\App\Events\EventListeners\Database\Connected;
|
||||||
use Siteworxpro\App\Log\Logger;
|
use Siteworxpro\App\Log\Logger;
|
||||||
use Siteworxpro\Tests\Unit;
|
use Siteworxpro\Tests\Unit;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user