Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
3693f85
feat: add process ID output when server starts
obarbosa89 Aug 6, 2025
86db315
fix: update phenixphp/framework requirement to dev-feature/users-module
obarbosa89 Aug 6, 2025
c31266a
feat: implement user management with CRUD operations and user table m…
obarbosa89 Aug 6, 2025
8b51753
Implement code changes to enhance functionality and improve performance
obarbosa89 Aug 6, 2025
8c6f9b5
fix: update phenixphp/framework requirement to dev-feature/users-module
obarbosa89 Aug 6, 2025
0331ccf
fix: revert phenixphp/framework requirement to stable version
obarbosa89 Aug 23, 2025
09c9519
chore: update dependencies
obarbosa89 Aug 23, 2025
0976b4b
chore: restore code
obarbosa89 Aug 27, 2025
2140e89
refactor: remove non required classes and code
obarbosa89 Aug 27, 2025
423cfc0
feat: install dev branch of framework
obarbosa89 Oct 7, 2025
33150e2
feat: register available app configs
obarbosa89 Oct 7, 2025
a5c4734
feat: add translations for validation
obarbosa89 Oct 7, 2025
ca064b1
feat: registration feature
obarbosa89 Oct 7, 2025
32615ba
feat: add user registration test
obarbosa89 Oct 24, 2025
6aa75c1
feat: implement email verification for user registration
obarbosa89 Oct 27, 2025
cb77569
feat: add mail configuration to .env.example
obarbosa89 Oct 27, 2025
1e3f5d0
feat: add RefreshDatabase trait to RegisterTest for improved test iso…
obarbosa89 Oct 27, 2025
b16f4fc
refactor: improve readability of getEnvFile method in TestCase
obarbosa89 Oct 27, 2025
5f087eb
refactor: remove unused InteractWithDatabase trait from RegisterTest
obarbosa89 Nov 5, 2025
4c50b45
refactor: update test case binding to use AsyncTestCase for unit tests
obarbosa89 Nov 5, 2025
29c2224
feat: enhance email validation in RegisterController with DNS and RFC…
obarbosa89 Nov 7, 2025
1f5a66b
refactor: update user table migration to use fluent column definition…
obarbosa89 Nov 7, 2025
1498ae3
feat: implement OneTimePasswordScope enum and UserOtp model with migr…
obarbosa89 Nov 7, 2025
5ea3721
refactor: enhance user_one_time_passwords migration with primary key …
obarbosa89 Nov 7, 2025
966e083
refactor: update user creation logic in RegisterController to use exp…
obarbosa89 Nov 7, 2025
ff61651
feat: implement One-Time Password (OTP) functionality with email veri…
obarbosa89 Nov 7, 2025
f342fc4
refactor: update email verification mail class in RegisterTest to use…
obarbosa89 Nov 7, 2025
cf127d6
style: php cs
obarbosa89 Nov 7, 2025
287d1ab
refactor: update config files
obarbosa89 Jan 5, 2026
e2cf1ad
chore: add *.sqlite* to .gitignore
obarbosa89 Jan 5, 2026
ba5011b
chore: remove empty .keep file from tests/Feature directory
obarbosa89 Jan 5, 2026
d9ae14e
chore: add SYS_PTRACE capability and seccomp security option to app s…
obarbosa89 Feb 10, 2026
1371e2a
feat: remove pest
obarbosa89 Feb 10, 2026
a86f14a
feat: register user
obarbosa89 Feb 10, 2026
81fe598
refactor: rewrite test in PHPUnit
obarbosa89 Feb 10, 2026
e255309
style: php cs
obarbosa89 Feb 10, 2026
8538bb3
feat: add pcntl and sockets extensions to Dockerfile
obarbosa89 Feb 10, 2026
e8f3eae
feat: update UserOtp creation method and enhance email verification test
obarbosa89 Feb 12, 2026
fc6d967
feat: update framework version to dev-feature/integration-v080 in com…
obarbosa89 Feb 12, 2026
1de439c
feat: email verification using otp
obarbosa89 Mar 2, 2026
2b27fe5
feat: use faker for dynamic email generation in VerifyEmailTest
obarbosa89 Mar 2, 2026
35111bd
feat: sync config files with framework
obarbosa89 Mar 11, 2026
6841c75
feat: add scheduled task to delete unused UserOtp records
obarbosa89 Mar 11, 2026
f0aba6f
feat: implement ResendOtpController and corresponding tests for email…
obarbosa89 Mar 13, 2026
e958748
tests(refactor): remove non required tests
obarbosa89 Mar 14, 2026
f450548
tests(refactor): remove data key wrapper
obarbosa89 Mar 14, 2026
3e38b3e
chore: remove non required docblocks
obarbosa89 Mar 14, 2026
9b61218
feat: login and logout flows
obarbosa89 Mar 20, 2026
fb1970c
feat: implement translations
obarbosa89 Mar 20, 2026
4d559c7
feat: reponds 401 if token is present in guest routes
obarbosa89 Mar 20, 2026
da550e4
refactor: rename class
obarbosa89 Mar 20, 2026
88ab737
refactor: fix import statement for Router class
obarbosa89 Mar 20, 2026
0e07ce9
feat: implement forgot and reset password functionality with OTP
obarbosa89 Mar 24, 2026
f5eb203
refactor: remove unauthorized response tests for login and resend OTP
obarbosa89 Mar 24, 2026
8186ee7
chore: remove invalid file
obarbosa89 Mar 24, 2026
92475cf
style: php cs
obarbosa89 Mar 24, 2026
963eb6b
chore: update dependencies
obarbosa89 Mar 24, 2026
d8326d3
feat: implement token management functionality with listing, refreshi…
obarbosa89 Mar 25, 2026
68a113a
feat: add cancel registration functionality with validation and tests
obarbosa89 Mar 25, 2026
f6b5e05
style: php cs
obarbosa89 Mar 25, 2026
6db9653
tests(refactor): use route global helper
obarbosa89 Mar 25, 2026
684c72c
feat: remove Guest middleware and update related routes and tests
obarbosa89 Mar 25, 2026
9b1ad8d
chore: update phenixphp/framework version to stable release
obarbosa89 Mar 25, 2026
062634f
feat: update composer.json to include additional extensions and impro…
obarbosa89 Mar 25, 2026
3241049
style: add phpstan ignore comment for mailable resolution
obarbosa89 Mar 25, 2026
ea4cf80
chore: update phenixphp/framework version to ^0.8.2
obarbosa89 Mar 26, 2026
3d9ce7a
ci: add key generation step before executing tests
obarbosa89 Mar 26, 2026
85094ad
ci: update environment configuration for Redis and MySQL services in …
obarbosa89 Mar 26, 2026
d505ccc
ci: update database username to 'root' and adjust MySQL service confi…
obarbosa89 Mar 26, 2026
a80616c
ci: update database password and configuration for MySQL service in C…
obarbosa89 Mar 26, 2026
b9306ab
ci: update environment file handling for PHPStan and PHPUnit execution
obarbosa89 Mar 26, 2026
508bc9b
ci: update MySQL service configuration and test execution command in …
obarbosa89 Mar 26, 2026
4223aad
feat: update phenixphp/framework version to ^0.8.3 in composer.json
obarbosa89 Mar 31, 2026
7fcbd5a
feat: update phenixphp/framework version to ^0.8.4 in composer.json
obarbosa89 Apr 1, 2026
4525077
feat: change LOG_CHANNEL from stream to file in .env.example
obarbosa89 Apr 1, 2026
cd843f3
feat: update phenixphp/framework version to ^0.8.5 in composer.json
obarbosa89 Apr 1, 2026
957946e
ci: update PHPStan command and change SonarQube action in CI workflow
obarbosa89 Apr 1, 2026
62d2013
refactor: enhance OTP handling in login and reset password processes …
obarbosa89 Apr 1, 2026
5b48b9c
refacttor: rename mailables
leonardo2006jaimes-ux Apr 3, 2026
4f876fa
refacttor: rename mailables
leonardo2006jaimes-ux Apr 3, 2026
35d6418
refactor: remove docblock
leonardo2006jaimes-ux Apr 3, 2026
018925e
refactor: split routes
leonardo2006jaimes-ux Apr 3, 2026
be2c206
Merge pull request #84 from leonardo2006jaimes-ux/feature/user-authen…
barbosa89 Apr 3, 2026
901e236
Merge branch 'feature/user-authentication' of github.com:phenixphp/ph…
obarbosa89 Apr 4, 2026
5c6e7b9
style: php cs
obarbosa89 Apr 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ coverage/
*.pid.lock

.phpunit.result.cache
.pest
.php_cs.cache

dist/
Expand Down
23 changes: 18 additions & 5 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,30 @@ DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=phenix
DB_USERNAME=phenix
DB_PASSWORD=
DB_USERNAME=root
DB_PASSWORD=secret

LOG_CHANNEL=stream
LOG_CHANNEL=file

QUEUE_DRIVER=parallel
CACHE_STORE=redis
RATE_LIMIT_STORE="${CACHE_STORE}"

QUEUE_DRIVER=redis

CORS_ORIGIN=

REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_USERNAME=
REDIS_PASSWORD=null

SESSION_DRIVER=local
SESSION_DRIVER=redis

MAIL_MAILER=smtp
MAIL_HOST=127.0.0.1
MAIL_PORT=587
MAIL_ENCRYPTION=tls
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS=hello@example.com
MAIL_FROM_NAME="Example"
6 changes: 2 additions & 4 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,10 @@ XDEBUG_MODE=off php public/index.php # Direct server start (faster, no debuggin

### Testing
```bash
composer test # Pest tests (XDEBUG_MODE=off)
composer test # PHPUnit tests (XDEBUG_MODE=off)
composer test:coverage # With coverage reports
composer test:parallel # Parallel execution
```
- **Test Framework**: Pest PHP with custom HTTP client helpers
- **Test Framework**: PHPUnit with custom HTTP client helpers
- **Test Structure**: `tests/Feature/` and `tests/Unit/` with shared `TestCase`
- **HTTP Testing**: Uses Amp HTTP client with helper functions: `get()`, `post()`, etc.

Expand Down Expand Up @@ -133,7 +132,6 @@ class MyController extends Controller
- `config/app.php` - Service provider registration and app config
- `vendor/phenixphp/framework/src/Queue/` - Queue implementation details
- `vendor/phenixphp/framework/src/Tasks/QueuableTask.php` - Base task class
- `tests/Pest.php` - HTTP testing helpers and setup
- `bootstrap/app.php` - Application bootstrap via `AppBuilder`

## Common Pitfalls
Expand Down
37 changes: 31 additions & 6 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,30 @@ on:
jobs:
test:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_DATABASE: phenix
MYSQL_USER: phenix
MYSQL_PASSWORD: secret
MYSQL_ROOT_PASSWORD: secret
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping -h 127.0.0.1 -uroot -psecret --silent"
--health-interval=10s
--health-timeout=5s
--health-retries=10
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd="redis-cli ping"
--health-interval=10s
--health-timeout=5s
--health-retries=10

steps:
- name: Checkout code
Expand All @@ -20,7 +44,7 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: 8.2
extensions: json, mbstring, pcntl, intl, fileinfo
extensions: json, mbstring, pcntl, intl, fileinfo, sockets, mysqli, sqlite3
coverage: xdebug

- name: Setup problem matchers
Expand All @@ -38,21 +62,22 @@ jobs:

- name: Analyze code statically with PHPStan
run: |
cp .env.example .env
vendor/bin/phpstan --xdebug
cp .env.example .env.testing
XDEBUG_MODE=off vendor/bin/phpstan --xdebug

- name: Execute tests
run: |
cp .env.example .env
vendor/bin/pest --coverage
cp .env.example .env.testing
php phenix key:generate .env.testing --force
vendor/bin/phpunit

- name: Prepare paths for SonarQube analysis
run: |
sed -i "s|$GITHUB_WORKSPACE|/github/workspace|g" build/logs/clover.xml
sed -i "s|$GITHUB_WORKSPACE|/github/workspace|g" build/report.junit.xml

- name: Run SonarQube analysis
uses: sonarsource/sonarcloud-github-action@master
uses: sonarsource/sonarqube-scan-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ tests/coverage
node_modules
npm-debug.log
package-lock.json
package.json
package.json
*.sqlite*
21 changes: 21 additions & 0 deletions app/Constants/OneTimePasswordScope.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace App\Constants;

enum OneTimePasswordScope: string
{
case LOGIN = 'login';

case RESET_PASSWORD = 'reset_password';

case VERIFY_EMAIL = 'verify_email';

case AUTHORIZE = 'authorize';

public static function toArray(): array
{
return array_column(self::cases(), 'value');
}
}
59 changes: 59 additions & 0 deletions app/Http/Controllers/Auth/ForgotPasswordController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Auth;

use App\Constants\OneTimePasswordScope;
use App\Models\User;
use App\Models\UserOtp;
use Egulias\EmailValidator\Validation\DNSCheckValidation;
use Egulias\EmailValidator\Validation\NoRFCWarningsValidation;
use Phenix\Http\Constants\HttpStatus;
use Phenix\Http\Controller;
use Phenix\Http\Request;
use Phenix\Http\Response;
use Phenix\Util\Date;
use Phenix\Validation\Types\Email;
use Phenix\Validation\Validator;

class ForgotPasswordController extends Controller
{
public function store(Request $request): Response
{
$validator = new Validator($request);
$validator->setRules([
'email' => Email::required()->validations(
new DNSCheckValidation(),
new NoRFCWarningsValidation()
)->max(100),
]);

if ($validator->fails()) {
return response()->json([
'errors' => $validator->failing(),
], HttpStatus::UNPROCESSABLE_ENTITY);
}

$user = User::query()
->whereEqual('email', $request->body('email'))
->whereNotNull('email_verified_at')
->first();

if ($user !== null) {
$otpCount = UserOtp::query()
->whereEqual('user_id', $user->id)
->whereEqual('scope', OneTimePasswordScope::RESET_PASSWORD->value)
->whereGreaterThanOrEqual('created_at', Date::now()->subHour()->toDateTimeString())
->count();

if ($otpCount < 5) {
$user->sendOneTimePassword(OneTimePasswordScope::RESET_PASSWORD);
}
}

return response()->json([
'message' => trans('auth.password_reset.sent'),
], HttpStatus::OK);
}
}
132 changes: 132 additions & 0 deletions app/Http/Controllers/Auth/LoginController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Auth;

use App\Constants\OneTimePasswordScope;
use App\Models\User;
use App\Models\UserOtp;
use Egulias\EmailValidator\Validation\DNSCheckValidation;
use Egulias\EmailValidator\Validation\NoRFCWarningsValidation;
use Phenix\Facades\Hash;
use Phenix\Http\Constants\HttpStatus;
use Phenix\Http\Controller;
use Phenix\Http\Request;
use Phenix\Http\Response;
use Phenix\Util\Date;
use Phenix\Validation\Types\Email;
use Phenix\Validation\Types\Numeric;
use Phenix\Validation\Types\Password;
use Phenix\Validation\Validator;

class LoginController extends Controller
{
public function login(Request $request): Response
{
$validator = new Validator($request);
$validator->setRules([
'email' => Email::required()->validations(
new DNSCheckValidation(),
new NoRFCWarningsValidation()
)->max(100)
->exists('users', 'email', function ($query): void {
$query->whereNotNull('email_verified_at');
}),
'password' => Password::required(),
]);

if ($validator->fails()) {
return response()->json([
'errors' => $validator->failing(),
], HttpStatus::UNPROCESSABLE_ENTITY);
}

$user = User::query()->whereEqual('email', $request->body('email'))->first();
$response = response()->json([
'message' => trans('auth.otp.login.sent'),
]);

if (! Hash::verify($user->password, (string) $request->body('password'))) {
$response = response()->json([
'message' => trans('auth.login.invalid_credentials'),
], HttpStatus::UNAUTHORIZED);
} else {
$otpCount = UserOtp::query()
->whereEqual('user_id', $user->id)
->whereEqual('scope', OneTimePasswordScope::LOGIN->value)
->whereGreaterThanOrEqual('created_at', Date::now()->subHour()->toDateTimeString())
->count();

if ($otpCount >= 5) {
$response = response()->json([
'message' => trans('auth.otp.limit_exceeded'),
], HttpStatus::TOO_MANY_REQUESTS);
} else {
$user->sendOneTimePassword(OneTimePasswordScope::LOGIN);
}
}

return $response;
}

public function authorize(Request $request): Response
{
$validator = new Validator($request);
$validator->setRules([
'email' => Email::required()->validations(
new DNSCheckValidation(),
new NoRFCWarningsValidation()
)->max(100)
->exists('users', 'email', function ($query): void {
$query->whereNotNull('email_verified_at');
}),
'otp' => Numeric::required()->digits(6),
]);

if ($validator->fails()) {
return response()->json([
'errors' => $validator->failing(),
], HttpStatus::UNPROCESSABLE_ENTITY);
}

$user = User::query()->whereEqual('email', $request->body('email'))->first();

$otp = UserOtp::query()
->whereEqual('user_id', $user->id)
->whereEqual('scope', OneTimePasswordScope::LOGIN->value)
->whereEqual('code', hash('sha256', (string) $request->body('otp')))
->whereNull('used_at')
->whereGreaterThanOrEqual('expires_at', Date::now()->toDateTimeString())
->first();

if (! $otp) {
return response()->json([
'message' => trans('auth.otp.invalid'),
], HttpStatus::NOT_FOUND);
}

$otp->usedAt = Date::now();
$otp->save();

$token = $user->createToken('auth_token');

return response()->json([
'access_token' => $token->toString(),
'expires_at' => $token->expiresAt()->toDateTimeString(),
'token_type' => 'Bearer',
]);
}

public function logout(Request $request): Response
{
/** @var User|null $user */
$user = $request->user();

$user?->currentAccessToken()?->delete();

return response()->json([
'message' => trans('auth.logout.success'),
], HttpStatus::OK);
}
}
Loading
Loading