You've already forked php-auth
generated from siteworxpro/Php-Template
Basics of auth
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ vendor/
|
||||
.phpunit.cache/
|
||||
|
||||
tests/reports/
|
||||
public/
|
||||
5
.rr.yaml
5
.rr.yaml
@@ -18,6 +18,11 @@ grpc:
|
||||
- "protos/example.proto"
|
||||
|
||||
http:
|
||||
middleware: [ "static" ]
|
||||
static:
|
||||
dir: "public/"
|
||||
allow: [ ".css", ".js", ".ico" ]
|
||||
|
||||
pool:
|
||||
allocate_timeout: 5s
|
||||
reset_timeout: 5s
|
||||
|
||||
@@ -28,7 +28,11 @@
|
||||
"guzzlehttp/guzzle": "^7.10",
|
||||
"zircote/swagger-php": "^5.7",
|
||||
"spiral/roadrunner-grpc": "^3.5",
|
||||
"league/tactician": "^1.1"
|
||||
"league/tactician": "^1.1",
|
||||
"league/oauth2-server": "^9.3",
|
||||
"ext-sodium": "*",
|
||||
"league/climate": "^3.10",
|
||||
"hansott/psr7-cookies": "^4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^12.4",
|
||||
|
||||
761
composer.lock
generated
761
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "d027bee8e875c5542f7ff9612bfac4e2",
|
||||
"content-hash": "9344732ab599b7f8ea85a31743adcee1",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/cli",
|
||||
@@ -208,6 +208,73 @@
|
||||
],
|
||||
"time": "2024-02-09T16:56:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "defuse/php-encryption",
|
||||
"version": "v2.4.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/defuse/php-encryption.git",
|
||||
"reference": "f53396c2d34225064647a05ca76c1da9d99e5828"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/defuse/php-encryption/zipball/f53396c2d34225064647a05ca76c1da9d99e5828",
|
||||
"reference": "f53396c2d34225064647a05ca76c1da9d99e5828",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-openssl": "*",
|
||||
"paragonie/random_compat": ">= 2",
|
||||
"php": ">=5.6.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^5|^6|^7|^8|^9|^10",
|
||||
"yoast/phpunit-polyfills": "^2.0.0"
|
||||
},
|
||||
"bin": [
|
||||
"bin/generate-defuse-key"
|
||||
],
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Defuse\\Crypto\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Hornby",
|
||||
"email": "taylor@defuse.ca",
|
||||
"homepage": "https://defuse.ca/"
|
||||
},
|
||||
{
|
||||
"name": "Scott Arciszewski",
|
||||
"email": "info@paragonie.com",
|
||||
"homepage": "https://paragonie.com"
|
||||
}
|
||||
],
|
||||
"description": "Secure PHP Encryption Library",
|
||||
"keywords": [
|
||||
"aes",
|
||||
"authenticated encryption",
|
||||
"cipher",
|
||||
"crypto",
|
||||
"cryptography",
|
||||
"encrypt",
|
||||
"encryption",
|
||||
"openssl",
|
||||
"security",
|
||||
"symmetric key cryptography"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/defuse/php-encryption/issues",
|
||||
"source": "https://github.com/defuse/php-encryption/tree/v2.4.0"
|
||||
},
|
||||
"time": "2023-06-19T06:10:36+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/inflector",
|
||||
"version": "2.1.0",
|
||||
@@ -726,6 +793,73 @@
|
||||
],
|
||||
"time": "2025-08-23T21:21:41+00:00"
|
||||
},
|
||||
{
|
||||
"name": "hansott/psr7-cookies",
|
||||
"version": "4.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/hansott/psr7-cookies.git",
|
||||
"reference": "fdd4790274996a3d5a723fa32d1c82e2b319e8fc"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/hansott/psr7-cookies/zipball/fdd4790274996a3d5a723fa32d1c82e2b319e8fc",
|
||||
"reference": "fdd4790274996a3d5a723fa32d1c82e2b319e8fc",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.0 || ^8.0",
|
||||
"psr/http-message": "^2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"guzzlehttp/psr7": "^2.0",
|
||||
"phpunit/phpunit": "^6.0 || ^7.0 || ^8.0 || ^9.0",
|
||||
"scrutinizer/ocular": "~1.1"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"HansOtt\\PSR7Cookies\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Hans Ott",
|
||||
"email": "hans@iott.consulting",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "🍪 bakes cookies for PSR-7 messages",
|
||||
"homepage": "https://github.com/hansott/psr7-cookies",
|
||||
"keywords": [
|
||||
"cookies",
|
||||
"fig",
|
||||
"hansott",
|
||||
"http-message",
|
||||
"psr7-cookies",
|
||||
"setcookie"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/hansott/psr7-cookies/issues",
|
||||
"source": "https://github.com/hansott/psr7-cookies/tree/4.0.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/hansott",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-01-08T11:06:34+00:00"
|
||||
},
|
||||
{
|
||||
"name": "illuminate/collections",
|
||||
"version": "v12.38.1",
|
||||
@@ -1197,6 +1331,70 @@
|
||||
},
|
||||
"time": "2025-10-09T13:42:30+00:00"
|
||||
},
|
||||
{
|
||||
"name": "lcobucci/clock",
|
||||
"version": "3.5.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/lcobucci/clock.git",
|
||||
"reference": "a3139d9e97d47826f27e6a17bb63f13621f86058"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/lcobucci/clock/zipball/a3139d9e97d47826f27e6a17bb63f13621f86058",
|
||||
"reference": "a3139d9e97d47826f27e6a17bb63f13621f86058",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "~8.3.0 || ~8.4.0 || ~8.5.0",
|
||||
"psr/clock": "^1.0"
|
||||
},
|
||||
"provide": {
|
||||
"psr/clock-implementation": "1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"infection/infection": "^0.31",
|
||||
"lcobucci/coding-standard": "^11.2.0",
|
||||
"phpstan/extension-installer": "^1.3.1",
|
||||
"phpstan/phpstan": "^2.0.0",
|
||||
"phpstan/phpstan-deprecation-rules": "^2.0.0",
|
||||
"phpstan/phpstan-phpunit": "^2.0.0",
|
||||
"phpstan/phpstan-strict-rules": "^2.0.0",
|
||||
"phpunit/phpunit": "^12.0.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Lcobucci\\Clock\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Luís Cobucci",
|
||||
"email": "lcobucci@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Yet another clock abstraction",
|
||||
"support": {
|
||||
"issues": "https://github.com/lcobucci/clock/issues",
|
||||
"source": "https://github.com/lcobucci/clock/tree/3.5.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/lcobucci",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://www.patreon.com/lcobucci",
|
||||
"type": "patreon"
|
||||
}
|
||||
],
|
||||
"time": "2025-10-27T09:03:17+00:00"
|
||||
},
|
||||
{
|
||||
"name": "lcobucci/jwt",
|
||||
"version": "5.6.0",
|
||||
@@ -1270,6 +1468,227 @@
|
||||
],
|
||||
"time": "2025-10-17T11:30:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/climate",
|
||||
"version": "3.10.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thephpleague/climate.git",
|
||||
"reference": "237f70e1032b16d32ff3f65dcda68706911e1c74"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thephpleague/climate/zipball/237f70e1032b16d32ff3f65dcda68706911e1c74",
|
||||
"reference": "237f70e1032b16d32ff3f65dcda68706911e1c74",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.3 || ^8.0",
|
||||
"psr/log": "^1.0 || ^2.0 || ^3.0",
|
||||
"seld/cli-prompt": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"mikey179/vfsstream": "^1.6.12",
|
||||
"mockery/mockery": "^1.6.12",
|
||||
"phpunit/phpunit": "^9.5.10",
|
||||
"squizlabs/php_codesniffer": "^3.10"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-mbstring": "If ext-mbstring is not available you MUST install symfony/polyfill-mbstring"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"League\\CLImate\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Joe Tannenbaum",
|
||||
"email": "hey@joe.codes",
|
||||
"homepage": "http://joe.codes/",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Craig Duncan",
|
||||
"email": "git@duncanc.co.uk",
|
||||
"homepage": "https://github.com/duncan3dc",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "PHP's best friend for the terminal. CLImate allows you to easily output colored text, special formats, and more.",
|
||||
"keywords": [
|
||||
"cli",
|
||||
"colors",
|
||||
"command",
|
||||
"php",
|
||||
"terminal"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/thephpleague/climate/issues",
|
||||
"source": "https://github.com/thephpleague/climate/tree/3.10.0"
|
||||
},
|
||||
"time": "2024-11-18T09:09:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/event",
|
||||
"version": "3.0.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thephpleague/event.git",
|
||||
"reference": "ec38ff7ea10cad7d99a79ac937fbcffb9334c210"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thephpleague/event/zipball/ec38ff7ea10cad7d99a79ac937fbcffb9334c210",
|
||||
"reference": "ec38ff7ea10cad7d99a79ac937fbcffb9334c210",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2.0",
|
||||
"psr/event-dispatcher": "^1.0"
|
||||
},
|
||||
"provide": {
|
||||
"psr/event-dispatcher-implementation": "1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^2.16",
|
||||
"phpstan/phpstan": "^0.12.45",
|
||||
"phpunit/phpunit": "^8.5"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"League\\Event\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Frank de Jonge",
|
||||
"email": "info@frenky.net"
|
||||
}
|
||||
],
|
||||
"description": "Event package",
|
||||
"keywords": [
|
||||
"emitter",
|
||||
"event",
|
||||
"listener"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/thephpleague/event/issues",
|
||||
"source": "https://github.com/thephpleague/event/tree/3.0.3"
|
||||
},
|
||||
"time": "2024-09-04T16:06:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/oauth2-server",
|
||||
"version": "9.3.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thephpleague/oauth2-server.git",
|
||||
"reference": "d8e2f39f645a82b207bbac441694d6e6079357cb"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thephpleague/oauth2-server/zipball/d8e2f39f645a82b207bbac441694d6e6079357cb",
|
||||
"reference": "d8e2f39f645a82b207bbac441694d6e6079357cb",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"defuse/php-encryption": "^2.4",
|
||||
"ext-json": "*",
|
||||
"ext-openssl": "*",
|
||||
"lcobucci/clock": "^2.3 || ^3.0",
|
||||
"lcobucci/jwt": "^5.0",
|
||||
"league/event": "^3.0",
|
||||
"league/uri": "^7.0",
|
||||
"php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
|
||||
"psr/http-message": "^2.0",
|
||||
"psr/http-server-middleware": "^1.0"
|
||||
},
|
||||
"replace": {
|
||||
"league/oauth2server": "*",
|
||||
"lncd/oauth2": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"laminas/laminas-diactoros": "^3.5",
|
||||
"php-parallel-lint/php-parallel-lint": "^1.3.2",
|
||||
"phpstan/extension-installer": "^1.3.1",
|
||||
"phpstan/phpstan": "^1.12|^2.0",
|
||||
"phpstan/phpstan-deprecation-rules": "^1.1.4|^2.0",
|
||||
"phpstan/phpstan-phpunit": "^1.3.15|^2.0",
|
||||
"phpstan/phpstan-strict-rules": "^1.5.2|^2.0",
|
||||
"phpunit/phpunit": "^10.5|^11.5|^12.0",
|
||||
"roave/security-advisories": "dev-master",
|
||||
"slevomat/coding-standard": "^8.14.1",
|
||||
"squizlabs/php_codesniffer": "^3.8"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"League\\OAuth2\\Server\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Alex Bilbie",
|
||||
"email": "hello@alexbilbie.com",
|
||||
"homepage": "http://www.alexbilbie.com",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Andy Millington",
|
||||
"email": "andrew@noexceptions.io",
|
||||
"homepage": "https://www.noexceptions.io",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "A lightweight and powerful OAuth 2.0 authorization and resource server library with support for all the core specification grants. This library will allow you to secure your API with OAuth and allow your applications users to approve apps that want to access their data from your API.",
|
||||
"homepage": "https://oauth2.thephpleague.com/",
|
||||
"keywords": [
|
||||
"Authentication",
|
||||
"api",
|
||||
"auth",
|
||||
"authorisation",
|
||||
"authorization",
|
||||
"oauth",
|
||||
"oauth 2",
|
||||
"oauth 2.0",
|
||||
"oauth2",
|
||||
"protect",
|
||||
"resource",
|
||||
"secure",
|
||||
"server"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/thephpleague/oauth2-server/issues",
|
||||
"source": "https://github.com/thephpleague/oauth2-server/tree/9.3.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/sephster",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-11-25T22:51:15+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/route",
|
||||
"version": "6.2.0",
|
||||
@@ -1415,6 +1834,188 @@
|
||||
},
|
||||
"time": "2021-02-14T15:29:04+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/uri",
|
||||
"version": "7.7.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thephpleague/uri.git",
|
||||
"reference": "8d587cddee53490f9b82bf203d3a9aa7ea4f9807"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thephpleague/uri/zipball/8d587cddee53490f9b82bf203d3a9aa7ea4f9807",
|
||||
"reference": "8d587cddee53490f9b82bf203d3a9aa7ea4f9807",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"league/uri-interfaces": "^7.7",
|
||||
"php": "^8.1",
|
||||
"psr/http-factory": "^1"
|
||||
},
|
||||
"conflict": {
|
||||
"league/uri-schemes": "^1.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-bcmath": "to improve IPV4 host parsing",
|
||||
"ext-dom": "to convert the URI into an HTML anchor tag",
|
||||
"ext-fileinfo": "to create Data URI from file contennts",
|
||||
"ext-gmp": "to improve IPV4 host parsing",
|
||||
"ext-intl": "to handle IDN host with the best performance",
|
||||
"ext-uri": "to use the PHP native URI class",
|
||||
"jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain",
|
||||
"league/uri-components": "Needed to easily manipulate URI objects components",
|
||||
"league/uri-polyfill": "Needed to backport the PHP URI extension for older versions of PHP",
|
||||
"php-64bit": "to improve IPV4 host parsing",
|
||||
"rowbot/url": "to handle WHATWG URL",
|
||||
"symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "7.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"League\\Uri\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ignace Nyamagana Butera",
|
||||
"email": "nyamsprod@gmail.com",
|
||||
"homepage": "https://nyamsprod.com"
|
||||
}
|
||||
],
|
||||
"description": "URI manipulation library",
|
||||
"homepage": "https://uri.thephpleague.com",
|
||||
"keywords": [
|
||||
"URN",
|
||||
"data-uri",
|
||||
"file-uri",
|
||||
"ftp",
|
||||
"hostname",
|
||||
"http",
|
||||
"https",
|
||||
"middleware",
|
||||
"parse_str",
|
||||
"parse_url",
|
||||
"psr-7",
|
||||
"query-string",
|
||||
"querystring",
|
||||
"rfc2141",
|
||||
"rfc3986",
|
||||
"rfc3987",
|
||||
"rfc6570",
|
||||
"rfc8141",
|
||||
"uri",
|
||||
"uri-template",
|
||||
"url",
|
||||
"ws"
|
||||
],
|
||||
"support": {
|
||||
"docs": "https://uri.thephpleague.com",
|
||||
"forum": "https://thephpleague.slack.com",
|
||||
"issues": "https://github.com/thephpleague/uri-src/issues",
|
||||
"source": "https://github.com/thephpleague/uri/tree/7.7.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/sponsors/nyamsprod",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-12-07T16:02:06+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/uri-interfaces",
|
||||
"version": "7.7.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thephpleague/uri-interfaces.git",
|
||||
"reference": "62ccc1a0435e1c54e10ee6022df28d6c04c2946c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/62ccc1a0435e1c54e10ee6022df28d6c04c2946c",
|
||||
"reference": "62ccc1a0435e1c54e10ee6022df28d6c04c2946c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-filter": "*",
|
||||
"php": "^8.1",
|
||||
"psr/http-message": "^1.1 || ^2.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-bcmath": "to improve IPV4 host parsing",
|
||||
"ext-gmp": "to improve IPV4 host parsing",
|
||||
"ext-intl": "to handle IDN host with the best performance",
|
||||
"php-64bit": "to improve IPV4 host parsing",
|
||||
"rowbot/url": "to handle WHATWG URL",
|
||||
"symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "7.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"League\\Uri\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ignace Nyamagana Butera",
|
||||
"email": "nyamsprod@gmail.com",
|
||||
"homepage": "https://nyamsprod.com"
|
||||
}
|
||||
],
|
||||
"description": "Common tools for parsing and resolving RFC3987/RFC3986 URI",
|
||||
"homepage": "https://uri.thephpleague.com",
|
||||
"keywords": [
|
||||
"data-uri",
|
||||
"file-uri",
|
||||
"ftp",
|
||||
"hostname",
|
||||
"http",
|
||||
"https",
|
||||
"parse_str",
|
||||
"parse_url",
|
||||
"psr-7",
|
||||
"query-string",
|
||||
"querystring",
|
||||
"rfc3986",
|
||||
"rfc3987",
|
||||
"rfc6570",
|
||||
"uri",
|
||||
"url",
|
||||
"ws"
|
||||
],
|
||||
"support": {
|
||||
"docs": "https://uri.thephpleague.com",
|
||||
"forum": "https://thephpleague.slack.com",
|
||||
"issues": "https://github.com/thephpleague/uri-src/issues",
|
||||
"source": "https://github.com/thephpleague/uri-interfaces/tree/7.7.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/sponsors/nyamsprod",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-12-07T16:03:21+00:00"
|
||||
},
|
||||
{
|
||||
"name": "monolog/monolog",
|
||||
"version": "3.9.0",
|
||||
@@ -1809,6 +2410,56 @@
|
||||
],
|
||||
"time": "2024-09-09T07:06:30+00:00"
|
||||
},
|
||||
{
|
||||
"name": "paragonie/random_compat",
|
||||
"version": "v9.99.100",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/paragonie/random_compat.git",
|
||||
"reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a",
|
||||
"reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">= 7"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "4.*|5.*",
|
||||
"vimeo/psalm": "^1"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
|
||||
},
|
||||
"type": "library",
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Paragon Initiative Enterprises",
|
||||
"email": "security@paragonie.com",
|
||||
"homepage": "https://paragonie.com"
|
||||
}
|
||||
],
|
||||
"description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
|
||||
"keywords": [
|
||||
"csprng",
|
||||
"polyfill",
|
||||
"pseudorandom",
|
||||
"random"
|
||||
],
|
||||
"support": {
|
||||
"email": "info@paragonie.com",
|
||||
"issues": "https://github.com/paragonie/random_compat/issues",
|
||||
"source": "https://github.com/paragonie/random_compat"
|
||||
},
|
||||
"time": "2020-10-15T08:29:30+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpdoc-parser",
|
||||
"version": "2.3.0",
|
||||
@@ -2020,6 +2671,56 @@
|
||||
},
|
||||
"time": "2021-11-05T16:47:00+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/event-dispatcher",
|
||||
"version": "1.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/event-dispatcher.git",
|
||||
"reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0",
|
||||
"reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\EventDispatcher\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "http://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "Standard interfaces for event handling.",
|
||||
"keywords": [
|
||||
"events",
|
||||
"psr",
|
||||
"psr-14"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/php-fig/event-dispatcher/issues",
|
||||
"source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0"
|
||||
},
|
||||
"time": "2019-01-08T18:20:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/http-client",
|
||||
"version": "1.0.3",
|
||||
@@ -2828,6 +3529,61 @@
|
||||
},
|
||||
"time": "2020-12-06T19:13:21+00:00"
|
||||
},
|
||||
{
|
||||
"name": "seld/cli-prompt",
|
||||
"version": "1.0.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Seldaek/cli-prompt.git",
|
||||
"reference": "b8dfcf02094b8c03b40322c229493bb2884423c5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Seldaek/cli-prompt/zipball/b8dfcf02094b8c03b40322c229493bb2884423c5",
|
||||
"reference": "b8dfcf02094b8c03b40322c229493bb2884423c5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^0.12.63"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Seld\\CliPrompt\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jordi Boggiano",
|
||||
"email": "j.boggiano@seld.be"
|
||||
}
|
||||
],
|
||||
"description": "Allows you to prompt for user input on the command line, and optionally hide the characters they type",
|
||||
"keywords": [
|
||||
"cli",
|
||||
"console",
|
||||
"hidden",
|
||||
"input",
|
||||
"prompt"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/Seldaek/cli-prompt/issues",
|
||||
"source": "https://github.com/Seldaek/cli-prompt/tree/1.0.4"
|
||||
},
|
||||
"time": "2020-12-15T21:32:01+00:00"
|
||||
},
|
||||
{
|
||||
"name": "siteworxpro/config",
|
||||
"version": "1.1.1",
|
||||
@@ -6844,7 +7600,8 @@
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
"php": "^8.5"
|
||||
"php": "^8.5",
|
||||
"ext-sodium": "*"
|
||||
},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.9.0"
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
drop table if exists client_users;
|
||||
drop table if exists client_scopes;
|
||||
drop table if exists scopes;
|
||||
drop table if exists client_redirect_uris;
|
||||
drop table if exists clients;
|
||||
drop table if exists users;
|
||||
@@ -1,3 +1,63 @@
|
||||
create table clients
|
||||
(
|
||||
id uuid default gen_random_uuid()
|
||||
constraint client_pk
|
||||
primary key,
|
||||
client_id varchar not null
|
||||
constraint client_client_id_key
|
||||
unique,
|
||||
client_secret varchar not null,
|
||||
name varchar not null,
|
||||
description varchar default '',
|
||||
private_key text not null,
|
||||
encryption_key text not null,
|
||||
grant_types jsonb not null default '[]'::jsonb,
|
||||
capabilities jsonb not null default '[]'::jsonb,
|
||||
confidential boolean not null default true,
|
||||
created_at timestamp default now(),
|
||||
updated_at timestamp default now()
|
||||
);
|
||||
|
||||
create table client_redirect_uris
|
||||
(
|
||||
id uuid default gen_random_uuid()
|
||||
constraint client_redirect_uris_pk
|
||||
primary key,
|
||||
client_id uuid not null
|
||||
constraint client_redirect_uris_client_id_fk
|
||||
references clients
|
||||
on delete cascade,
|
||||
redirect_uri varchar not null
|
||||
);
|
||||
|
||||
create table scopes
|
||||
(
|
||||
id uuid default gen_random_uuid()
|
||||
constraint scopes_pk
|
||||
primary key,
|
||||
name varchar not null
|
||||
constraint scopes_name_key
|
||||
unique,
|
||||
description varchar
|
||||
);
|
||||
|
||||
create table client_scopes
|
||||
(
|
||||
id uuid default gen_random_uuid()
|
||||
constraint client_scopes_pk
|
||||
primary key,
|
||||
client_id uuid not null
|
||||
constraint client_scopes_client_id_fk
|
||||
references clients
|
||||
on delete cascade,
|
||||
scope_id uuid not null
|
||||
constraint client_scopes_scope_id_fk
|
||||
references scopes
|
||||
on delete cascade,
|
||||
constraint client_scopes_client_id_scope_id_key
|
||||
unique (client_id, scope_id)
|
||||
);
|
||||
|
||||
create table users
|
||||
(
|
||||
id uuid default gen_random_uuid()
|
||||
@@ -9,5 +69,23 @@ create table users
|
||||
constraint users_email_key
|
||||
unique,
|
||||
password varchar not null,
|
||||
created_at timestamp default now()
|
||||
created_at timestamp default now(),
|
||||
updated_at timestamp default now()
|
||||
);
|
||||
|
||||
create table client_users
|
||||
(
|
||||
id uuid default gen_random_uuid()
|
||||
constraint client_users_pk
|
||||
primary key,
|
||||
client_id uuid not null
|
||||
constraint client_users_client_id_fk
|
||||
references clients
|
||||
on delete cascade,
|
||||
user_id uuid not null
|
||||
constraint client_users_user_id_fk
|
||||
references users
|
||||
on delete cascade,
|
||||
constraint client_users_client_id_user_id_key
|
||||
unique (client_id, user_id)
|
||||
);
|
||||
|
||||
1
front-end/.env.development
Normal file
1
front-end/.env.development
Normal file
@@ -0,0 +1 @@
|
||||
VITE_AUTH_URL=https://auth.dev.int
|
||||
1
front-end/.env.production
Normal file
1
front-end/.env.production
Normal file
@@ -0,0 +1 @@
|
||||
VITE_AUTH_URL=https://auth.careeruprising.com
|
||||
1
front-end/.env.sandbox
Normal file
1
front-end/.env.sandbox
Normal file
@@ -0,0 +1 @@
|
||||
VITE_AUTH_URL=https://auth.sandbox.careeruprising.com
|
||||
26
front-end/.gitignore
vendored
Normal file
26
front-end/.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
.env
|
||||
1
front-end/.nvmrc
Normal file
1
front-end/.nvmrc
Normal file
@@ -0,0 +1 @@
|
||||
v22.14.0
|
||||
BIN
front-end/assets/favicon.ico
Executable file
BIN
front-end/assets/favicon.ico
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
15
front-end/index.html
Normal file
15
front-end/index.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<link rel="icon" href="./assets/favicon.ico"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Career UpRising - Login</title>
|
||||
<script src="https://kit.fontawesome.com/a0fd516b66.js" crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="/src/style.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
2990
front-end/package-lock.json
generated
Normal file
2990
front-end/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
front-end/package.json
Normal file
32
front-end/package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "front-end",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build --emptyOutDir",
|
||||
"preview": "vite preview",
|
||||
"clean": "rm -Rf node_modules && rm -Rf ../public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@primevue/themes": "^4.0.5",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"@vuelidate/core": "^2.0.3",
|
||||
"@vuelidate/validators": "^2.0.4",
|
||||
"axios": "^1.13.2",
|
||||
"lodash": "^4.17.21",
|
||||
"primevue": "^4.4.1",
|
||||
"vue": "^3.5.24",
|
||||
"vue-router": "^4.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.2",
|
||||
"sass": "^1.94.2",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.2.4",
|
||||
"vue-tsc": "^3.1.4"
|
||||
}
|
||||
}
|
||||
14
front-end/src/App.vue
Normal file
14
front-end/src/App.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<Toast/>
|
||||
<router-view />
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import {defineComponent} from "vue";
|
||||
|
||||
export default defineComponent({})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
29
front-end/src/main.ts
Normal file
29
front-end/src/main.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import ToastService from 'primevue/toastservice'
|
||||
import theme from '@primevue/themes/nora'
|
||||
import router from './router'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Button from 'primevue/button'
|
||||
import Toast from 'primevue/toast'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(PrimeVue, {
|
||||
theme: {
|
||||
preset: theme,
|
||||
options: {
|
||||
prefix: 'p-',
|
||||
darkModeSelector: 'off',
|
||||
cssLayer: false
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.use(ToastService);
|
||||
app.use(router)
|
||||
app.component('InputText', InputText)
|
||||
app.component('Button', Button)
|
||||
app.component('Toast', Toast)
|
||||
|
||||
app.mount('#app')
|
||||
56
front-end/src/pages/error.vue
Normal file
56
front-end/src/pages/error.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="flex flex-row justify-center md:p-10">
|
||||
<Card class="p-10 w-full md:w-2/3 lg:w-1/2 xl:w-5/12">
|
||||
<template #header>
|
||||
<h1 class="text-3xl font-bold text-center mb-4">
|
||||
Oh No, An Error!
|
||||
</h1>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="text-center">
|
||||
<i class="fa-solid fa-triangle-exclamation text-6xl text-yellow-500 mb-8"></i>
|
||||
<p class="mb-8">Error: {{ getErrorMessage() }}</p>
|
||||
<p>
|
||||
Please check your configuration or contact support if the issue persists.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<p class="text-xs text-center text-gray-500 mt-10">
|
||||
Authentication Portal ::
|
||||
<i class="text-xs fa-sharp fa-thin fa-copyright"></i> {{ date() }} :: {{ version() }}
|
||||
</p>
|
||||
<p class="text-center text-gray-500 mt-10">
|
||||
<i class="fa-regular text-2xl fa-shield-keyhole"></i>
|
||||
</p>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import {defineAsyncComponent} from "vue";
|
||||
|
||||
const Card = defineAsyncComponent(() => import("primevue/card"));
|
||||
|
||||
function date() {
|
||||
return new Date().getFullYear()
|
||||
}
|
||||
|
||||
function version (): string {
|
||||
return import.meta.env.VITE_VERSION || 'dev-master'
|
||||
}
|
||||
|
||||
function getErrorMessage(): string {
|
||||
const status = new URLSearchParams(window.location.search).get('e') || '';
|
||||
|
||||
switch (status) {
|
||||
case 'invalid_client':
|
||||
return 'Unknown client. Please check the client ID and try again.';
|
||||
case 'unsupported_grant_type':
|
||||
return 'The authentication method is not supported. Please contact support.';
|
||||
default:
|
||||
return 'Sorry, something went wrong on our end. Please try again later.';
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
290
front-end/src/pages/login.vue
Normal file
290
front-end/src/pages/login.vue
Normal file
@@ -0,0 +1,290 @@
|
||||
<template>
|
||||
<div class="flex flex-row justify-center md:p-10">
|
||||
<Card class="p-10 w-full md:w-2/3 lg:w-1/2 xl:w-5/12">
|
||||
<template #header>
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div>
|
||||
<Image width="300"
|
||||
src="https://i.careeruprising.com/_Pa5TnsUJ5v-EHQQZy3BHnbaiCjMGxusd7qNcvhd8jA/pr:sm/sm:1/enc/Ec8S-CxpyLc2M5XdibEf85vGU5KNfdR0Dx8Qf6DI2nbZG85hSSFtDV7TuynR5djSw5jhdTIyjd5xDX5z-Dgemw"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-2xl mt-5">
|
||||
<span v-if="capabilities.client_name !== ''">{{ capabilities.client_name }}</span>
|
||||
<span v-else>Login Portal</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<div v-if="capabilities.userPass" @keydown.enter="login">
|
||||
<div>
|
||||
<InputText
|
||||
v-model="form.email"
|
||||
:invalid="v$.$dirty && v$.form.email.$invalid"
|
||||
class="w-full"
|
||||
placeholder="Email"
|
||||
/>
|
||||
<label v-if="v$.$dirty && v$.form.email.required.$invalid" class="text-xs text-red-600">Please enter an
|
||||
email address</label>
|
||||
<label v-if="v$.$dirty && v$.form.email.email.$invalid" class="text-xs text-red-600">Email address is
|
||||
invalid</label>
|
||||
</div>
|
||||
|
||||
<div v-if="!magicLink" class="mt-5">
|
||||
<InputText
|
||||
v-model="form.password"
|
||||
:invalid="v$.$dirty && v$.form.password.$invalid"
|
||||
class="w-full"
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
/>
|
||||
<label v-if="v$.$dirty && v$.form.password.required.$invalid" class="text-xs text-red-600">Please enter a
|
||||
password</label>
|
||||
</div>
|
||||
<div v-if="capabilities.magicLogin" class="mt-5">
|
||||
<div class="flex items-center">
|
||||
<Checkbox id="magic-link" v-model="magicLink" binary class="mr-3" />
|
||||
<label for="magic-link">
|
||||
Use Magic Login (Password-less)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!magicLink && capabilities.userPass">
|
||||
<Button
|
||||
class="w-full mt-5"
|
||||
:loading="loading"
|
||||
raised
|
||||
@click="login"
|
||||
icon="fa-regular fa-sharp fa-right-to-bracket"
|
||||
label="Login"
|
||||
/>
|
||||
|
||||
<p class="text-center mt-5">
|
||||
<router-link
|
||||
to="/password-reset"
|
||||
class="p-button p-button-raised p-button-secondary w-full"
|
||||
>
|
||||
<i class="fa-light fa-sharp fa-lock"></i>
|
||||
Reset Password
|
||||
</router-link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="magicLink">
|
||||
<Button
|
||||
class="w-full mt-5"
|
||||
:loading="loading"
|
||||
raised
|
||||
label="Send Magic Link"
|
||||
@click="sendMagicLink"
|
||||
icon="fa-light fa-wand-magic-sparkles"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="capabilities.socials && Object.keys(capabilities.socials).length > 0" class="mt-5">
|
||||
<div class="text-center mt-5 mb-5">
|
||||
<div class="mb-5 w-1/4 ml-auto mr-auto" style="border-bottom: 1px solid rgba(156,134,134,0.27)" />
|
||||
Social Logins
|
||||
</div>
|
||||
<div class="flex justify-around mt-5">
|
||||
<Button style="display: none" />
|
||||
<a v-if="capabilities.socials.google" :href="capabilities.socials.google.redirectUrl" class="p-button"
|
||||
style="background-color: #de5246">
|
||||
<i class="fa-brands fa-google mr-2"></i> Google
|
||||
</a>
|
||||
<a v-if="capabilities.socials.linkedIn" :href="capabilities.socials.linkedIn.redirectUrl" class="p-button"
|
||||
style="background-color: #55ACEE">
|
||||
<i class="fa-brands fa-linkedin mr-2"></i> LinkedIn
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<p class="text-xs text-center text-gray-500 mt-10">
|
||||
Career Uprising, Inc :: Authentication Portal ::
|
||||
<i class="text-xs fa-sharp fa-thin fa-copyright"></i> {{ date() }} :: {{ version() }}
|
||||
</p>
|
||||
<p class="text-center text-gray-500 mt-10">
|
||||
<i class="fa-regular text-2xl fa-shield-keyhole"></i>
|
||||
</p>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { ToastMessageOptions } from 'primevue/toast'
|
||||
import Image from 'primevue/image'
|
||||
import Card from 'primevue/card'
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { email, required } from '@vuelidate/validators'
|
||||
|
||||
interface Data {
|
||||
form: {
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
magicLink: boolean
|
||||
loading: boolean
|
||||
capabilities: {
|
||||
client_name: string
|
||||
userPass: boolean
|
||||
magicLogin: boolean
|
||||
socials: {
|
||||
linkedIn?: socialProvider
|
||||
google?: socialProvider
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface socialProvider {
|
||||
provider: string
|
||||
clientId: string
|
||||
redirectUrl: string
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Image,
|
||||
Card,
|
||||
Checkbox,
|
||||
},
|
||||
setup () {
|
||||
return { v$: useVuelidate() }
|
||||
},
|
||||
data: (): Data => ({
|
||||
form: {
|
||||
email: '',
|
||||
password: '',
|
||||
},
|
||||
magicLink: false,
|
||||
loading: false,
|
||||
capabilities: {
|
||||
client_name: '',
|
||||
userPass: false,
|
||||
magicLogin: false,
|
||||
socials: {},
|
||||
},
|
||||
}),
|
||||
validations: {
|
||||
form: {
|
||||
email: {
|
||||
required,
|
||||
email,
|
||||
},
|
||||
password: {
|
||||
required: function (value: string) {
|
||||
// @ts-ignore
|
||||
if (!this.magicLink) {
|
||||
return value !== ''
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.getCapabilities()
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const error = urlParams.get('error')
|
||||
|
||||
// response type === nil means no auth needed. send the user
|
||||
// back through the flow
|
||||
const responseType = urlParams.get('response_type')
|
||||
const redirectUri = urlParams.get('redirect_url')
|
||||
if (responseType === 'nil') {
|
||||
window.location.href = redirectUri || '/'
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (error) {
|
||||
this.$toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Failed',
|
||||
detail: error,
|
||||
life: 5000,
|
||||
closable: false,
|
||||
} as ToastMessageOptions)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
date() {
|
||||
return new Date().getFullYear()
|
||||
},
|
||||
version (): string {
|
||||
return import.meta.env.VITE_VERSION || 'dev-master'
|
||||
},
|
||||
getCapabilities () {
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
|
||||
axios.get(`/client/capabilities?client_id=${urlParams.get('client_id')}`).then((response) => {
|
||||
this.capabilities = response.data
|
||||
})
|
||||
},
|
||||
sendMagicLink () {
|
||||
this.v$.$touch()
|
||||
|
||||
if (this.v$.$error) {
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
axios.post('/magic-link', {
|
||||
email: this.form.email,
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
this.v$.$reset()
|
||||
}).then(() => {
|
||||
this.$toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Magic Link Sent',
|
||||
detail: 'A Link sent to your email if it exists.',
|
||||
life: 3000,
|
||||
})
|
||||
}).catch(() => {
|
||||
this.$toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'An error occurred. Please try again later.',
|
||||
life: 3000,
|
||||
})
|
||||
})
|
||||
},
|
||||
login () {
|
||||
|
||||
if (this.magicLink) {
|
||||
return this.sendMagicLink()
|
||||
}
|
||||
|
||||
this.v$.$touch()
|
||||
|
||||
if (this.v$.$error) {
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
|
||||
axios.post('/login', this.form).then(r => {
|
||||
window.location.href = r.data.location
|
||||
}).catch(() => {
|
||||
this.$toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Failed',
|
||||
detail: 'The Login Has Failed',
|
||||
life: 5000,
|
||||
closable: false,
|
||||
} as ToastMessageOptions)
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
this.v$.$reset()
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
101
front-end/src/pages/password-reset.vue
Normal file
101
front-end/src/pages/password-reset.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div class="flex flex-row justify-center p-10">
|
||||
<Card class="p-10 w-full md:w-1/2">
|
||||
<template #header>
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div>
|
||||
<Image width="300"
|
||||
src="https://i.careeruprising.com/_Pa5TnsUJ5v-EHQQZy3BHnbaiCjMGxusd7qNcvhd8jA/pr:sm/sm:1/enc/Ec8S-CxpyLc2M5XdibEf85vGU5KNfdR0Dx8Qf6DI2nbZG85hSSFtDV7TuynR5djSw5jhdTIyjd5xDX5z-Dgemw"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-1xl mt-5">
|
||||
Password Reset
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<InputText class="w-full" placeholder="Email" v-model="form.email" />
|
||||
</template>
|
||||
<template #footer>
|
||||
<div>
|
||||
<Button raised :loading="loading" class="w-full" @click="sendReset">
|
||||
<template #default>
|
||||
<span class="p-button-label">
|
||||
<i class="fa-regular fa-paper-plane"></i>
|
||||
Send Reset
|
||||
</span>
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
<router-link class="w-full p-button p-button-raised p-component p-button-secondary mt-5" to="/">
|
||||
<span class="p-button-label">
|
||||
<i class="fa-sharp fa-regular fa-hand-point-left"></i>
|
||||
Back
|
||||
</span>
|
||||
</router-link>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import Image from 'primevue/image'
|
||||
import Card from 'primevue/card'
|
||||
import axios from 'axios'
|
||||
|
||||
interface Data {
|
||||
loading: boolean
|
||||
form: {
|
||||
email: string
|
||||
}
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Image,
|
||||
Card,
|
||||
},
|
||||
data: (): Data => ({
|
||||
loading: false,
|
||||
form: {
|
||||
email: '',
|
||||
},
|
||||
}),
|
||||
methods: {
|
||||
sendReset () {
|
||||
if (this.form.email === '') {
|
||||
this.$toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Please complete all fields.',
|
||||
life: 3000,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
|
||||
axios.post('/password-reset', this.form).then(() => {
|
||||
this.$toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Password Reset',
|
||||
detail: 'A link was sent to your email if it exists.',
|
||||
life: 3000,
|
||||
})
|
||||
|
||||
this.$router.push('/')
|
||||
}).catch(() => {
|
||||
this.$toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'An error occurred. Please try again later.',
|
||||
life: 3000,
|
||||
})
|
||||
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
148
front-end/src/pages/update-password.vue
Normal file
148
front-end/src/pages/update-password.vue
Normal file
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<div class="flex flex-row justify-center p-10">
|
||||
<Card class="p-10 w-full md:w-1/2">
|
||||
<template #header>
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div>
|
||||
<Image width="300"
|
||||
src="https://i.careeruprising.com/_Pa5TnsUJ5v-EHQQZy3BHnbaiCjMGxusd7qNcvhd8jA/pr:sm/sm:1/enc/Ec8S-CxpyLc2M5XdibEf85vGU5KNfdR0Dx8Qf6DI2nbZG85hSSFtDV7TuynR5djSw5jhdTIyjd5xDX5z-Dgemw"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-1xl mt-5">
|
||||
Password Reset
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="w-full">
|
||||
<Password
|
||||
toggleMask
|
||||
required
|
||||
inputClass="w-full"
|
||||
v-model="password"
|
||||
placeholder="Password"
|
||||
class="w-full"
|
||||
ref="passwordField"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<InputText class="w-full" type="password" v-model="passwordAgain" placeholder="Password Again"/>
|
||||
</div>
|
||||
<div v-if="toStrong" class="mt-3">
|
||||
<Message :value="true" severity="error">Your Password is To Strong</Message>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<Button
|
||||
:loading="loading"
|
||||
:disabled="!formValid"
|
||||
class="w-full"
|
||||
label="Update Password"
|
||||
@click="updatePassword"
|
||||
/>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import {defineComponent} from "vue";
|
||||
import Image from 'primevue/image'
|
||||
import Card from 'primevue/card'
|
||||
import Password from 'primevue/password'
|
||||
import Message from 'primevue/message'
|
||||
import axios from "axios";
|
||||
|
||||
interface Data {
|
||||
password: string
|
||||
passwordAgain: string
|
||||
loading: boolean
|
||||
toStrong: boolean
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Image,
|
||||
Card,
|
||||
Password,
|
||||
Message
|
||||
},
|
||||
data: (): Data => ({
|
||||
password: '',
|
||||
passwordAgain: '',
|
||||
loading: false,
|
||||
toStrong: false,
|
||||
}),
|
||||
created() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
axios.get('/password-reset?k=' + urlParams.get('k'))
|
||||
.catch(() => {
|
||||
this.$toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Key is no longer valid. Please make your request again.',
|
||||
life: 5000,
|
||||
})
|
||||
|
||||
this.$router.push('/')
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
formValid(): boolean {
|
||||
this.toStrong = false
|
||||
|
||||
if (this.password === '' || this.passwordAgain === '') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.password !== this.passwordAgain) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.password.length < 8) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.password.toLowerCase.toString() === 'chucknorris' || this.password.toLowerCase.toString()=== 'chuck norris') {
|
||||
this.toStrong = true
|
||||
return false
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return this.$refs.passwordField.$data.meter?.strength !== null && this.$refs.passwordField.$data.meter?.strength !== 'weak'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updatePassword() {
|
||||
this.loading = true
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
axios.put('/password-reset', {
|
||||
password: this.password,
|
||||
k: urlParams.get('k')
|
||||
}).then(() => {
|
||||
this.$toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Password Reset',
|
||||
detail: 'Your password has been updated.',
|
||||
life: 5000,
|
||||
})
|
||||
|
||||
this.$router.push('/')
|
||||
})
|
||||
.catch(() => {
|
||||
this.$toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'An error occurred. Please try again later.',
|
||||
life: 5000,
|
||||
})
|
||||
|
||||
this.loading = false
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
15
front-end/src/router/index.ts
Normal file
15
front-end/src/router/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { createWebHashHistory, createRouter } from 'vue-router'
|
||||
|
||||
const routes = [
|
||||
{ path: '/', component: () => import('../pages/login.vue') },
|
||||
{ path: '/error', component: () => import('../pages/error.vue') },
|
||||
{ path: '/password-reset', component: () => import('../pages/password-reset.vue') },
|
||||
{ path: '/update-password', component: () => import('../pages/update-password.vue') },
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes,
|
||||
})
|
||||
|
||||
export default router
|
||||
64
front-end/src/style.css
Normal file
64
front-end/src/style.css
Normal file
@@ -0,0 +1,64 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
body {
|
||||
background-color: #474747;
|
||||
}
|
||||
|
||||
:root {
|
||||
color: #444;
|
||||
font-family: "Poppins", sans-serif;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-size: 13px;
|
||||
|
||||
--color-primary: #304d63;
|
||||
--color-primary-500: #426f8a;
|
||||
--color-secondary: #b3e7e8;
|
||||
--color-success: #8fb9aa;
|
||||
--color-success-900: #479881;
|
||||
--color-error: #dc5e5e;
|
||||
--color-warning: #f2d196;
|
||||
|
||||
--p--inputtext-border-radius: 0 !important;
|
||||
--p--card-border-radius: 0 !important;
|
||||
--p--select-border-radius: 0 !important;
|
||||
|
||||
--p--button-primary-background: var(--color-primary) !important;
|
||||
--p--button-primary-hover-background: var(--color-primary-500) !important;
|
||||
--p--button-primary-active-background: var(--color-primary) !important;
|
||||
|
||||
--p--button-secondary-background: var(--color-secondary) !important;
|
||||
|
||||
--p--button-info-background: #ed8975 !important;
|
||||
--p--button-info-hover-background: #ea9b8c !important;
|
||||
--p--button-info-border-color: #ed8975 !important;
|
||||
--p--button-info-hover-border-color: #ed8975 !important;
|
||||
|
||||
--p--button-danger-background: var(--color-error) !important;
|
||||
--p--button-danger-border-color: #ffffff !important;
|
||||
|
||||
--p--button-label-font-weight: 100 !important;
|
||||
|
||||
--p--message-error-background: var(--color-error) !important;
|
||||
--p--message-error-border-color: #ffffff !important;
|
||||
|
||||
--p--button-help-background: #4d95df !important;
|
||||
--p--button-help-hover-background: #2880d6 !important;
|
||||
--p--button-help-border-color: #84b0d8 !important;
|
||||
--p--button-help-hover-border-color: #4f799f !important;
|
||||
|
||||
|
||||
--p--toast-success-background: var(--color-success-900) !important;
|
||||
|
||||
--p--toast-info-background: var(--color-secondary) !important;
|
||||
--p--toast-info-color: #4a2525 !important;
|
||||
--p--toast-info-detail-color: #4a3131 !important;
|
||||
--p--toast-info-border-color: #3d5875 !important;
|
||||
--p--toast-info-close-button-hover-background: #ed8975 !important;
|
||||
|
||||
--p--toast-warn-background: var(--color-warning) !important;
|
||||
--p--toast-warn-color: #712828 !important;
|
||||
--p--toast-warn-detail-color: #4a3131 !important;
|
||||
|
||||
--p--toast-error-background: var(--color-error) !important;
|
||||
}
|
||||
1
front-end/src/vite-env.d.ts
vendored
Normal file
1
front-end/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
12
front-end/tailwind.config.js
Normal file
12
front-end/tailwind.config.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{vue,js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
25
front-end/tsconfig.json
Normal file
25
front-end/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
10
front-end/tsconfig.node.json
Normal file
10
front-end/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
10
front-end/vite.config.ts
Normal file
10
front-end/vite.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue(), tailwindcss()],
|
||||
build: {
|
||||
outDir: '../public'
|
||||
}
|
||||
})
|
||||
10
src/Api.php
10
src/Api.php
@@ -9,6 +9,8 @@ use League\Route\Http\Exception\NotFoundException;
|
||||
use League\Route\RouteGroup;
|
||||
use League\Route\Router;
|
||||
use Nyholm\Psr7\Factory\Psr17Factory;
|
||||
use Siteworxpro\App\Controllers\AuthorizeController;
|
||||
use Siteworxpro\App\Controllers\CapabilitiesController;
|
||||
use Siteworxpro\App\Controllers\HealthcheckController;
|
||||
use Siteworxpro\App\Controllers\IndexController;
|
||||
use Siteworxpro\App\Controllers\OpenApiController;
|
||||
@@ -71,8 +73,6 @@ class Api
|
||||
);
|
||||
|
||||
$this->router = new Router();
|
||||
$this->router->get('/', IndexController::class . '::get');
|
||||
$this->router->post('/', IndexController::class . '::post');
|
||||
$this->router->get('/healthz', HealthcheckController::class . '::get');
|
||||
|
||||
$this->router->group('/.well-known', function (RouteGroup $router) {
|
||||
@@ -80,6 +80,12 @@ class Api
|
||||
$router->get('/swagger.json', OpenApiController::class . '::get');
|
||||
});
|
||||
|
||||
$this->router->group('/client', function (RouteGroup $group) {
|
||||
$group->get('/capabilities', CapabilitiesController::class . '::get');
|
||||
});
|
||||
|
||||
$this->router->get('/authorize', AuthorizeController::class . '::get');
|
||||
|
||||
$this->router->middleware(new CorsMiddleware());
|
||||
$this->router->middleware(new JwtMiddleware());
|
||||
$this->router->middleware(new ScopeMiddleware());
|
||||
|
||||
@@ -5,12 +5,11 @@ declare(strict_types=1);
|
||||
namespace Siteworxpro\App\Cli;
|
||||
|
||||
use Ahc\Cli\Application;
|
||||
use Siteworxpro\App\Cli\Commands\DemoCommand;
|
||||
use Siteworxpro\App\Cli\Commands\OAuth\AddRedirectUri;
|
||||
use Siteworxpro\App\Cli\Commands\OAuth\CreateClient;
|
||||
use Siteworxpro\App\Cli\Commands\Queue\Start;
|
||||
use Siteworxpro\App\Cli\Commands\Queue\TestJob;
|
||||
use Siteworxpro\App\Helpers\Version;
|
||||
use Siteworxpro\App\Kernel;
|
||||
use Siteworxpro\App\Services\Facades\Config;
|
||||
|
||||
class App
|
||||
{
|
||||
@@ -22,11 +21,11 @@ class App
|
||||
public function __construct()
|
||||
{
|
||||
Kernel::boot();
|
||||
$this->app = new Application('Php-Template', Version::VERSION);
|
||||
$this->app = new Application('Php-Auth', Version::VERSION);
|
||||
|
||||
$this->app->add(new DemoCommand());
|
||||
$this->app->add(new CreateClient());
|
||||
$this->app->add(new AddRedirectUri());
|
||||
$this->app->add(new Start());
|
||||
$this->app->add(new TestJob());
|
||||
}
|
||||
|
||||
public function run(): int
|
||||
|
||||
20
src/Cli/Commands/Command.php
Normal file
20
src/Cli/Commands/Command.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Cli\Commands;
|
||||
|
||||
use Ahc\Cli\Application as App;
|
||||
use League\CLImate\CLImate;
|
||||
|
||||
abstract class Command extends \Ahc\Cli\Input\Command
|
||||
{
|
||||
protected Climate $climate;
|
||||
|
||||
public function __construct(string $_name, string $_desc = '', bool $_allowUnknown = false, ?App $_app = null)
|
||||
{
|
||||
parent::__construct($_name, $_desc, $_allowUnknown, $_app);
|
||||
|
||||
$this->climate = new CLImate();
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Cli\Commands;
|
||||
|
||||
use Ahc\Cli\Input\Command;
|
||||
use Siteworxpro\App\CommandBus\Commands\ExampleCommand;
|
||||
use Siteworxpro\App\Services\Facades\CommandBus;
|
||||
|
||||
class DemoCommand extends Command implements CommandInterface
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('api:demo', 'A demo command to showcase the CLI functionality.');
|
||||
|
||||
$this->argument('[name]', 'Your name')
|
||||
->option('-g, --greet', 'Include a greeting message');
|
||||
}
|
||||
|
||||
public function execute(): int
|
||||
{
|
||||
$pb = $this->progress(100);
|
||||
|
||||
for ($i = 0; $i < 100; $i += 10) {
|
||||
usleep(100000); // Simulate work
|
||||
$pb->advance(10);
|
||||
}
|
||||
|
||||
$pb->finish();
|
||||
|
||||
$this->writer()->boldBlue("Demo Command Executed!\n");
|
||||
$name = $this->values()['name'];
|
||||
$greet = $this->values()['greet'] ?? false;
|
||||
|
||||
if ($greet) {
|
||||
$this->writer()->green("Hello, $name! Welcome to the CLI demo.\n");
|
||||
} else {
|
||||
$exampleCommand = new ExampleCommand($name);
|
||||
$this->writer()->yellow(CommandBus::handle($exampleCommand));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
62
src/Cli/Commands/OAuth/AddRedirectUri.php
Normal file
62
src/Cli/Commands/OAuth/AddRedirectUri.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Cli\Commands\OAuth;
|
||||
|
||||
use League\CLImate\TerminalObject\Dynamic\Input;
|
||||
use Siteworxpro\App\Models\ClientRedirectUri;
|
||||
use Siteworxpro\App\OAuth\Entities\Client;
|
||||
|
||||
class AddRedirectUri extends \Siteworxpro\App\Cli\Commands\Command
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('oauth:redirect-uri:add', 'Add a redirect URI to an existing OAuth client.');
|
||||
}
|
||||
|
||||
public function execute(): int
|
||||
{
|
||||
$clients = Client::all('id', 'name');
|
||||
|
||||
/** @var Input $input */
|
||||
$input = $this->climate->input(
|
||||
'Select the OAuth client to add a redirect URI to' . PHP_EOL .
|
||||
$clients->map(fn(Client $client) => "[$client->id $client->name]")->implode(PHP_EOL) .
|
||||
PHP_EOL .
|
||||
'Enter the client ID: '
|
||||
);
|
||||
$input->accept(
|
||||
$clients->pluck('id')->toArray()
|
||||
);
|
||||
|
||||
$id = $input->prompt();
|
||||
|
||||
$client = Client::find($id);
|
||||
if (!$client) {
|
||||
$this->climate->error('Client not found.');
|
||||
return 1;
|
||||
}
|
||||
|
||||
/** @var Input $uriInput */
|
||||
$uriInput = $this->climate->input('Enter the redirect URI to add: ');
|
||||
$uriInput->accept(function (string $value) {
|
||||
return filter_var($value, FILTER_VALIDATE_URL) !== false;
|
||||
}, 'Please enter a valid URL.');
|
||||
|
||||
$redirectUri = $uriInput->prompt();
|
||||
|
||||
$redirectUris = $client->clientRedirectUris;
|
||||
if (in_array($redirectUri, $redirectUris->toArray(), true)) {
|
||||
$this->climate->error('The redirect URI already exists for this client.');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$clientRedirectUri = new ClientRedirectUri();
|
||||
$clientRedirectUri->client_id = $client->id;
|
||||
$clientRedirectUri->redirect_uri = $redirectUri;
|
||||
$clientRedirectUri->save();
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
48
src/Cli/Commands/OAuth/CreateClient.php
Normal file
48
src/Cli/Commands/OAuth/CreateClient.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Cli\Commands\OAuth;
|
||||
|
||||
use Ahc\Cli\IO\Interactor;
|
||||
use Siteworxpro\App\CommandBus\Commands\CreateClient as CreateClientCommand;
|
||||
use Siteworxpro\App\CommandBus\Exceptions\CommandHandlerException;
|
||||
use Siteworxpro\App\OAuth\Entities\Client;
|
||||
use Siteworxpro\App\Services\Facades\CommandBus;
|
||||
|
||||
class CreateClient extends \Siteworxpro\App\Cli\Commands\Command
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('oauth:client:create', 'Create a new OAuth client.');
|
||||
}
|
||||
|
||||
public function execute(): int
|
||||
{
|
||||
$interactor = new Interactor();
|
||||
$clientName = $interactor->prompt('Enter client name');
|
||||
$clientDescription = $interactor->prompt('Enter client description (optional)', '');
|
||||
$clientGrantsString = $interactor->prompt(
|
||||
'Enter client grants (comma separated, e.g. "authorization_code,refresh_token")',
|
||||
false
|
||||
);
|
||||
|
||||
$grants = explode(',', $clientGrantsString);
|
||||
|
||||
$command = new CreateClientCommand($clientName, $grants, $clientDescription);
|
||||
try {
|
||||
/** @var Client $client */
|
||||
$client = CommandBus::handle($command);
|
||||
|
||||
$this->climate->green('OAuth client created successfully');
|
||||
$this->climate->info('Client ID: ' . $client->client_id);
|
||||
$this->climate->info('Client Secret: ' . $client->client_secret)->br(2);
|
||||
} catch (CommandHandlerException $exception) {
|
||||
$this->climate->error($exception->getMessage());
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Cli\Commands\Queue;
|
||||
|
||||
use Ahc\Cli\Input\Command;
|
||||
use Siteworxpro\App\Async\Messages\SayHelloMessage;
|
||||
use Siteworxpro\App\Cli\Commands\CommandInterface;
|
||||
|
||||
/**
|
||||
* Class TestJob
|
||||
*
|
||||
* A CLI command to schedule a demo job that dispatches a SayHelloMessage.
|
||||
*/
|
||||
class TestJob extends Command implements CommandInterface
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('queue:demo', 'Schedule a demo job.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the command to dispatch a SayHelloMessage.
|
||||
*
|
||||
* @return int Exit code
|
||||
*/
|
||||
public function execute(): int
|
||||
{
|
||||
SayHelloMessage::dispatch('World from TestJob Command!');
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -17,11 +17,50 @@ class AttributeLocator implements HandlerLocator
|
||||
public function __construct()
|
||||
{
|
||||
$directory = __DIR__ . '/Handlers';
|
||||
$this->scanDir($directory);
|
||||
}
|
||||
|
||||
public function getHandlerForCommand($commandName)
|
||||
{
|
||||
if (isset($this->handlers[$commandName])) {
|
||||
$handlerClass = $this->handlers[$commandName];
|
||||
return new $handlerClass();
|
||||
}
|
||||
|
||||
throw new CanNotInvokeHandlerException("No handler found for command: " . $commandName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $directory
|
||||
* @return void
|
||||
*/
|
||||
public function scanDir(string $directory): void
|
||||
{
|
||||
$files = scandir($directory);
|
||||
foreach ($files as $file) {
|
||||
if ($file === '.' || $file === '..') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fullPath = $directory . DIRECTORY_SEPARATOR . $file;
|
||||
|
||||
if (is_dir($fullPath)) {
|
||||
$this->scanDir($fullPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pathinfo($file, PATHINFO_EXTENSION) === 'php') {
|
||||
$className = pathinfo($file, PATHINFO_FILENAME);
|
||||
$fullClassName = self::HANDLER_NAMESPACE . $className;
|
||||
|
||||
$relativePath = str_replace(__DIR__ . '/Handlers/', '', $fullPath);
|
||||
$namespacePath = str_replace(DIRECTORY_SEPARATOR, '\\', dirname($relativePath));
|
||||
if ($namespacePath === '.') {
|
||||
$namespacePath = '';
|
||||
} else {
|
||||
$namespacePath .= '\\';
|
||||
}
|
||||
|
||||
$fullClassName = self::HANDLER_NAMESPACE . $namespacePath . $className;
|
||||
|
||||
if (class_exists($fullClassName)) {
|
||||
$reflectionClass = new \ReflectionClass($fullClassName);
|
||||
@@ -36,14 +75,4 @@ class AttributeLocator implements HandlerLocator
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getHandlerForCommand($commandName)
|
||||
{
|
||||
if (isset($this->handlers[$commandName])) {
|
||||
$handlerClass = $this->handlers[$commandName];
|
||||
return new $handlerClass();
|
||||
}
|
||||
|
||||
throw new CanNotInvokeHandlerException("No handler found for command: " . $commandName);
|
||||
}
|
||||
}
|
||||
|
||||
50
src/CommandBus/Commands/CreateClient.php
Normal file
50
src/CommandBus/Commands/CreateClient.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Siteworxpro\App\CommandBus\Commands;
|
||||
|
||||
readonly class CreateClient extends Command
|
||||
{
|
||||
private const array VALID_GRANTS = [
|
||||
'authorization_code',
|
||||
'password',
|
||||
'client_credentials',
|
||||
'refresh_token',
|
||||
'implicit',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private string $clientName,
|
||||
private array $clientGrants = [],
|
||||
private string $clientDescription = ''
|
||||
) {
|
||||
foreach ($this->clientGrants as $grant) {
|
||||
if (!in_array($grant, self::VALID_GRANTS, true)) {
|
||||
throw new \InvalidArgumentException("Invalid grant type: $grant");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getClientName(): string
|
||||
{
|
||||
return $this->clientName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getClientDescription(): string
|
||||
{
|
||||
return $this->clientDescription;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getClientGrants(): array
|
||||
{
|
||||
return $this->clientGrants;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\CommandBus\Commands;
|
||||
|
||||
readonly class ExampleCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private string $name
|
||||
) {
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
}
|
||||
9
src/CommandBus/Exceptions/CommandHandlerException.php
Normal file
9
src/CommandBus/Exceptions/CommandHandlerException.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\CommandBus\Exceptions;
|
||||
|
||||
class CommandHandlerException extends \InvalidArgumentException
|
||||
{
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\CommandBus\Handlers;
|
||||
|
||||
use Siteworxpro\App\Attributes\CommandBus\HandlesCommand;
|
||||
use Siteworxpro\App\CommandBus\Commands\Command;
|
||||
use Siteworxpro\App\CommandBus\Commands\ExampleCommand;
|
||||
use Siteworxpro\App\Services\Facades\Logger;
|
||||
|
||||
#[HandlesCommand(ExampleCommand::class)]
|
||||
class ExampleHandler extends CommandHandler
|
||||
{
|
||||
/**
|
||||
* @param Command|ExampleCommand $command
|
||||
* @return string
|
||||
*/
|
||||
public function __invoke(Command|ExampleCommand $command): string
|
||||
{
|
||||
if (!method_exists($command, 'getName')) {
|
||||
throw new \TypeError('Invalid command type provided to ExampleHandler.');
|
||||
}
|
||||
|
||||
$name = $command->getName();
|
||||
Logger::info('Handling ExampleCommand for name: ' . $name);
|
||||
|
||||
return 'Hello, ' . $name . '!';
|
||||
}
|
||||
}
|
||||
31
src/CommandBus/Handlers/OAuth/CreateClient.php
Normal file
31
src/CommandBus/Handlers/OAuth/CreateClient.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\CommandBus\Handlers\OAuth;
|
||||
|
||||
use Siteworxpro\App\Attributes\CommandBus\HandlesCommand;
|
||||
use Siteworxpro\App\CommandBus\Commands\Command;
|
||||
use Siteworxpro\App\CommandBus\Exceptions\CommandHandlerException;
|
||||
use Siteworxpro\App\CommandBus\Handlers\CommandHandler;
|
||||
use Siteworxpro\App\OAuth\Entities\Client;
|
||||
|
||||
#[HandlesCommand(\Siteworxpro\App\CommandBus\Commands\CreateClient::class)]
|
||||
class CreateClient extends CommandHandler
|
||||
{
|
||||
public function __invoke(Command $command): Client
|
||||
{
|
||||
if (!$command instanceof \Siteworxpro\App\CommandBus\Commands\CreateClient) {
|
||||
throw new CommandHandlerException('Invalid command type');
|
||||
}
|
||||
|
||||
$client = new Client();
|
||||
$client->name = $command->getClientName();
|
||||
$client->description = $command->getClientDescription();
|
||||
$client->grant_types = $command->getClientGrants();
|
||||
|
||||
$client->save();
|
||||
|
||||
return $client;
|
||||
}
|
||||
}
|
||||
165
src/Controllers/AuthorizeController.php
Normal file
165
src/Controllers/AuthorizeController.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Controllers;
|
||||
|
||||
use HansOtt\PSR7Cookies\SetCookie;
|
||||
use League\OAuth2\Server\Exception\OAuthServerException;
|
||||
use Nyholm\Psr7\Response;
|
||||
use Nyholm\Psr7\ServerRequest;
|
||||
use Nyholm\Psr7\Stream;
|
||||
use Psr\SimpleCache\InvalidArgumentException;
|
||||
use Siteworxpro\App\Helpers\Rand;
|
||||
use Siteworxpro\App\Http\JsonResponseFactory;
|
||||
use Siteworxpro\App\Http\Responses\ServerErrorResponse;
|
||||
use Siteworxpro\App\OAuth\Entities\Client;
|
||||
use Siteworxpro\App\Services\Facades\Logger;
|
||||
use Siteworxpro\App\Services\Facades\Redis;
|
||||
use Siteworxpro\HttpStatus\CodesEnum;
|
||||
|
||||
final class AuthorizeController extends Controller
|
||||
{
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
// #[\Override] public function post(ServerRequest $request): Response
|
||||
// {
|
||||
// $s = $request->getCookieParams()['s'] ?? '';
|
||||
//
|
||||
// $password = $request->getParsedBody()['password'] ?? '';
|
||||
// $email = $request->getParsedBody()['email'] ?? '';
|
||||
//
|
||||
// if (!$this->redis->get('session:' . $s)) {
|
||||
// $this->log->error('Session Timed out', ['session' => $s]);
|
||||
//
|
||||
// return $this->sendJsonResponse(
|
||||
// [
|
||||
// 'error' => "your login session has timed out. please try again."
|
||||
// ],
|
||||
// 400
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// /** @var AuthorizationRequest $authRequest */
|
||||
// $authRequest = unserialize($this->redis->get('session:' . $s));
|
||||
//
|
||||
// if ($authRequest->isAuthorizationApproved()) {
|
||||
// $response = $this
|
||||
// ->authorizationServer
|
||||
// ->completeAuthorizationRequest($authRequest, $this->sendJsonResponse());
|
||||
//
|
||||
// return $this->sendJsonResponse(
|
||||
// [
|
||||
// 'success' => true,
|
||||
// 'location' => $response->getHeader('Location')[0]
|
||||
// ]
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// /** @var Client $client */
|
||||
// $client = $authRequest->getClient();
|
||||
//
|
||||
// /** @var LoginInterface $entitiesModel */
|
||||
// $entitiesModel = $client->entities_model;
|
||||
//
|
||||
// /** @var User | null $entity */
|
||||
// $entity = $entitiesModel::performLogin($email, $password);
|
||||
// if (!$entity) {
|
||||
// return $this->sendJsonResponse(
|
||||
// [
|
||||
// 'success' => false,
|
||||
// 'reason' => 'login failed'
|
||||
// ],
|
||||
// 401
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// $authRequest->setUser($entity);
|
||||
// $authRequest->setAuthorizationApproved(true);
|
||||
// $response = $this
|
||||
// ->authorizationServer
|
||||
// ->completeAuthorizationRequest($authRequest, $this->sendJsonResponse());
|
||||
//
|
||||
// $this->redis->delete('session:' . $s);
|
||||
//
|
||||
// return $this->sendJsonResponse(
|
||||
// [
|
||||
// 'success' => true,
|
||||
// 'location' => $response->getHeader('Location')[0]
|
||||
// ]
|
||||
// );
|
||||
// }
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function get(ServerRequest $request): Response
|
||||
{
|
||||
try {
|
||||
if (!file_exists('public/index.html')) {
|
||||
throw new \RuntimeException('Frontend not built. Please run `npm run build`.');
|
||||
}
|
||||
$contents = file_get_contents('public/index.html');
|
||||
|
||||
if ($request->getQueryParams()['e']) {
|
||||
return new Response(
|
||||
200,
|
||||
['content-type' => 'text/html'],
|
||||
Stream::create($contents)
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
isset($request->getCookieParams()['s']) &&
|
||||
Redis::exists('session:' . $request->getCookieParams()['s'] ?? '')
|
||||
) {
|
||||
$s = $request->getCookieParams()['s'];
|
||||
} else {
|
||||
$s = Rand::string();
|
||||
}
|
||||
|
||||
$clientId = $request->getQueryParams()['client_id'] ?? '';
|
||||
Logger::info('Authorization request', ['client_id' => $clientId]);
|
||||
|
||||
$client = Client::byClientId($clientId);
|
||||
if ($client === null) {
|
||||
Logger::warning('Invalid client in authorization request', ['client_id' => $clientId]);
|
||||
throw OAuthServerException::invalidClient($request);
|
||||
}
|
||||
|
||||
$authRequest = $client->getAuthorizationServer()->validateAuthorizationRequest($request);
|
||||
Redis::set('session:' . $s, serialize($authRequest), 'EX', 60 * 60 * 24);
|
||||
|
||||
$response = new Response(
|
||||
200,
|
||||
['content-type' => 'text/html'],
|
||||
Stream::create($contents)
|
||||
);
|
||||
|
||||
$cookie = new SetCookie('s', $s, time() + 3600, '/', secure: true);
|
||||
|
||||
/** @var Response $response */
|
||||
$response = $cookie->addToResponse($response);
|
||||
|
||||
return $response;
|
||||
} catch (OAuthServerException $e) {
|
||||
return new Response(
|
||||
CodesEnum::TEMPORARY_REDIRECT->value,
|
||||
[
|
||||
'Location' => sprintf(
|
||||
'/authorize?e=%s&client_id=%s&response_type=%s&redirect_uri=%s#/error',
|
||||
$e->getMessage(),
|
||||
$request->getQueryParams()['client_id'] ?? '',
|
||||
$request->getQueryParams()['response_type'] ?? '',
|
||||
$request->getQueryParams()['redirect_uri'] ?? ''
|
||||
)
|
||||
]
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Logger::error($e->getMessage(), ['exception' => $e]);
|
||||
|
||||
return JsonResponseFactory::createJsonResponse(new ServerErrorResponse($e));
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/Controllers/CapabilitiesController.php
Normal file
30
src/Controllers/CapabilitiesController.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Controllers;
|
||||
|
||||
use Nyholm\Psr7\ServerRequest;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Siteworxpro\App\Http\JsonResponseFactory;
|
||||
use Siteworxpro\App\Http\Responses\NotFoundResponse;
|
||||
use Siteworxpro\App\OAuth\Entities\Client;
|
||||
|
||||
final class CapabilitiesController extends Controller
|
||||
{
|
||||
/**
|
||||
* @throws \JsonException
|
||||
*/
|
||||
public function get(ServerRequest $request): ResponseInterface
|
||||
{
|
||||
$clientId = $request->getQueryParams()['client_id'] ?? '0';
|
||||
|
||||
$client = Client::byClientId($clientId);
|
||||
|
||||
if (!$client) {
|
||||
return JsonResponseFactory::createJsonResponse(new NotFoundResponse($request->getUri()->getPath()));
|
||||
}
|
||||
|
||||
return JsonResponseFactory::createJsonResponse($client->capabilities->toArray());
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ use OpenApi\Attributes as OA;
|
||||
*
|
||||
* @package Siteworxpro\App\Controllers
|
||||
*/
|
||||
class HealthcheckController extends Controller
|
||||
final class HealthcheckController extends Controller
|
||||
{
|
||||
/**
|
||||
* Handles the GET request for health check.
|
||||
|
||||
@@ -20,7 +20,7 @@ use Siteworxpro\App\Services\Facades\CommandBus;
|
||||
*
|
||||
* This class handles the index route of the application.
|
||||
*/
|
||||
class IndexController extends Controller
|
||||
final class IndexController extends Controller
|
||||
{
|
||||
/**
|
||||
* Handles the GET request for the index route.
|
||||
|
||||
@@ -9,7 +9,7 @@ use Nyholm\Psr7\ServerRequest;
|
||||
use OpenApi\Generator;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class OpenApiController extends Controller
|
||||
final class OpenApiController extends Controller
|
||||
{
|
||||
/**
|
||||
* Handles the GET request to generate and return the OpenAPI specification.
|
||||
|
||||
@@ -28,8 +28,6 @@ class Connected extends Listener
|
||||
throw new \TypeError("Invalid event type passed to listener " . static::class);
|
||||
}
|
||||
|
||||
Logger::info("Database connection event", [get_class($event), $event->connectionName]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
24
src/Helpers/Rand.php
Normal file
24
src/Helpers/Rand.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Helpers;
|
||||
|
||||
use Random\RandomException;
|
||||
|
||||
class Rand
|
||||
{
|
||||
/**
|
||||
* @throws RandomException
|
||||
*/
|
||||
public static function string(int $length = 16): string
|
||||
{
|
||||
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
$charactersLength = strlen($characters);
|
||||
$randomString = '';
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$randomString .= $characters[random_int(0, $charactersLength - 1)];
|
||||
}
|
||||
return $randomString;
|
||||
}
|
||||
}
|
||||
26
src/Models/ClientRedirectUri.php
Normal file
26
src/Models/ClientRedirectUri.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Siteworxpro\App\OAuth\Entities\Client;
|
||||
|
||||
/**
|
||||
* Class ClientRedirectUrl
|
||||
* @package Siteworxpro\App\Models
|
||||
*
|
||||
* @property string $id
|
||||
* @property string $client_id
|
||||
* @property string $redirect_uri
|
||||
*/
|
||||
class ClientRedirectUri extends Model
|
||||
{
|
||||
public $timestamps = false;
|
||||
|
||||
public function client(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Client::class);
|
||||
}
|
||||
}
|
||||
18
src/Models/ClientScope.php
Normal file
18
src/Models/ClientScope.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Models;
|
||||
|
||||
/**
|
||||
* Class ClientScope
|
||||
* @package Siteworxpro\App\Models
|
||||
*
|
||||
* @property string $id
|
||||
* @property string $client_id
|
||||
* @property string $scope_id
|
||||
*/
|
||||
class ClientScope extends Model
|
||||
{
|
||||
|
||||
}
|
||||
18
src/Models/ClientUser.php
Normal file
18
src/Models/ClientUser.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Models;
|
||||
|
||||
/**
|
||||
* Class ClientUser
|
||||
* @package Siteworxpro\App\Models
|
||||
*
|
||||
* @property string $id
|
||||
* @property string $client_id
|
||||
* @property string $user_id
|
||||
*/
|
||||
class ClientUser extends Model
|
||||
{
|
||||
|
||||
}
|
||||
@@ -10,6 +10,8 @@ use Illuminate\Database\Eloquent\Model as ORM;
|
||||
* Class Model
|
||||
*
|
||||
* @package Siteworxpro\App\Models
|
||||
* @method static static|null find(string $id, array $columns = ['*'])
|
||||
* @method static where(string $column, string $operator = null, string $value = null, string $boolean = 'and')
|
||||
*/
|
||||
abstract class Model extends ORM
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Siteworxpro\App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use League\OAuth2\Server\Entities\Traits\EntityTrait;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Siteworxpro\App\Helpers\Ulid;
|
||||
|
||||
@@ -40,6 +41,8 @@ use Siteworxpro\App\Helpers\Ulid;
|
||||
)]
|
||||
class User extends Model
|
||||
{
|
||||
use EntityTrait;
|
||||
|
||||
protected $casts = [
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
|
||||
50
src/OAuth/AccessTokenRepository.php
Normal file
50
src/OAuth/AccessTokenRepository.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\OAuth;
|
||||
|
||||
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
|
||||
use League\OAuth2\Server\Entities\ClientEntityInterface;
|
||||
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
|
||||
use Siteworxpro\App\OAuth\Entities\AccessToken;
|
||||
use Siteworxpro\App\OAuth\Entities\Client;
|
||||
|
||||
class AccessTokenRepository implements AccessTokenRepositoryInterface
|
||||
{
|
||||
public function getNewToken(
|
||||
ClientEntityInterface | Client $clientEntity,
|
||||
array $scopes,
|
||||
?string $userIdentifier = null
|
||||
): AccessTokenEntityInterface | AccessToken {
|
||||
$accessToken = new AccessToken();
|
||||
$accessToken->setClient($clientEntity);
|
||||
foreach ($scopes as $scope) {
|
||||
$accessToken->addScope($scope);
|
||||
}
|
||||
$accessToken->setUserIdentifier($userIdentifier);
|
||||
|
||||
return $accessToken;
|
||||
}
|
||||
|
||||
public function persistNewAccessToken(AccessTokenEntityInterface | AccessToken $accessTokenEntity): void
|
||||
{
|
||||
$accessTokenEntity->save();
|
||||
}
|
||||
|
||||
public function revokeAccessToken(string $tokenId): void
|
||||
{
|
||||
$accessToken = AccessToken::find($tokenId);
|
||||
|
||||
if ($accessToken) {
|
||||
$accessToken->delete();
|
||||
}
|
||||
}
|
||||
|
||||
public function isAccessTokenRevoked(string $tokenId): bool
|
||||
{
|
||||
$accessToken = AccessToken::find($tokenId);
|
||||
|
||||
return $accessToken === null;
|
||||
}
|
||||
}
|
||||
31
src/OAuth/AuthCodeRepository.php
Normal file
31
src/OAuth/AuthCodeRepository.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\OAuth;
|
||||
|
||||
use League\OAuth2\Server\Entities\AuthCodeEntityInterface;
|
||||
use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface;
|
||||
|
||||
class AuthCodeRepository implements AuthCodeRepositoryInterface
|
||||
{
|
||||
public function getNewAuthCode(): AuthCodeEntityInterface
|
||||
{
|
||||
// TODO: Implement getNewAuthCode() method.
|
||||
}
|
||||
|
||||
public function persistNewAuthCode(AuthCodeEntityInterface $authCodeEntity): void
|
||||
{
|
||||
// TODO: Implement persistNewAuthCode() method.
|
||||
}
|
||||
|
||||
public function revokeAuthCode(string $codeId): void
|
||||
{
|
||||
// TODO: Implement revokeAuthCode() method.
|
||||
}
|
||||
|
||||
public function isAuthCodeRevoked(string $codeId): bool
|
||||
{
|
||||
// TODO: Implement isAuthCodeRevoked() method.
|
||||
}
|
||||
}
|
||||
59
src/OAuth/ClientRepository.php
Normal file
59
src/OAuth/ClientRepository.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\OAuth;
|
||||
|
||||
use League\OAuth2\Server\Entities\ClientEntityInterface;
|
||||
use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
|
||||
use Siteworxpro\App\OAuth\Entities\Client;
|
||||
|
||||
readonly class ClientRepository implements ClientRepositoryInterface
|
||||
{
|
||||
public function __construct(private Client $client)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* get a client entity.
|
||||
*
|
||||
* @param string $clientIdentifier
|
||||
* @return ClientEntityInterface|null
|
||||
*/
|
||||
public function getClientEntity(string $clientIdentifier): ?ClientEntityInterface
|
||||
{
|
||||
|
||||
if ($this->client->client_id === $clientIdentifier) {
|
||||
return $this->client;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* validate a client with given data.
|
||||
*
|
||||
* @param string $clientIdentifier
|
||||
* @param string|null $clientSecret
|
||||
* @param string|null $grantType
|
||||
* @return bool
|
||||
*/
|
||||
public function validateClient(string $clientIdentifier, ?string $clientSecret, ?string $grantType): bool
|
||||
{
|
||||
$client = Client::find($clientIdentifier);
|
||||
|
||||
if ($client === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($clientSecret && $client->client_secret != $clientSecret) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($grantType && !in_array($grantType, $client->grant_types)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
61
src/OAuth/Entities/AccessToken.php
Normal file
61
src/OAuth/Entities/AccessToken.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\OAuth\Entities;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
|
||||
use League\OAuth2\Server\Entities\ClientEntityInterface;
|
||||
use League\OAuth2\Server\Entities\ScopeEntityInterface;
|
||||
use League\OAuth2\Server\Entities\Traits\AccessTokenTrait;
|
||||
|
||||
class AccessToken extends RedisModel implements AccessTokenEntityInterface
|
||||
{
|
||||
use AccessTokenTrait;
|
||||
|
||||
public function getClient(): ClientEntityInterface
|
||||
{
|
||||
// TODO: Implement getClient() method.
|
||||
}
|
||||
|
||||
public function getExpiryDateTime(): DateTimeImmutable
|
||||
{
|
||||
// TODO: Implement getExpiryDateTime() method.
|
||||
}
|
||||
|
||||
public function getUserIdentifier(): string|null
|
||||
{
|
||||
// TODO: Implement getUserIdentifier() method.
|
||||
}
|
||||
|
||||
public function getScopes(): array
|
||||
{
|
||||
// TODO: Implement getScopes() method.
|
||||
}
|
||||
|
||||
protected static function getRedisPrefix(): string
|
||||
{
|
||||
return 'oauth_access_token';
|
||||
}
|
||||
|
||||
public function setExpiryDateTime(DateTimeImmutable $dateTime): void
|
||||
{
|
||||
// TODO: Implement setExpiryDateTime() method.
|
||||
}
|
||||
|
||||
public function setUserIdentifier(string $identifier): void
|
||||
{
|
||||
// TODO: Implement setUserIdentifier() method.
|
||||
}
|
||||
|
||||
public function setClient(ClientEntityInterface $client): void
|
||||
{
|
||||
// TODO: Implement setClient() method.
|
||||
}
|
||||
|
||||
public function addScope(ScopeEntityInterface $scope): void
|
||||
{
|
||||
// TODO: Implement addScope() method.
|
||||
}
|
||||
}
|
||||
61
src/OAuth/Entities/AuthorizationCode.php
Normal file
61
src/OAuth/Entities/AuthorizationCode.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\OAuth\Entities;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use League\OAuth2\Server\Entities\AuthCodeEntityInterface;
|
||||
use League\OAuth2\Server\Entities\ClientEntityInterface;
|
||||
use League\OAuth2\Server\Entities\ScopeEntityInterface;
|
||||
use League\OAuth2\Server\Entities\Traits\AuthCodeTrait;
|
||||
|
||||
class AuthorizationCode extends RedisModel implements AuthCodeEntityInterface
|
||||
{
|
||||
use AuthCodeTrait;
|
||||
|
||||
protected static function getRedisPrefix(): string
|
||||
{
|
||||
return 'oauth_auth_code';
|
||||
}
|
||||
|
||||
public function getExpiryDateTime(): DateTimeImmutable
|
||||
{
|
||||
// TODO: Implement getExpiryDateTime() method.
|
||||
}
|
||||
|
||||
public function setExpiryDateTime(DateTimeImmutable $dateTime): void
|
||||
{
|
||||
// TODO: Implement setExpiryDateTime() method.
|
||||
}
|
||||
|
||||
public function setUserIdentifier(string $identifier): void
|
||||
{
|
||||
// TODO: Implement setUserIdentifier() method.
|
||||
}
|
||||
|
||||
public function getUserIdentifier(): string|null
|
||||
{
|
||||
// TODO: Implement getUserIdentifier() method.
|
||||
}
|
||||
|
||||
public function getClient(): ClientEntityInterface
|
||||
{
|
||||
// TODO: Implement getClient() method.
|
||||
}
|
||||
|
||||
public function setClient(ClientEntityInterface $client): void
|
||||
{
|
||||
// TODO: Implement setClient() method.
|
||||
}
|
||||
|
||||
public function addScope(ScopeEntityInterface $scope): void
|
||||
{
|
||||
// TODO: Implement addScope() method.
|
||||
}
|
||||
|
||||
public function getScopes(): array
|
||||
{
|
||||
// TODO: Implement getScopes() method.
|
||||
}
|
||||
}
|
||||
206
src/OAuth/Entities/Client.php
Normal file
206
src/OAuth/Entities/Client.php
Normal file
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\OAuth\Entities;
|
||||
|
||||
use Defuse\Crypto\Exception\BadFormatException;
|
||||
use Defuse\Crypto\Exception\EnvironmentIsBrokenException;
|
||||
use Defuse\Crypto\Key;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||
use League\OAuth2\Server\AuthorizationServer;
|
||||
use League\OAuth2\Server\Entities\ClientEntityInterface;
|
||||
use League\OAuth2\Server\Entities\Traits\EntityTrait;
|
||||
use Random\RandomException;
|
||||
use Siteworxpro\App\Helpers\Rand;
|
||||
use Siteworxpro\App\Models\ClientRedirectUri;
|
||||
use Siteworxpro\App\Models\ClientScope;
|
||||
use Siteworxpro\App\Models\ClientUser;
|
||||
use Siteworxpro\App\Models\Model;
|
||||
use Siteworxpro\App\Models\User;
|
||||
use Siteworxpro\App\OAuth\AccessTokenRepository;
|
||||
use Siteworxpro\App\OAuth\ClientRepository;
|
||||
use Siteworxpro\App\OAuth\ScopeRepository;
|
||||
|
||||
/**
|
||||
* Class Client
|
||||
* @package Siteworxpro\App\Models
|
||||
*
|
||||
* @property string $id
|
||||
* @property string $client_id
|
||||
* @property string $client_secret
|
||||
* @property string $name
|
||||
* @property string $description
|
||||
* @property string $private_key
|
||||
* @property string $encryption_key
|
||||
* @property string[] $grant_types
|
||||
* @property bool $confidential
|
||||
*
|
||||
* @property-read ClientCapabilities $capabilities
|
||||
* @property-read Collection<ClientRedirectUri> $clientRedirectUris
|
||||
* @property-read Scope[]|Collection $scopes
|
||||
*/
|
||||
class Client extends Model implements ClientEntityInterface
|
||||
{
|
||||
use EntityTrait;
|
||||
|
||||
protected $casts = [
|
||||
'id' => 'string',
|
||||
'grant_types' => 'collection',
|
||||
'confidential' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* @throws RandomException|EnvironmentIsBrokenException
|
||||
*/
|
||||
public function __construct(array $attributes = [])
|
||||
{
|
||||
parent::__construct($attributes);
|
||||
|
||||
$this->client_id = Rand::string(32);
|
||||
$this->client_secret = Rand::string(64);
|
||||
$this->generatePrivateKey();
|
||||
}
|
||||
|
||||
public static function byClientId(string $clientId): ?Client
|
||||
{
|
||||
return self::where('client_id', $clientId)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @throws EnvironmentIsBrokenException
|
||||
*/
|
||||
private function generatePrivateKey(): void
|
||||
{
|
||||
// generate rsa private and public key pair
|
||||
$config = [
|
||||
"digest_alg" => "sha256",
|
||||
"private_key_bits" => 4096,
|
||||
"private_key_type" => OPENSSL_KEYTYPE_RSA,
|
||||
];
|
||||
|
||||
$res = openssl_pkey_new($config);
|
||||
openssl_pkey_export($res, $privateKey);
|
||||
$this->private_key = $privateKey;
|
||||
$this->encryption_key = Key::createNewRandomKey()->saveToAsciiSafeString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasMany
|
||||
*/
|
||||
public function clientRedirectUris(): HasMany
|
||||
{
|
||||
return $this->hasMany(ClientRedirectUri::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasManyThrough
|
||||
*/
|
||||
public function scopes(): HasManyThrough
|
||||
{
|
||||
return $this->hasManyThrough(Scope::class, ClientScope::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasManyThrough
|
||||
*/
|
||||
public function users(): HasManyThrough
|
||||
{
|
||||
return $this->hasManyThrough(User::class, ClientUser::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getIdentifier(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|array
|
||||
*/
|
||||
public function getRedirectUri(): string|array
|
||||
{
|
||||
return $this->clientRedirectUris->pluck('redirect_uri')->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isConfidential(): bool
|
||||
{
|
||||
return $this->confidential;
|
||||
}
|
||||
|
||||
public function getCapabilitiesAttribute(string $capabilities): ClientCapabilities
|
||||
{
|
||||
return ClientCapabilities::fromJson($capabilities);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \JsonException
|
||||
*/
|
||||
public function setCapabilitiesAttribute(ClientCapabilities $capabilities): void
|
||||
{
|
||||
$this->attributes->capabilities = $capabilities->toJson();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadFormatException
|
||||
* @throws EnvironmentIsBrokenException
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getAuthorizationServer(): AuthorizationServer
|
||||
{
|
||||
|
||||
$authorizationServer = new AuthorizationServer(
|
||||
new ClientRepository($this),
|
||||
new AccessTokenRepository(),
|
||||
new ScopeRepository(),
|
||||
$this->private_key,
|
||||
Key::loadFromAsciiSafeString($this->encryption_key)
|
||||
);
|
||||
|
||||
if (!empty($this->grant_types)) {
|
||||
foreach ($this->grant_types as $grantType) {
|
||||
switch ($grantType) {
|
||||
case 'authorization_code':
|
||||
$grant = new \League\OAuth2\Server\Grant\AuthCodeGrant(
|
||||
new \Siteworxpro\App\OAuth\AuthCodeRepository(),
|
||||
new \Siteworxpro\App\OAuth\RefreshTokenRepository(),
|
||||
new \DateInterval('PT10M')
|
||||
);
|
||||
$grant->setRefreshTokenTTL(new \DateInterval('P1M'));
|
||||
break;
|
||||
case 'client_credentials':
|
||||
$grant = new \League\OAuth2\Server\Grant\ClientCredentialsGrant();
|
||||
break;
|
||||
case 'refresh_token':
|
||||
$grant = new \League\OAuth2\Server\Grant\RefreshTokenGrant(
|
||||
new \Siteworxpro\App\OAuth\RefreshTokenRepository()
|
||||
);
|
||||
$grant->setRefreshTokenTTL(new \DateInterval('P1M'));
|
||||
break;
|
||||
default:
|
||||
continue 2;
|
||||
}
|
||||
|
||||
$authorizationServer->enableGrantType($grant);
|
||||
}
|
||||
}
|
||||
|
||||
return $authorizationServer;
|
||||
}
|
||||
}
|
||||
74
src/OAuth/Entities/ClientCapabilities.php
Normal file
74
src/OAuth/Entities/ClientCapabilities.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\OAuth\Entities;
|
||||
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
|
||||
class ClientCapabilities implements Arrayable
|
||||
{
|
||||
private bool $userPass = false;
|
||||
private bool $magicLink = false;
|
||||
private bool $passkey = false;
|
||||
private array $socials = [];
|
||||
|
||||
private array $theme = [
|
||||
'primaryColor' => '#000000',
|
||||
'secondaryColor' => '#FFFFFF',
|
||||
'logoUrl' => null,
|
||||
];
|
||||
|
||||
public function __construct(array $capabilities)
|
||||
{
|
||||
if (isset($capabilities['userPass'])) {
|
||||
$this->userPass = (bool)$capabilities['userPass'];
|
||||
}
|
||||
|
||||
if (isset($capabilities['magicLink'])) {
|
||||
$this->magicLink = (bool)$capabilities['magicLink'];
|
||||
}
|
||||
|
||||
if (isset($capabilities['passkey'])) {
|
||||
$this->passkey = (bool)$capabilities['passkey'];
|
||||
}
|
||||
|
||||
if (isset($capabilities['socials']) && is_array($capabilities['socials'])) {
|
||||
$this->socials = $capabilities['socials'];
|
||||
}
|
||||
|
||||
if (isset($capabilities['theme']) && is_array($capabilities['theme'])) {
|
||||
$this->theme = array_merge($this->theme, $capabilities['theme']);
|
||||
}
|
||||
}
|
||||
|
||||
public static function fromJson(string $data): self
|
||||
{
|
||||
try {
|
||||
$arrayData = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
return new self($arrayData);
|
||||
} catch (\JsonException $e) {
|
||||
return new self([]);
|
||||
}
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'userPass' => $this->userPass,
|
||||
'magicLink' => $this->magicLink,
|
||||
'passkey' => $this->passkey,
|
||||
'socials' => $this->socials,
|
||||
'theme' => $this->theme,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \JsonException
|
||||
*/
|
||||
public function toJson(): string
|
||||
{
|
||||
return json_encode($this->toArray(), JSON_THROW_ON_ERROR);
|
||||
}
|
||||
}
|
||||
60
src/OAuth/Entities/RedisModel.php
Normal file
60
src/OAuth/Entities/RedisModel.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\OAuth\Entities;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use DateTimeImmutable;
|
||||
use League\OAuth2\Server\Entities\Traits\EntityTrait;
|
||||
use Psr\SimpleCache\InvalidArgumentException;
|
||||
use Siteworxpro\App\Services\Facades\Redis;
|
||||
|
||||
abstract class RedisModel
|
||||
{
|
||||
use EntityTrait;
|
||||
|
||||
private \Predis\Client $redis;
|
||||
|
||||
protected ?DateTimeImmutable $expireTime;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->redis = Redis::getFacadeRoot();
|
||||
}
|
||||
|
||||
abstract protected static function getRedisPrefix(): string;
|
||||
|
||||
public static function find(string $identifier): ?self
|
||||
{
|
||||
$instance = Redis::get(static::getRedisPrefix() . ':' . $identifier);
|
||||
|
||||
if ($instance !== null) {
|
||||
return unserialize($instance);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$diff = 0;
|
||||
if ($this->expireTime) {
|
||||
$diff = $this->expireTime->getTimestamp() - Carbon::now()->timestamp;
|
||||
}
|
||||
|
||||
$this->redis->set(
|
||||
static::getRedisPrefix() . ':' . $this->getIdentifier(),
|
||||
serialize($this),
|
||||
$diff
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function delete(): void
|
||||
{
|
||||
$this->redis->delete(static::getRedisPrefix() . ':' . $this->getIdentifier());
|
||||
}
|
||||
}
|
||||
18
src/OAuth/Entities/RefreshToken.php
Normal file
18
src/OAuth/Entities/RefreshToken.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\OAuth\Entities;
|
||||
|
||||
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
|
||||
use League\OAuth2\Server\Entities\Traits\RefreshTokenTrait;
|
||||
|
||||
class RefreshToken extends RedisModel implements RefreshTokenEntityInterface
|
||||
{
|
||||
use RefreshTokenTrait;
|
||||
|
||||
protected static function getRedisPrefix(): string
|
||||
{
|
||||
return 'oauth_refresh_token';
|
||||
}
|
||||
}
|
||||
27
src/OAuth/Entities/Scope.php
Normal file
27
src/OAuth/Entities/Scope.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\OAuth\Entities;
|
||||
|
||||
use League\OAuth2\Server\Entities\ScopeEntityInterface;
|
||||
use League\OAuth2\Server\Entities\Traits\ScopeTrait;
|
||||
use Siteworxpro\App\Models\Model;
|
||||
|
||||
/**
|
||||
* Class Scope
|
||||
* @package Siteworxpro\App\Models
|
||||
*
|
||||
* @property string $id
|
||||
* @property string $name
|
||||
* @property string $description
|
||||
*/
|
||||
class Scope extends Model implements ScopeEntityInterface
|
||||
{
|
||||
use ScopeTrait;
|
||||
|
||||
public function getIdentifier(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
}
|
||||
32
src/OAuth/RefreshTokenRepository.php
Normal file
32
src/OAuth/RefreshTokenRepository.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\OAuth;
|
||||
|
||||
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
|
||||
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
|
||||
|
||||
class RefreshTokenRepository implements RefreshTokenRepositoryInterface
|
||||
{
|
||||
|
||||
public function getNewRefreshToken(): ?RefreshTokenEntityInterface
|
||||
{
|
||||
// TODO: Implement getNewRefreshToken() method.
|
||||
}
|
||||
|
||||
public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity): void
|
||||
{
|
||||
// TODO: Implement persistNewRefreshToken() method.
|
||||
}
|
||||
|
||||
public function revokeRefreshToken(string $tokenId): void
|
||||
{
|
||||
// TODO: Implement revokeRefreshToken() method.
|
||||
}
|
||||
|
||||
public function isRefreshTokenRevoked(string $tokenId): bool
|
||||
{
|
||||
// TODO: Implement isRefreshTokenRevoked() method.
|
||||
}
|
||||
}
|
||||
28
src/OAuth/ScopeRepository.php
Normal file
28
src/OAuth/ScopeRepository.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\OAuth;
|
||||
|
||||
use League\OAuth2\Server\Entities\ClientEntityInterface;
|
||||
use League\OAuth2\Server\Entities\ScopeEntityInterface;
|
||||
use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
|
||||
use Siteworxpro\App\OAuth\Entities\Scope;
|
||||
|
||||
class ScopeRepository implements ScopeRepositoryInterface
|
||||
{
|
||||
public function getScopeEntityByIdentifier(string $identifier): ?ScopeEntityInterface
|
||||
{
|
||||
return Scope::where('name', $identifier)->first();
|
||||
}
|
||||
|
||||
public function finalizeScopes(
|
||||
array $scopes,
|
||||
string $grantType,
|
||||
ClientEntityInterface $clientEntity,
|
||||
?string $userIdentifier = null,
|
||||
?string $authCodeId = null
|
||||
): array {
|
||||
return $scopes;
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ use Siteworxpro\App\Services\Facade;
|
||||
* @method static Status|null set(string $key, $value, $expireResolution = null, $expireTTL = null, $flag = null)
|
||||
* @method static array keys(string $pattern)
|
||||
* @method static int del(string $key)
|
||||
* @method static bool exists(string $key)
|
||||
* @method static Status ping()
|
||||
*/
|
||||
class Redis extends Facade
|
||||
|
||||
Reference in New Issue
Block a user