-
Notifications
You must be signed in to change notification settings - Fork 0
das-22-implement-missing-api-routes-author-openapi-specification #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
📝 WalkthroughSummary by CodeRabbitRelease Notes
✏️ Tip: You can customize this high-level summary in your review settings. WalkthroughAdds a complete versioned API surface: registration/login/logout/sessions, password reset, email verification, two‑factor flows, account deactivate/reactivate/delete; new FormRequests, ApiThrottle middleware/config, ApiResponse helper, OpenAPI generator + spec, UserDeletionService, User model changes, migrations and indexes, many controllers/resources, extensive tests, and CI/frontend pnpm changes. Changes
Sequence Diagram(s)sequenceDiagram
actor Client
participant API as RegisterController
participant Req as RegisterRequest
participant DB as Database
participant Sanctum as Sanctum
participant Mailer as Notification Queue
Client->>API: POST /api/v1/register (name,email,password)
API->>Req: validate()
Req-->>API: validated data
API->>DB: User::create(...)
DB-->>API: User
API->>Sanctum: createToken(api-token-{device})
Sanctum-->>API: token
API->>Mailer: dispatch VerifyEmailNotification
Mailer-->>API: queued
API-->>Client: 201 {user, token, token_type}
sequenceDiagram
actor Client
participant API as TwoFactorController
participant Google2FA as Google2FA Lib
participant Crypto as Laravel Encryption
participant DB as Database
Client->>API: POST /api/v1/user/two-factor-authentication
API->>Google2FA: generateSecret()
Google2FA-->>API: secret
API->>Crypto: encrypt(secret)
Crypto-->>API: encrypted_secret
API->>DB: save encrypted secret on user
DB-->>API: stored
API-->>Client: 200 {secret, qr_code_url}
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Areas needing focused review:
Suggested labels
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsErrors were encountered while retrieving linked issues. Errors (1)
Comment |
|
| Method | Status | Description |
|---|---|---|
| success() | 200 | Standard success payload |
| error() | 400+ | General errors with optional code |
| validationError() | 422 | Validation failures |
| unauthorized() | 401 | Authentication required |
| forbidden() | 403 | Permission denied |
| notFound() | 404 | Resource missing |
| rateLimited() | 429 | Throttling response with headers |
return ApiResponse::success($data, 'Fetched successfully');Implications
- Consistent API format.
- Automatic HTTP codes.
- Enhanced error codes for clients.
Middleware: ApiThrottle
Extends Laravel’s ThrottleRequests to apply custom limits:
- Auth routes: 5 attempts/min (configurable).
- Password reset: 3 attempts/15 min.
- Default API: 100 attempts/min.
- Returns standardized 429 JSON via
ApiResponse.
// In Kernel.php
'apiThrottle' => \App\Http\Middleware\ApiThrottle::class,Model & Notification Updates
- User model adds SoftDeletes, deactivation fields, encrypted 2FA secrets, and overrides
sendEmailVerificationNotification(). - VerifyEmailNotification sends a signed URL for email verification via API.
$user->notify(new VerifyEmailNotification);API Controllers & Endpoints
All endpoints use ApiResponse and FormRequests to enforce validation and security. The baseUrl for examples is https://api.example.com.
Register a New User
Registers and returns a Sanctum token.
{
"title": "Register User",
"description": "Create a new user account and return access token",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/register",
"headers": [],
"bodyType": "json",
"requestBody": "{\n \"name\": \"John Doe\",\n \"email\": \"[email protected]\",\n \"password\": \"password123\",\n \"password_confirmation\": \"password123\"\n}",
"responses": {
"201": {
"description": "User registered successfully",
"body": "{\n \"success\": true,\n \"message\": \"User registered successfully\",\n \"data\": {\n \"user\": {\"id\":1,\"name\":\"John Doe\",\"email\":\"[email protected]\"},\n \"token\":\"<token>\",\n \"token_type\":\"Bearer\"\n }\n}"
},
"422": {
"description": "Validation failed",
"body": "{\n \"success\": false,\n \"message\": \"Validation failed\",\n \"errors\": {\"email\":[\"The email has already been taken.\"]}\n}"
},
"500": {
"description": "Registration error",
"body": "{\n \"success\": false,\n \"message\": \"Registration failed\",\n \"error_code\": \"REGISTRATION_ERROR\"\n}"
}
}
}
Login
Authenticates user and returns a token.
{
"title": "User Login",
"description": "Authenticate with email and password",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/login",
"headers": [],
"bodyType": "json",
"requestBody": "{\n \"email\": \"[email protected]\",\n \"password\": \"password123\",\n \"device\": \"web\"\n}",
"responses": {
"200": {
"description": "Login successful",
"body": "{\n \"success\": true,\n \"message\": \"Login successful\",\n \"data\": {\"user\": {\"id\":1,\"name\":\"John Doe\",\"email\":\"[email protected]\"},\"token\":\"<token>\",\"token_type\":\"Bearer\"}\n}"
},
"422": {
"description": "Invalid credentials",
"body": "{\n \"errors\": {\"email\":[\"The provided credentials are incorrect.\"]}\n}"
}
}
}
Logout (Current Device)
Revokes current token.
{
"title": "Logout Current Device",
"description": "Revoke the current API token",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/logout",
"headers": [
{"key":"Authorization","value":"Bearer <token>","required":true}
],
"bodyType": "none",
"responses": {
"200": {
"description": "Logged out successfully",
"body": "{\n \"success\": true,\n \"message\": \"Successfully logged out from this device.\",\n \"data\": null\n}"
},
"400": {
"description": "Requires token auth",
"body": "{\n \"success\": false,\n \"message\": \"This endpoint requires token-based authentication. Use session logout instead.\",\n \"error_code\": \"TOKEN_AUTH_REQUIRED\"\n}"
}
}
}
Logout All Devices
Revokes all tokens.
{
"title": "Logout All Devices",
"description": "Revoke all API tokens for the user",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/logout-all",
"headers": [
{"key":"Authorization","value":"Bearer <token>","required":true}
],
"bodyType": "none",
"responses": {
"200": {
"description": "All sessions terminated",
"body": "{\n \"success\": true,\n \"message\": \"Successfully logged out from all devices.\",\n \"data\": null\n}"
}
}
}
Get Authenticated User
Returns current user info.
{
"title": "Get Current User",
"description": "Retrieve authenticated user information",
"method": "GET",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user",
"headers": [
{"key":"Authorization","value":"Bearer <token>","required":true}
],
"bodyType": "none",
"responses": {
"200": {
"description": "User data",
"body": "{\n \"success\": true,\n \"data\": {\"user\": {\"id\":1,\"name\":\"John Doe\",\"email\":\"[email protected]\"}}\n}"
}
}
}
Update Profile
Edits name/email; sends verification if email changes.
{
"title": "Update Profile",
"description": "Modify user name or email",
"method": "PATCH",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/profile/update",
"headers": [
{"key":"Authorization","value":"Bearer <token>","required":true}
],
"bodyType": "json",
"requestBody": "{\n \"name\": \"Jane Doe\",\n \"email\": \"[email protected]\",\n \"current_password\": \"password123\"\n}",
"responses": {
"200": {
"description": "Profile updated",
"body": "{\n \"data\": {\"user\": {\"id\":1,\"name\":\"Jane Doe\",\"email\":\"[email protected]\",\"email_verified_at\":null}},\n \"message\": \"Profile updated successfully.\"\n}"
},
"422": {
"description": "Validation failed",
"body": "{\n \"errors\": {\"email\":[\"This email address is already in use.\"]}\n}"
}
}
}
Update Password
Changes password and revokes other tokens.
{
"title": "Update Password",
"description": "Change user password",
"method": "PATCH",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/password/update",
"headers": [
{"key":"Authorization","value":"Bearer <token>","required":true}
],
"bodyType": "json",
"requestBody": "{\n \"current_password\": \"oldpass123\",\n \"password\": \"newpass123\",\n \"password_confirmation\": \"newpass123\"\n}",
"responses": {
"200": {
"description": "Password updated",
"body": "{\n \"success\": true,\n \"message\": \"Password updated successfully.\" \n}"
},
"422": {
"description": "Validation failed",
"body": "{\n \"errors\": {\"current_password\":[\"The current password is incorrect.\"]}\n}"
}
}
}
Forgot Password
Sends reset link to email.
{
"title": "Forgot Password",
"description": "Request password reset link",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/forgot-password",
"headers": [],
"bodyType": "json",
"requestBody": "{\n \"email\": \"[email protected]\"\n}",
"responses": {
"200": {
"description": "Reset link sent",
"body": "{\n \"success\": true,\n \"message\": \"Password reset link sent to your email address.\" \n}"
},
"422": {
"description": "Validation failed",
"body": "{\n \"errors\": {\"email\":[\"Please provide a valid email address.\"]}\n}"
}
}
}
Reset Password
Resets password using token.
{
"title": "Reset Password",
"description": "Reset password with token",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/reset-password",
"headers": [],
"bodyType": "json",
"requestBody": "{\n \"email\": \"[email protected]\",\n \"token\": \"reset-token\",\n \"password\": \"newpass123\",\n \"password_confirmation\": \"newpass123\"\n}",
"responses": {
"200": {
"description": "Password reset",
"body": "{\n \"success\": true,\n \"message\": \"Password has been reset successfully.\" \n}"
},
"422": {
"description": "Invalid token or validation error",
"body": "{\n \"errors\": {\"email\":[\"This password reset token is invalid.\"]}\n}"
}
}
}
Deactivate Account
Marks account deactivated and revokes tokens.
{
"title": "Deactivate Account",
"description": "Temporarily disable user account",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/deactivate",
"headers": [
{"key":"Authorization","value":"Bearer <token>","required":true}
],
"bodyType": "json",
"requestBody": "{\n \"current_password\": \"password123\",\n \"reason\": \"No longer needed\"\n}",
"responses": {
"200": {
"description": "Account deactivated",
"body": "{\n \"success\": true,\n \"message\": \"Account has been deactivated successfully. All sessions have been terminated.\" \n}"
},
"400": {
"description": "Already deactivated",
"body": "{\n \"success\": false,\n \"error_code\": \"ACCOUNT_ALREADY_DEACTIVATED\",\n \"message\": \"Account is already deactivated.\" \n}"
},
"422": {
"description": "Invalid password",
"body": "{\n \"success\": false,\n \"error_code\": \"INVALID_CURRENT_PASSWORD\",\n \"message\": \"Current password is incorrect.\" \n}"
}
}
}
Reactivate Account
Re-enables a deactivated account.
{
"title": "Reactivate Account",
"description": "Restore a deactivated account",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/reactivate",
"headers": [
{"key":"Authorization","value":"Bearer <token>","required":true}
],
"bodyType": "none",
"responses": {
"200": {
"description": "Account reactivated",
"body": "{\n \"success\": true,\n \"message\": \"Account has been reactivated successfully.\" \n}"
},
"400": {
"description": "Already active",
"body": "{\n \"success\": false,\n \"error_code\": \"ACCOUNT_ALREADY_ACTIVE\",\n \"message\": \"Account is already active.\" \n}"
}
}
}
Delete Account
Permanently deletes user via GDPR-compliant service.
{
"title": "Delete Account",
"description": "Permanently remove user and related data",
"method": "DELETE",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/account/delete",
"headers": [
{"key":"Authorization","value":"Bearer <token>","required":true}
],
"bodyType": "json",
"requestBody": "{\n \"current_password\": \"password123\"\n}",
"responses": {
"200": {
"description": "Account deleted",
"body": "{\n \"success\": true,\n \"message\": \"Account has been deleted successfully.\" \n}"
},
"422": {
"description": "Invalid password",
"body": "{\n \"success\": false,\n \"error_code\": \"INVALID_CURRENT_PASSWORD\",\n \"message\": \"Current password is incorrect.\" \n}"
}
}
}
Send Email Verification
Sends a signed verification link.
{
"title": "Send Verification Email",
"description": "Email user a verification link",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/email/verification-notification",
"headers": [
{"key":"Authorization","value":"Bearer <token>","required":true}
],
"bodyType": "none",
"responses": {
"200": {
"description": "Link sent",
"body": "{\n \"success\": true,\n \"message\": \"Email verification link sent successfully.\" \n}"
},
"400": {
"description": "Already verified",
"body": "{\n \"success\": false,\n \"error_code\": \"EMAIL_ALREADY_VERIFIED\",\n \"message\": \"Email is already verified.\" \n}"
},
"500": {
"description": "Send failed",
"body": "{\n \"success\": false,\n \"error_code\": \"EMAIL_SEND_FAILED\",\n \"message\": \"Failed to send verification email. Please try again later.\" \n}"
}
}
}
Verify Email
Confirms email via signed link.
{
"title": "Verify Email",
"description": "Validate email verification link",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/email/verify",
"headers": [],
"bodyType": "json",
"requestBody": "{\n \"id\":1,\n \"hash\":\"<hash>\",\n \"signature\":\"<signature>\"\n}",
"responses": {
"200": {
"description": "Email verified",
"body": "{\n \"success\": true,\n \"message\": \"Email verified successfully.\",\n \"data\": {\"user\": {\"id\":1,\"name\":\"John Doe\",\"email\":\"[email protected]\",\"email_verified_at\":\"2025-01-01T00:00:00Z\"}}\n}"
},
"400": {
"description": "Invalid or expired",
"body": "{\n \"success\": false,\n \"error_code\": \"INVALID_VERIFICATION_LINK\",\n \"message\": \"Invalid verification link.\" \n}"
}
}
}
Enable Two-Factor Authentication
Initiates 2FA setup.
{
"title": "Enable Two-Factor",
"description": "Generate 2FA secret and QR code",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/two-factor/enable",
"headers": [
{"key":"Authorization","value":"Bearer <token>","required":true}
],
"bodyType": "json",
"requestBody": "{\n \"password\": \"password123\"\n}",
"responses": {
"200": {
"description": "Setup initiated",
"body": "{\n \"data\": {\"secret\":\"<secret>\",\"qr_code_url\":\"<url>\",\"next_step\":\"Scan...\"},\n \"message\": \"Two-factor authentication setup initiated.\" \n}"
},
"400": {
"description": "Already enabled",
"body": "{\n \"success\": false,\n \"error_code\": \"TWO_FACTOR_ALREADY_ENABLED\",\n \"message\": \"Two-factor authentication is already enabled.\" \n}"
}
}
}
Confirm Two-Factor Authentication
Verifies 2FA code and generates recovery codes.
{
"title": "Confirm Two-Factor",
"description": "Validate 2FA code and enable it",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/two-factor/confirm",
"headers": [
{"key":"Authorization","value":"Bearer <token>","required":true}
],
"bodyType": "json",
"requestBody": "{\n \"code\":\"123456\",\n \"password\":\"password123\"\n}",
"responses": {
"200": {
"description": "2FA enabled",
"body": "{\n \"data\": {\"recovery_codes\":[\"ABCDEF...\",...],\"warning\":\"Save these codes.\"},\n \"message\": \"Two-factor authentication has been enabled successfully.\" \n}"
},
"422": {
"description": "Invalid code or setup not initiated",
"body": "{\n \"success\": false,\n \"error_code\": \"INVALID_TWO_FACTOR_CODE\",\n \"message\": \"Invalid two-factor authentication code.\" \n}"
}
}
}
Get Recovery Codes
Retrieves active recovery codes.
{
"title": "Get Recovery Codes",
"description": "Fetch saved 2FA recovery codes",
"method": "GET",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/two-factor/recovery-codes",
"headers": [
{"key":"Authorization","value":"Bearer <token>","required":true}
],
"bodyType": "none",
"responses": {
"200": {
"description": "Codes retrieved",
"body": "{\n \"data\": {\"recovery_codes\":[\"ABCDEF...\",...],\"warning\":\"Make sure to save...\"},\n \"message\": \"Recovery codes retrieved successfully.\" \n}"
}
}
}
Regenerate Recovery Codes
Invalidates old codes and returns new ones.
{
"title": "Regenerate Recovery Codes",
"description": "Create new 2FA recovery codes",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/two-factor/recovery-codes/regenerate",
"headers": [
{"key":"Authorization","value":"Bearer <token>","required":true}
],
"bodyType": "none",
"responses": {
"200": {
"description": "New codes",
"body": "{\n \"data\": {\"recovery_codes\":[\"XYZ123...\",...],\"warning\":\"Previous codes invalidated.\"},\n \"message\": \"Recovery codes regenerated successfully.\" \n}"
}
}
}
Disable Two-Factor Authentication
Turns off 2FA.
{
"title": "Disable Two-Factor",
"description": "Remove 2FA secrets and codes",
"method": "DELETE",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/two-factor/disable",
"headers": [
{"key":"Authorization","value":"Bearer <token>","required":true}
],
"bodyType": "none",
"responses": {
"200": {
"description": "2FA disabled",
"body": "{\n \"success\": true,\n \"message\": \"Two-factor authentication has been disabled successfully.\" \n}"
}
}
}
List Sessions
Lists active API tokens.
{
"title": "List Sessions",
"description": "Get active API sessions",
"method": "GET",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/sessions",
"headers": [
{"key":"Authorization","value":"Bearer <token>","required":true}
],
"bodyType": "none",
"responses": {
"200": {
"description": "Session list",
"body": "{\n \"data\": {\"sessions\":[{...}],\"total\":3},\n \"message\": \"Operation successful\" \n}"
}
}
}
Revoke Session
Deletes a specific token (not current).
{
"title": "Revoke Session",
"description": "Remove a specific API session token",
"method": "DELETE",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/sessions/123",
"headers": [
{"key":"Authorization","value":"Bearer <token>","required":true}
],
"pathParams": [
{"key":"sessionId","value":"Session token ID","required":true}
],
"bodyType": "none",
"responses": {
"200": {
"description": "Session revoked",
"body": "{\n \"success\": true,\n \"message\": \"Session revoked successfully.\" \n}"
},
"404": {
"description": "Not found",
"body": "{\n \"success\": false,\n \"error_code\": \"SESSION_NOT_FOUND\",\n \"message\": \"Session not found.\" \n}"
},
"400": {
"description": "Cannot revoke current session",
"body": "{\n \"success\": false,\n \"error_code\": \"CANNOT_REVOKE_CURRENT_SESSION\",\n \"message\": \"Cannot revoke the current session.\" \n}"
}
}
}
Settings Profile Deletion (Web)
The web Settings\ProfileController now uses UserDeletionService for GDPR-compliance:
$deletionService->deleteUser($user, 'User requested account deletion via web interface');Implications
- Atomic deletion via transactions.
- Audit logs and cascade cleanup.
- Service reuse across web and API.
HandleInertiaRequests Update
Casts Inertia-shared quote data to string for type safety:
- 'quote' => ['message' => trim($message), 'author' => trim($author)],
+ 'quote' => ['message' => trim((string) $message), 'author' => trim((string) $author)],Request Validation Classes
Each API endpoint now has a dedicated FormRequest:
- Auth:
LoginRequest,RegisterRequest,ForgotPasswordRequest,ResetPasswordRequest,UpdatePasswordRequest,ConfirmTwoFactorRequest,EnableTwoFactorRequest,DeactivateAccountRequest,DeleteAccountRequest,VerifyEmailRequest,UpdateProfileRequest. - Enforces
authorize()and custom messages. - Secures routes with
auth()->check()defense in depth.
This PR delivers a robust API foundation with standardized responses, full validation, security best practices, and automated OpenAPI documentation generation.
This documentation was automatically generated by DocuWriter.ai 🚀
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 63
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
app/Http/Middleware/HandleInertiaRequests.php (1)
39-49: String casts are fine, but this whole quote pipeline is trying way too hardThe
(string)casts beforetrim()are a reasonable fix to keep PHP 8.1+ from whining when$authorcomes back asnull. No complaints there.The rest of this, though:
[$message, $author] = str(Inspiring::quotes()->random())->explode('-');is classic “I saw Stringable in the docs once” energy. You’re spinning up a
Stringable, turning it into aCollection, then destructuring it, just to split on a dash. Also, if the quote doesn’t contain'-',$authorbecomesnulland silently turns into''via the cast, which is a bit magic.If you ever touch this again, consider something closer to:
[$message, $author] = explode('-', Inspiring::quotes()->random(), 2); $quote = [ 'message' => trim((string) $message), // keep `null` when there is no author, instead of hiding it as '' 'author' => $author !== null ? trim((string) $author) : null, ];Same behavior for normal quotes, clearer intent, less Laravel ceremony for what’s basically
explode(). But as-is, the new casts themselves are fine to ship.app/Actions/Fortify/CreateNewUser.php (1)
17-24: I'll help you verify the concerns raised in this review comment about password hashing and validation. Let me investigate the User model's password handling and how the CreateNewUser action is called.
<function_calls>
#!/bin/bashFirst, let's find the User model and check for password hashing mechanism
echo "=== Finding User model ==="
fd -t f "User.php" app/ | head -20echo ""
echo "=== Checking User model for password mutator/cast ==="
if [ -f "app/Models/User.php" ]; then
wc -l app/Models/User.php
fi
</function_calls><function_calls>
#!/bin/bashRead the User model to check for password handling
cat -n app/Models/User.php
</function_calls><function_calls>
#!/bin/bashSearch for password mutator specifically and casts array
echo "=== Searching for password mutator in User model ==="
rg -n "setPasswordAttribute|'password'|makeHidden" app/Models/User.php -A3 -B1echo ""
echo "=== Searching for 'password' references in casts or attributes ==="
rg -n "protected.$casts|protected.$hidden" app/Models/User.php -A5
</function_calls><function_calls>
#!/bin/bashFind all usages of CreateNewUser action
echo "=== Finding CreateNewUser usage ==="
rg -n "CreateNewUser|create(array" app/Actions/Fortify/ -C5echo ""
echo "=== Searching broader for CreateNewUser ==="
rg -rn "new CreateNewUser|CreateNewUser" --type=php -C3
</function_calls><function_calls>
#!/bin/bashCheck if there's a Fortify provider or configuration that handles validation
echo "=== Looking for Fortify related files ==="
find . -path ./node_modules -prune -o -name "Fortify" -type f -print | head -20echo ""
echo "=== Checking CreateNewUser file itself ==="
head -50 app/Actions/Fortify/CreateNewUser.php
</function_calls><function_calls>
#!/bin/bashCheck config/fortify.php for registration validation
echo "=== Checking Fortify configuration ==="
if [ -f "config/fortify.php" ]; then
cat -n config/fortify.php | head -100
fi
</function_calls><function_calls>
#!/bin/bashLook for Request validation classes for registration
echo "=== Finding Request classes ==="
fd -t f -e php "Request" app/ | grep -i registerecho ""
echo "=== Checking for validation rules in registration ==="
rg -n "rules()|authorize()" --type=php | head -30
</function_calls><function_calls>
#!/bin/bashCheck if User model uses Hash facade or mutator for password
echo "=== Searching for Hash usage in User model ==="
rg -n "Hash|mutate|password" app/Models/User.phpecho ""
echo "=== Checking Laravel's authentication scaffolding patterns ==="
echo "Looking for trait usage that might handle password hashing:"
rg -n "use.*Authenticatable|implements.*UserContract" app/Models/User.php
</function_calls>Now let me check web documentation about Laravel's default password handling:
<function_calls>
Laravel User model password hashing setPasswordAttribute mutator
</function_calls><function_calls>
Laravel Fortify CreateNewUser password validation registration
</function_calls><function_calls>
#!/bin/bashLet's check the broader context - looking at how registration flows through Fortify
echo "=== Looking for registration controller or action ==="
fd -t f ".Register..php" --type f | head -20echo ""
echo "=== Checking app/Http directory ==="
find app/Http -name "*.php" -type f 2>/dev/null | head -20
</function_calls><function_calls>
#!/bin/bashLook for where CreateNewUser gets instantiated and how it's called
echo "=== Complete search for CreateNewUser instantiation ==="
rg -n "CreateNewUser" --type=php -B5 -A5echo ""
echo "=== Check how the action receives its input ==="
rg -n "->create(" --type=php -B3 -A3 | head -50
</function_calls><function_calls>
#!/bin/bashLook at the entire CreateNewUser file
echo "=== Complete CreateNewUser.php file ==="
cat -n app/Actions/Fortify/CreateNewUser.phpecho ""
echo "=== Check complete User.php file ==="
wc -l app/Models/User.php
</function_calls>Let me get more specific information about the User model:
<function_calls>
#!/bin/bashFull User model content
if [ -f "app/Models/User.php" ]; then
cat app/Models/User.php
fi
</function_calls><function_calls>
#!/bin/bashCheck for a RegistrationRequest or similar validation class
echo "=== Looking for Requests directory ==="
find app -type d -name "Requests" 2>/dev/nullecho ""
echo "=== Searching for validation rules related to registration/create ==="
rg -n "password.*confirmed|'email'|'name'" app/ --type=php | grep -i "rules|validate" | head -20
</function_calls><function_calls>
#!/bin/bashCheck if password is explicitly hashed somewhere before being passed to create
echo "=== Checking if password is hashed before User::create ==="
rg -n "Hash::|bcrypt" app/Actions/Fortify/ -B5 -A5echo ""
echo "=== Check Fortify Provider configuration ==="
find . -path ./node_modules -prune -o -name "FortifyServiceProvider*" -print
</function_calls><function_calls>
#!/bin/bashLet's check the Laravel framework's default User model setup
echo "=== Checking if setPasswordAttribute mutator exists elsewhere in codebase ==="
rg -rn "setPasswordAttribute" --type=phpecho ""
echo "=== Check Laravel's Authenticatable trait default behavior ==="
echo "Looking for password-related attributes in User:"
rg -n "protected.*password|'password'.*hidden" app/Models/User.php
</function_calls><function_calls>
#!/bin/bashSearch for any validation being applied
echo "=== Looking for validation implementation ==="
rg -n "validate|Validator|Request" app/Actions/Fortify/CreateNewUser.phpecho ""
echo "=== Full CreateNewUser action ==="
cat app/Actions/Fortify/CreateNewUser.phpecho ""
echo "=== Full User model ==="
cat app/Models/User.php
</function_calls><function_calls>
#!/bin/bashCheck if this is Laravel Fortify by looking at providers and config
echo "=== Check if Fortify is installed ==="
grep -r "Fortify" config/ composer.json 2>/dev/null | head -10echo ""
echo "=== Look for where CreateNewUser action is registered/called ==="
rg -rn "CreateNewUser|CreatesNewUsers" --type=php
</function_calls><function_calls>
#!/bin/bashLook for provider or controller that handles registration requests
echo "=== Searching for registration handlers ==="
find app -name "*.php" -type f -exec grep -l "CreateNewUser|register" {} ; | head -20echo ""
echo "=== Check Provider registration ==="
if [ -f "app/Providers/FortifyServiceProvider.php" ]; then
cat app/Providers/FortifyServiceProvider.php
fi
</function_calls><function_calls>
#!/bin/bashFinal check: look for where password might be hashed in the request handling
echo "=== Search for bcrypt or Hash in entire app directory ==="
rg -n "bcrypt|Hash::make" app/ --type=phpecho ""
echo "=== Check for any middleware or service that handles password ==="
rg -n "password.*hash|hash.*password" app/ --type=php -i
</function_calls>Based on my verification, I can now provide the rewritten review comment:
Password hashing must be verified—this action doesn't hash passwords, relying entirely on upstream handling.
The
create()method passes the raw input array directly toUser::create()with no explicit password hashing. Laravel'sAuthenticatabletrait does NOT automatically hash passwords on create; the password is stored as-is.This is only safe if:
- The input array's
passwordfield is pre-hashed before reaching this action- This action is exclusively called from a validated request handler that hashes the password beforehand
If neither condition is met, plaintext passwords will be stored in the database.
Verify that:
- Passwords are hashed before this action receives them (e.g., in a request validation or controller layer)
- Or add password hashing here:
'password' => Hash::make($input['password'])
| branches: | ||
| - develop | ||
| - main | ||
| - master | ||
| pull_request: | ||
| branches: | ||
| - develop | ||
| - main | ||
| - master | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
CI migrate step may be pointing at the wrong database
The new php artisan migrate --force step is where things can go sideways:
- In CI,
php artisan migratewill use.env, not the sqlite:memory:config fromphpunit.xml. If.env.exampleis still MySQL‑by‑default, this step can blow up unless you’ve actually wired a DB service + secrets. - Your tests then run with Pest, which do honor
phpunit.xmland use sqlite in memory, so you’re effectively migrating one DB and testing against a different one.
Consider one of these saner options:
- Configure DB env vars in the workflow before running migrate so it uses the same sqlite connection as phpunit; or
- Drop the global migrate step and rely on Laravel’s usual testing traits (
RefreshDatabase, etc.) to handle migrations per test; or - Run
php artisan migrate --env=testing --forceand ensureconfig('database')fortestingmatches what phpunit uses.
Also, you’re doing full npm ci + npm run build for a PHP test job. If there are no JS/E2E tests in this workflow, that’s just burning minutes for fun. Split JS builds into their own workflow or job if you don’t need them here.
Also applies to: 49-50
🤖 Prompt for AI Agents
In .github/workflows/tests.yml around lines 5-12 (and also apply to lines
49-50), the CI job runs php artisan migrate which will pick up .env (likely
MySQL) while phpunit uses sqlite :memory:, and the job also runs npm build
unnecessarily; fix by (a) removing the global migrate step and relying on test
traits (RefreshDatabase) OR configuring DB env vars in the workflow before
migrate to point to the same sqlite in-memory connection phpunit uses OR run
migrate with --env=testing and ensure the testing database config matches
phpunit.xml, and (b) split out or remove npm ci + npm run build from this
PHP-only test job so JS builds run only in a dedicated job/workflow when needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review continued from previous batch...
|
| Variable | Description | Default |
|---|---|---|
| API_THROTTLE_MAX_ATTEMPTS | Max requests per decay period (general endpoints) | 100 |
| API_THROTTLE_DECAY_MINUTES | Decay period in minutes for resetting general attempts | 1 |
| API_THROTTLE_AUTH_MAX_ATTEMPTS | Max auth attempts per decay period (login, register, etc.) | 5 |
| API_THROTTLE_AUTH_DECAY_MINUTES | Decay period in minutes for resetting auth attempts | 1 |
| API_THROTTLE_PASSWORD_RESET_MAX_ATTEMPTS | Max password reset requests per decay period | 3 |
| API_THROTTLE_PASSWORD_RESET_DECAY_MINUTES | Decay period in minutes for resetting password reset attempts | 15 |
# .env.example snippet
API_THROTTLE_MAX_ATTEMPTS=100
API_THROTTLE_DECAY_MINUTES=1
API_THROTTLE_AUTH_MAX_ATTEMPTS=5
API_THROTTLE_AUTH_DECAY_MINUTES=1
API_THROTTLE_PASSWORD_RESET_MAX_ATTEMPTS=3
API_THROTTLE_PASSWORD_RESET_DECAY_MINUTES=15Development Guideline Updates
The following documentation files now include rector/rector (RECTOR) - v2 in the list of tooling:
- .junie/guidelines.md
- AGENTS.md
- CLAUDE.md
- GEMINI.md
Inline Validation Removed in Registration Action
The CreateNewUser action no longer performs inline validation; it delegates validation to the RegisterRequest FormRequest.
// app/Actions/Fortify/CreateNewUser.php
public function create(array $input): User
{
// Validator::make(...) removed
return User::create([
'name' => $input['name'],
'email' => $input['email'],
'password' => Hash::make($input['password']),
]);
}OpenAPI Specification Generator ✨
A new Artisan command generates an OpenAPI 3.0.3 YAML file by introspecting API routes, controllers, and FormRequest rules.
Usage
php artisan openapi:generate --output=path/to/openapi.yaml- Routes filtered by name (
api.v1.*), middleware (apiThrottle,api), or URI prefix (api/v1). - Automatic summaries extracted from route names, attributes (
#[Operation]), or docblocks. - Request bodies generated from FormRequest rules via reflection.
- Rate-limit responses included for status 429.
# Example output path
storage/openapi.yamlStandardized API Responses 📦
The App\Helpers\ApiResponse class offers static methods to build uniform JSON responses:
| Method | Purpose | Status Code |
|---|---|---|
success($data, $msg) |
Successful operation | 200 |
error($msg, $errors) |
Generic error | 400 |
validationError($errs) |
Validation failures | 422 |
unauthorized($msg) |
Unauthorized access | 401 |
forbidden($msg) |
Access forbidden | 403 |
notFound($msg) |
Resource not found | 404 |
rateLimited($retryAfter) |
Rate limit exceeded | 429 |
// Example
return ApiResponse::success(['id'=>1], 'User created', 201);Authentication Endpoints
POST /api/v1/register
Registers a new user and issues an API token.
{
"title": "Register User",
"description": "Create a new user account with credentials",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/register",
"headers": [
{ "key": "Content-Type", "value": "application/json", "required": true }
],
"bodyType": "json",
"requestBody": "{\n \"name\": \"John Doe\",\n \"email\": \"[email protected]\",\n \"password\": \"password123\"\n}",
"responses": {
"201": {
"description": "User registered successfully",
"body": "{\n \"success\": true,\n \"message\": \"User registered successfully\",\n \"data\": { \"user\": { \"id\":1, \"name\":\"John Doe\" }, \"token\":\"<token>\", \"token_type\":\"Bearer\" }\n}"
},
"422": {
"description": "Validation failed",
"body": "{\n \"success\": false,\n \"message\": \"Validation failed\",\n \"errors\": { \"email\": [\"The email has already been taken.\"] }\n}"
}
}
}
POST /api/v1/login
Authenticates a user and returns an access token.
{
"title": "User Login",
"description": "Authenticate user and get token",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/login",
"headers": [
{ "key": "Content-Type", "value": "application/json", "required": true }
],
"bodyType": "json",
"requestBody": "{\n \"email\": \"[email protected]\",\n \"password\": \"password123\"\n}",
"responses": {
"200": {
"description": "Login successful",
"body": "{\n \"success\": true,\n \"message\": \"Login successful\",\n \"data\": { \"user\": { \"id\":1 }, \"token\": \"<token>\", \"token_type\": \"Bearer\" }\n}"
},
"422": {
"description": "Invalid credentials",
"body": "{\n \"success\": false,\n \"message\": \"The provided credentials are incorrect.\",\n \"errors\": { \"email\": [\"The provided credentials are incorrect.\"] }\n}"
}
}
}
POST /api/v1/logout
Revokes the current token or session.
{
"title": "Logout",
"description": "Logout from current device",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/logout",
"headers": [
{ "key": "Authorization", "value": "Bearer <token>", "required": true }
],
"bodyType": "none",
"responses": {
"200": {
"description": "Successfully logged out",
"body": "{\n \"success\": true,\n \"message\": \"Successfully logged out from this device.\",\n \"data\": null\n}"
},
"401": {
"description": "Invalid token",
"body": "{\n \"success\": false,\n \"message\": \"Invalid or expired authentication token.\",\n \"error_code\": \"INVALID_TOKEN\"\n}"
}
}
}
POST /api/v1/logout-all
Revokes all tokens for the authenticated user.
{
"title": "Logout All Devices",
"description": "Logout from all devices",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/logout-all",
"headers": [
{ "key": "Authorization", "value": "Bearer <token>", "required": true }
],
"bodyType": "none",
"responses": {
"200": {
"description": "Successfully logged out from all devices",
"body": "{\n \"success\": true,\n \"message\": \"Successfully logged out from all devices.\",\n \"data\": null\n}"
},
"400": {
"description": "Token authentication required",
"body": "{\n \"success\": false,\n \"message\": \"This endpoint requires token-based authentication.\",\n \"error_code\": \"TOKEN_AUTH_REQUIRED\"\n}"
}
}
}
User & Profile Endpoints
GET /api/v1/user
Retrieves the authenticated user’s profile.
{
"title": "Get Authenticated User",
"description": "Get authenticated user information",
"method": "GET",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user",
"headers": [
{ "key": "Authorization", "value": "Bearer <token>", "required": true }
],
"bodyType": "none",
"responses": {
"200": {
"description": "Successful operation",
"body": "{\n \"success\": true,\n \"message\": \"Operation successful\",\n \"data\": { \"user\": { \"id\":1, \"email_verified_at\":null } }\n}"
}
}
}
PUT /api/v1/user/profile
Updates name/email and triggers re-verification if email changes.
{
"title": "Update User Profile",
"description": "Update user profile",
"method": "PUT",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/profile",
"headers": [
{ "key": "Authorization", "value": "Bearer <token>", "required": true },
{ "key": "Content-Type", "value": "application/json", "required": true }
],
"bodyType": "json",
"requestBody": "{\n \"name\": \"Jane Doe\",\n \"email\": \"[email protected]\",\n \"current_password\": \"password123\"\n}",
"responses": {
"200": {
"description": "Profile updated successfully",
"body": "{\n \"success\": true,\n \"message\": \"Profile updated successfully.\",\n \"data\": { \"user\": { \"email_verified_at\": null } }\n}"
},
"422": {
"description": "Validation failed",
"body": "{\n \"success\": false,\n \"message\": \"Validation failed\",\n \"errors\": { \"email\": [\"The email must be a valid email address.\"] }\n}"
}
}
}
PUT /api/v1/user/password
Updates the authenticated user’s password.
{
"title": "Update Password",
"description": "Update user password",
"method": "PUT",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/password",
"headers": [
{ "key": "Authorization", "value": "Bearer <token>", "required": true },
{ "key": "Content-Type", "value": "application/json", "required": true }
],
"bodyType": "json",
"requestBody": "{\n \"current_password\": \"oldPass123\",\n \"password\": \"newPass123\",\n \"password_confirmation\": \"newPass123\"\n}",
"responses": {
"200": {
"description": "Password updated successfully",
"body": "{\n \"success\": true,\n \"message\": \"Password updated successfully.\",\n \"data\": null\n}"
},
"422": {
"description": "Validation failed",
"body": "{\n \"success\": false,\n \"message\": \"Validation failed\",\n \"errors\": { \"password\": [\"The password confirmation does not match.\"] }\n}"
}
}
}
POST /api/v1/user/deactivate
Deactivates the user account and revokes all tokens.
{
"title": "Deactivate Account",
"description": "Deactivate user account",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/deactivate",
"headers": [
{ "key": "Authorization", "value": "Bearer <token>", "required": true },
{ "key": "Content-Type", "value": "application/json", "required": true }
],
"bodyType": "json",
"requestBody": "{\n \"current_password\": \"password123\",\n \"reason\": \"No longer needed\"\n}",
"responses": {
"200": {
"description": "Account deactivated",
"body": "{\n \"success\": true,\n \"message\": \"Account has been deactivated successfully. All sessions have been terminated.\",\n \"data\": null\n}"
},
"400": {
"description": "Already deactivated",
"body": "{\n \"success\": false,\n \"message\": \"Account is already deactivated.\",\n \"error_code\": \"ACCOUNT_ALREADY_DEACTIVATED\"\n}"
}
}
}
POST /api/v1/user/reactivate
Reactivates a deactivated account after password confirmation.
{
"title": "Reactivate Account",
"description": "Reactivate user account",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/reactivate",
"headers": [
{ "key": "Authorization", "value": "Bearer <token>", "required": true },
{ "key": "Content-Type", "value": "application/json", "required": true }
],
"bodyType": "json",
"requestBody": "{\n \"current_password\": \"password123\"\n}",
"responses": {
"200": {
"description": "Account reactivated",
"body": "{\n \"success\": true,\n \"message\": \"Account has been reactivated successfully.\",\n \"data\": null\n}"
},
"400": {
"description": "Already active or wrong password",
"body": "{\n \"success\": false,\n \"message\": \"The provided password does not match your current password.\",\n \"errors\": { \"current_password\": [\"The provided password does not match your current password.\"] }\n}"
}
}
}
DELETE /api/v1/user/account
Deletes the user account with GDPR-compliant cleanup.
{
"title": "Delete Account",
"description": "Delete user account",
"method": "DELETE",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/account",
"headers": [
{ "key": "Authorization", "value": "Bearer <token>", "required": true },
{ "key": "Content-Type", "value": "application/json", "required": true }
],
"bodyType": "json",
"requestBody": "{\n \"current_password\": \"password123\"\n}",
"responses": {
"200": {
"description": "Account deleted",
"body": "{\n \"success\": true,\n \"message\": \"Account has been deleted successfully.\",\n \"data\": null\n}"
},
"401": {
"description": "Unauthorized or invalid password",
"body": "{\n \"success\": false,\n \"message\": \"The provided password does not match your current password.\",\n \"errors\": { \"current_password\": [\"The provided password does not match your current password.\"] }\n}"
},
"500": {
"description": "Deletion failed",
"body": "{\n \"success\": false,\n \"message\": \"ACCOUNT_DELETION_FAILED\",\n \"error_code\": \"ACCOUNT_DELETION_FAILED\"\n}"
}
}
}
Password Reset Endpoints
POST /api/v1/forgot-password
Requests a password reset link.
{
"title": "Forgot Password",
"description": "Request password reset",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/forgot-password",
"headers": [
{ "key": "Content-Type", "value": "application/json", "required": true }
],
"bodyType": "json",
"requestBody": "{\n \"email\": \"[email protected]\"\n}",
"responses": {
"200": {
"description": "Link sent",
"body": "{\n \"success\": true,\n \"message\": \"Password reset link sent to your email address.\",\n \"data\": null\n}"
},
"422": {
"description": "Validation failed",
"body": "{\n \"success\": false,\n \"message\": \"Validation failed\",\n \"errors\": { \"email\": [\"We can't find a user with that email address.\"] }\n}"
}
}
}
POST /api/v1/reset-password
Resets the password using token.
{
"title": "Reset Password",
"description": "Reset password with token",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/reset-password",
"headers": [
{ "key": "Content-Type", "value": "application/json", "required": true }
],
"bodyType": "json",
"requestBody": "{\n \"email\": \"[email protected]\",\n \"token\": \"reset-token-here\",\n \"password\": \"newPass123\",\n \"password_confirmation\": \"newPass123\"\n}",
"responses": {
"200": {
"description": "Password reset successful",
"body": "{\n \"success\": true,\n \"message\": \"Password has been reset successfully.\",\n \"data\": null\n}"
},
"422": {
"description": "Validation failed",
"body": "{\n \"success\": false,\n \"message\": \"Validation failed\",\n \"errors\": { \"token\": [\"This password reset token is invalid.\"] }\n}"
}
}
}
Email Verification Endpoints
POST /api/v1/email/verification-notification
Sends an email verification link.
{
"title": "Send Verification Email",
"description": "Send email verification notification",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/email/verification-notification",
"headers": [
{ "key": "Authorization", "value": "Bearer <token>", "required": true }
],
"bodyType": "none",
"responses": {
"200": {
"description": "Link sent",
"body": "{\n \"success\": true,\n \"message\": \"Email verification link sent successfully.\",\n \"data\": null\n}"
},
"400": {
"description": "Already verified",
"body": "{\n \"success\": false,\n \"message\": \"Email is already verified.\",\n \"error_code\": \"EMAIL_ALREADY_VERIFIED\"\n}"
}
}
}
POST /api/v1/email/verify
Verifies the user’s email via signed URL.
{
"title": "Verify Email",
"description": "Verify user email",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/email/verify",
"headers": [
{ "key": "Content-Type", "value": "application/json", "required": true }
],
"bodyType": "json",
"requestBody": "{\n \"id\": 1,\n \"hash\": \"verification-hash-here\",\n \"signature\": \"signed-string\"\n}",
"responses": {
"200": {
"description": "Email verified",
"body": "{\n \"success\": true,\n \"message\": \"Email verified successfully.\",\n \"data\": { \"user\": { \"email_verified_at\": \"2025-01-01T00:00:00Z\" } }\n}"
},
"400": {
"description": "Invalid link or missing signature",
"body": "{\n \"success\": false,\n \"message\": \"Invalid verification link.\",\n \"error_code\": \"INVALID_VERIFICATION_LINK\"\n}"
}
}
}
Session Management Endpoints
GET /api/v1/user/sessions
Lists all active API sessions.
{
"title": "List Sessions",
"description": "List active user sessions",
"method": "GET",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/sessions",
"headers": [
{ "key": "Authorization", "value": "Bearer <token>", "required": true }
],
"bodyType": "none",
"responses": {
"200": {
"description": "Sessions listed",
"body": "{\n \"success\": true,\n \"message\": \"Operation successful\",\n \"data\": { \"sessions\": [...], \"total\": 3 }\n}"
}
}
}
DELETE /api/v1/user/sessions/{sessionId}
Revokes a specific session (except current).
{
"title": "Revoke Session",
"description": "Revoke a user session",
"method": "DELETE",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/sessions/123",
"headers": [
{ "key": "Authorization", "value": "Bearer <token>", "required": true }
],
"pathParams": [
{ "key": "sessionId", "value": "Session ID to revoke", "required": true }
],
"bodyType": "none",
"responses": {
"200": {
"description": "Session revoked",
"body": "{\n \"success\": true,\n \"message\": \"Session revoked successfully.\",\n \"data\": null\n}"
},
"404": {
"description": "Session not found",
"body": "{\n \"success\": false,\n \"message\": \"Session not found.\",\n \"error_code\": \"SESSION_NOT_FOUND\"\n}"
}
}
}
Two-Factor Authentication Endpoints
POST /api/v1/user/two-factor/enable
Initiates 2FA setup and returns QR code URL.
{
"title": "Enable Two-Factor",
"description": "Enable two-factor authentication",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/two-factor/enable",
"headers": [
{ "key": "Authorization", "value": "Bearer <token>", "required": true },
{ "key": "Content-Type", "value": "application/json", "required": true }
],
"bodyType": "json",
"requestBody": "{\n \"password\": \"password123\"\n}",
"responses": {
"200": {
"description": "Setup initiated",
"body": "{\n \"success\": true,\n \"message\": \"Two-factor authentication setup initiated.\",\n \"data\": { \"secret\": \"ABC123...\", \"qr_code_url\": \"https://...\", \"next_step\": \"Scan QR code...\" }\n}"
},
"400": {
"description": "Already enabled",
"body": "{\n \"success\": false,\n \"message\": \"Two-factor authentication is already enabled.\",\n \"error_code\": \"TWO_FACTOR_ALREADY_ENABLED\"\n}"
}
}
}
POST /api/v1/user/two-factor/confirm
Confirms 2FA with code and returns recovery codes.
{
"title": "Confirm Two-Factor",
"description": "Confirm two-factor authentication",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/two-factor/confirm",
"headers": [
{ "key": "Authorization", "value": "Bearer <token>", "required": true },
{ "key": "Content-Type", "value": "application/json", "required": true }
],
"bodyType": "json",
"requestBody": "{\n \"code\": \"123456\",\n \"password\": \"password123\"\n}",
"responses": {
"200": {
"description": "2FA enabled",
"body": "{\n \"success\": true,\n \"message\": \"Two-factor authentication has been enabled successfully.\",\n \"data\": { \"recovery_codes\": [\"ABCD12...\"], \"warning\": \"Save these codes.\" }\n}"
},
"422": {
"description": "Invalid code",
"body": "{\n \"success\": false,\n \"message\": \"Invalid two-factor authentication code.\",\n \"error_code\": \"INVALID_TWO_FACTOR_CODE\"\n}"
}
}
}
GET /api/v1/user/two-factor/recovery-codes
Retrieves current recovery codes.
{
"title": "Get Recovery Codes",
"description": "Get two-factor recovery codes",
"method": "GET",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/two-factor/recovery-codes",
"headers": [
{ "key": "Authorization", "value": "Bearer <token>", "required": true }
],
"bodyType": "none",
"responses": {
"200": {
"description": "Codes retrieved",
"body": "{\n \"success\": true,\n \"message\": \"Recovery codes retrieved successfully.\",\n \"data\": { \"recovery_codes\": [\"ABCD12...\"], \"warning\": \"Save these codes.\" }\n}"
},
"400": {
"description": "Not enabled",
"body": "{\n \"success\": false,\n \"message\": \"Two-factor authentication is not enabled.\",\n \"error_code\": \"TWO_FACTOR_NOT_ENABLED\"\n}"
}
}
}
POST /api/v1/user/two-factor/recovery-codes/regenerate
Regenerates and returns new recovery codes.
{
"title": "Regenerate Recovery Codes",
"description": "Regenerate two-factor recovery codes",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/two-factor/recovery-codes/regenerate",
"headers": [
{ "key": "Authorization", "value": "Bearer <token>", "required": true }
],
"bodyType": "none",
"responses": {
"200": {
"description": "Codes regenerated",
"body": "{\n \"success\": true,\n \"message\": \"Recovery codes regenerated successfully.\",\n \"data\": { \"recovery_codes\": [\"XYZ789...\"], \"warning\": \"Save these new codes.\" }\n}"
}
}
}
POST /api/v1/user/two-factor/disable
Disables two-factor authentication.
{
"title": "Disable Two-Factor",
"description": "Disable two-factor authentication",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/two-factor/disable",
"headers": [
{ "key": "Authorization", "value": "Bearer <token>", "required": true },
{ "key": "Content-Type", "value": "application/json", "required": true }
],
"bodyType": "json",
"requestBody": "{\n \"password\": \"password123\"\n}",
"responses": {
"200": {
"description": "2FA disabled",
"body": "{\n \"success\": true,\n \"message\": \"Two-factor authentication has been disabled successfully.\",\n \"data\": null\n}"
},
"400": {
"description": "Not enabled",
"body": "{\n \"success\": false,\n \"message\": \"Two-factor authentication is not enabled.\",\n \"error_code\": \"TWO_FACTOR_NOT_ENABLED\"\n}"
}
}
}
Summary
This PR delivers:
- Complete API surface for user auth, profile, password, email verification, sessions, and 2FA.
- Uniform JSON responses via
ApiResponse. - Rate limiting middleware and configuration.
- A powerful OpenAPI generator for up-to-date API specs.
🎉 All endpoints are now documented, validated, and protected by rate limits and security checks.
This documentation was automatically generated by DocuWriter.ai 🚀
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 22
♻️ Duplicate comments (20)
.github/workflows/tests.yml (1)
67-68: You're still migrating the wrong database.The past review nailed it: this migrate step hits
.env(probably MySQL) while your tests use sqlite:memory:fromphpunit.xml. You're literally prepping one DB and testing a completely different one.Drop this global migrate step and let
RefreshDatabasehandle it in your tests, or wire up DB env vars before this line to point at the same sqlite connection your tests use.app/Http/Requests/Api/Auth/DeleteAccountRequest.php (1)
15-18: Same redundant auth check as DeactivateAccountRequest.If your route already has
auth:sanctummiddleware (which Line 12 says it MUST), thisauth()->check()is redundant. The middleware already bounced unauthenticated requests.Defense-in-depth is fine if you're paranoid, but you can simplify to
return true;if you trust your route definitions.public function authorize(): bool { - return auth()->check(); + return true; }app/Console/Commands/GenerateOpenApiSpec.php (4)
109-110:$schemasis still an empty array - dead variable.Past review called this out. You declare it, never populate it, then pass the empty array to
components.schemas. Either implement schema extraction or remove it.- $schemas = []; - foreach ($routes as $route) {And at line 145:
'components' => [ - 'schemas' => $schemas, + 'schemas' => new \stdClass(), // Empty object for valid OpenAPI 'securitySchemes' => [
487-527: Still parsing min/max but throwing them away, andnumeric→integeris still wrong.Past review flagged this exact issue. Three problems persist:
$minLengthand$maxLengthare parsed but never added to the OpenAPI property$isRequiredis tracked but discarded (duplicate ofgetRequiredFieldsFromRules)numericshould map tonumber, notinteger- Laravel'snumericallows floats like3.14The static analysis even flags the unused variables.
private function convertFieldRulesToOpenApiProperty(array $rules, string $fieldName): array { $property = [ 'type' => 'string', // Default type ]; - $isRequired = false; $isEmail = false; $isPassword = false; - $minLength = null; - $maxLength = null; foreach ($rules as $rule) { if (is_string($rule)) { - if ($rule === 'required') { - $isRequired = true; - } elseif ($rule === 'email') { + if ($rule === 'email') { $isEmail = true; $property['format'] = 'email'; } elseif ($rule === 'password') { $isPassword = true; $property['format'] = 'password'; } elseif (str_starts_with($rule, 'min:')) { - $minLength = (int) substr($rule, 4); + $property['minLength'] = (int) substr($rule, 4); } elseif (str_starts_with($rule, 'max:')) { - $maxLength = (int) substr($rule, 4); - } elseif ($rule === 'integer' || $rule === 'numeric') { + $property['maxLength'] = (int) substr($rule, 4); + } elseif ($rule === 'integer') { $property['type'] = 'integer'; + } elseif ($rule === 'numeric') { + $property['type'] = 'number'; } elseif ($rule === 'boolean') { $property['type'] = 'boolean'; } } }
674-689: Dead code alert:createMockFormRequestis never called.Past review flagged this. The method ignores its
$formRequestClassparameter and always returns an anonymous class. The comment says "kept for backward compatibility" in a brand new file. That's not how backward compatibility works.Delete it.
- /** - * Create a mock FormRequest instance without triggering validation. - */ - private function createMockFormRequest(string $formRequestClass): \Illuminate\Foundation\Http\FormRequest - { - // This method is kept for backward compatibility but we now use extractRulesWithoutValidation - return new class extends \Illuminate\Foundation\Http\FormRequest - { - public function authorize(): bool - { - return true; - } - - public function rules(): array - { - return []; - } - }; - }
584-627:getRequestPropertiesandgetRequiredFieldsare 90% copy-paste.Past review flagged this. Both methods do the same dance: resolve controller → get FormRequest → reflect → extract rules. The only difference is what they return at the end. When you fix a bug in one, you'll forget the other.
Extract a shared helper that returns the rules, then call it from both.
Also applies to: 694-734
app/Http/Requests/Api/Auth/EnableTwoFactorRequest.php (1)
7-44: This is actually solid — past security concern has been addressedThe previous review flagged the missing
:sanctumguard oncurrent_password, and you fixed it (line 28). The code now properly specifiescurrent_password:sanctumwhich prevents the guard context bug where Laravel defaults to web guard instead of your Sanctum-authenticated user.The defensive
auth()->check()inauthorize()is good practice even though it's redundant with middleware — defense in depth is underrated.One tiny nit: That comment about "MUST have auth:sanctum middleware" is a smell — if it MUST have it, why not enforce it at the FormRequest level? But honestly, middleware enforcement is the right place for this, so the comment is just documentation. Ship it.
app/Http/Controllers/Api/Auth/UserController.php (1)
10-24: Previous security issue? Fixed. This is clean.You're explicitly projecting safe fields instead of yeeting the entire User model at the client. This addresses the past review concern about accidentally leaking 2FA secrets, recovery codes, or other spicy attributes.
The field selection is minimal and safe: id, name, email, email_verified_at. No hidden landmines.
Optional consideration: Users might want to know their
two_factor_enabledstatus in this response. Not a blocker, but if the frontend needs it, you'll be back here adding it later. Plan ahead or pay in PRs.storage/postman/api_collection (3)
80-84: Still missingpassword_confirmationin register payload.This was flagged in a previous review. If your
RegisterRequestvalidatespassword_confirmation(which it should for user registration), this sample payload will fail validation.
158-161: Reset password payload also missingpassword_confirmation.Same issue as register - if the endpoint requires password confirmation, the sample needs to include it.
377-380: Password update payload missingpassword_confirmation.Pattern continues. Either your validation doesn't require confirmation (questionable for password changes), or your Postman samples are incomplete.
app/Services/UserDeletionService.php (3)
35-41: Still logging PII in the "GDPR-compliant" deletion service. The irony.You fixed this in
softDeleteUserby hashing the email, butdeleteUserstill logs the raw email three times (lines 37, 81, 91). Pick one approach and be consistent. If you're deleting user data for GDPR compliance, immortalizing their email in your audit logs kind of defeats the purpose.Log::info('User account deletion initiated', [ 'user_id' => $user->id, - 'user_email' => $user->email, + 'user_email_hash' => hash('sha256', $user->email), 'reason' => $reason, 'deleted_at' => now(), 'gdpr_compliant' => true, ]);Apply the same fix to lines 81 and 91.
64-70: This Pulse cleanup is still a performance landmine.
LIKE '%user_id%'andLIKE '%user_email%'on what could be a massivepulse_entriestable? Full table scan every time. No index can save you.Worse: you could match false positives. If user ID 123 exists, this query also deletes entries containing "1234", "12345", or anything that contains "123" as a substring.
Laravel Pulse doesn't store user-identifiable data by default. If you've added custom recorders that do, add a proper
user_idcolumn with an index. Otherwise, just delete this block.- if (Schema::hasTable('pulse_entries')) { - // Pulse might store user-related metrics, clean them up - DB::table('pulse_entries') - ->where('key', 'like', '%'.$user->id.'%') - ->orWhere('key', 'like', '%'.$user->email.'%') - ->delete(); - } + // Note: Laravel Pulse doesn't store user-identifiable data by default. + // If custom Pulse recorders store user data, add a proper user_id column + // with an index rather than using LIKE pattern matching.
87-98: Logging the full stack trace is overkill for audit logs.
'trace' => $e->getTraceAsString()dumps the entire stack trace into your logs. That's a lot of noise for an audit trail. The error message should be sufficient for debugging, and you've already gotreport()or Laravel's exception handler for detailed traces.Log::error('User account deletion failed', [ 'user_id' => $user->id, - 'user_email' => $user->email, + 'user_email_hash' => hash('sha256', $user->email), 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString(), ]);If you need the trace, use
report($e)which respects your logging configuration.storage/openapi.yaml (3)
1-9: Global security and hardcoded server URL - already flagged in previous review.No global
securityfield defined, andhttp://localhostis marginally better thanhttps://dashops.testbut still useless for anyone not on your machine. Previous review already covered both of these.
186-205: MissingsessionIdpath parameter - still not fixed from previous review.The path
/api/v1/user/sessions/{sessionId}has a path parameter but no parameter definition. OpenAPI validators will choke on this, and API consumers have no idea what typesessionIdshould be.
106-125: Protected endpoints still missing 401 responses - previous review flagged this.Every endpoint with
bearerAuthshould document what happens when auth fails. Right now you only show 200 and 429. Clients integrating with your API are flying blind.app/Http/Requests/Api/Auth/UpdateProfileRequest.php (1)
27-36: Still using loose equality for ID comparisons - previous review flagged this.Lines 28 and 34 use
!=instead of!==. Type juggling with IDs is how you get bugs that only surface in production when someone's ID is"1"vs1. The fix was already suggested in the previous review.- if ($routeUserId && $routeUserId != $authenticatedUser->getKey()) { + if ($routeUserId && (string) $routeUserId !== (string) $authenticatedUser->getKey()) { return false; } // Check if user ID is provided in request body (defensive programming) $requestUserId = $this->input('user_id') ?? $this->input('id'); - if ($requestUserId && $requestUserId != $authenticatedUser->getKey()) { + if ($requestUserId && (string) $requestUserId !== (string) $authenticatedUser->getKey()) { return false; }tests/Feature/ApiTest.php (1)
972-995: Using invalid 2FA secret - previous review flagged this.
'two_factor_secret' => 'test-secret'is not a valid base32-encoded Google Authenticator secret. This test passes because you're only checking recovery code retrieval, not OTP validation. If someone copies this pattern for OTP validation tests, they'll be confused.Use a valid base32 secret like
'JBSWY3DPEHPK3PXP'for consistency.app/Http/Controllers/Api/Auth/TwoFactorController.php (1)
18-48: Returning raw TOTP secret is intentional but document the security implications.Previous review flagged this. Yes, users need the secret for "can't scan QR" flows, but this endpoint's response should never be logged. Add a comment or ensure your logging middleware excludes this response body.
The implementation is acceptable for 2FA setup flows, but document the sensitivity.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
Cache: Disabled due to Reviews > Disable Cache setting
📒 Files selected for processing (39)
.cursor/rules/laravel-boost.mdc(1 hunks).env.example(1 hunks).github/copilot-instructions.md(1 hunks).github/workflows/lint.yml(1 hunks).github/workflows/tests.yml(3 hunks).junie/guidelines.md(1 hunks)AGENTS.md(1 hunks)CLAUDE.md(1 hunks)GEMINI.md(1 hunks)app/Console/Commands/GenerateOpenApiSpec.php(1 hunks)app/Helpers/ApiResponse.php(1 hunks)app/Http/Controllers/Api/Auth/AccountController.php(1 hunks)app/Http/Controllers/Api/Auth/EmailVerificationController.php(1 hunks)app/Http/Controllers/Api/Auth/LoginController.php(1 hunks)app/Http/Controllers/Api/Auth/LogoutController.php(1 hunks)app/Http/Controllers/Api/Auth/PasswordController.php(1 hunks)app/Http/Controllers/Api/Auth/ProfileController.php(1 hunks)app/Http/Controllers/Api/Auth/RegisterController.php(1 hunks)app/Http/Controllers/Api/Auth/SessionController.php(1 hunks)app/Http/Controllers/Api/Auth/TwoFactorController.php(1 hunks)app/Http/Controllers/Api/Auth/UserController.php(1 hunks)app/Http/Controllers/Settings/ProfileController.php(3 hunks)app/Http/Middleware/ApiThrottle.php(1 hunks)app/Http/Requests/Api/Auth/DeactivateAccountRequest.php(1 hunks)app/Http/Requests/Api/Auth/DeleteAccountRequest.php(1 hunks)app/Http/Requests/Api/Auth/DisableTwoFactorRequest.php(1 hunks)app/Http/Requests/Api/Auth/EnableTwoFactorRequest.php(1 hunks)app/Http/Requests/Api/Auth/RegisterRequest.php(1 hunks)app/Http/Requests/Api/Auth/UpdatePasswordRequest.php(1 hunks)app/Http/Requests/Api/Auth/UpdateProfileRequest.php(1 hunks)app/Http/Requests/Api/Auth/VerifyEmailRequest.php(1 hunks)app/Services/UserDeletionService.php(1 hunks)app/Support/TimeFormatter.php(1 hunks)database/migrations/2025_11_30_174945_add_indexes_to_user_account_management_columns.php(1 hunks)routes/api.php(1 hunks)storage/openapi.yaml(1 hunks)storage/postman/api_collection(2 hunks)tests/Feature/ApiTest.php(1 hunks)tests/Feature/Settings/ProfileUpdateTest.php(6 hunks)
🧰 Additional context used
🧬 Code graph analysis (21)
app/Http/Requests/Api/Auth/EnableTwoFactorRequest.php (1)
app/Http/Requests/Api/Auth/DisableTwoFactorRequest.php (3)
authorize(15-18)rules(25-30)messages(35-41)
app/Http/Controllers/Api/Auth/ProfileController.php (3)
app/Helpers/ApiResponse.php (2)
ApiResponse(7-117)success(12-23)app/Http/Requests/Api/Auth/UpdateProfileRequest.php (1)
UpdateProfileRequest(8-86)app/Models/User.php (1)
sendEmailVerificationNotification(66-69)
app/Http/Requests/Api/Auth/RegisterRequest.php (4)
app/Http/Requests/Api/Auth/DisableTwoFactorRequest.php (3)
authorize(15-18)rules(25-30)messages(35-41)app/Http/Requests/Api/Auth/EnableTwoFactorRequest.php (3)
authorize(15-18)rules(25-30)messages(37-43)app/Http/Requests/Api/Auth/UpdateProfileRequest.php (3)
authorize(15-40)rules(47-66)messages(73-85)app/Http/Requests/Api/Auth/ForgotPasswordRequest.php (3)
authorize(12-15)rules(22-27)messages(34-40)
app/Http/Controllers/Api/Auth/UserController.php (1)
app/Helpers/ApiResponse.php (2)
ApiResponse(7-117)success(12-23)
app/Http/Requests/Api/Auth/DisableTwoFactorRequest.php (2)
app/Http/Requests/Api/Auth/DeleteAccountRequest.php (3)
authorize(15-18)rules(25-30)messages(35-41)app/Http/Requests/Api/Auth/EnableTwoFactorRequest.php (3)
authorize(15-18)rules(25-30)messages(37-43)
app/Support/TimeFormatter.php (1)
app/Helpers/ApiResponse.php (1)
secondsToHuman(113-116)
app/Http/Requests/Api/Auth/UpdatePasswordRequest.php (2)
app/Http/Requests/Api/Auth/DisableTwoFactorRequest.php (3)
authorize(15-18)rules(25-30)messages(35-41)app/Http/Requests/Api/Auth/UpdateProfileRequest.php (3)
authorize(15-40)rules(47-66)messages(73-85)
app/Http/Controllers/Api/Auth/EmailVerificationController.php (3)
app/Helpers/ApiResponse.php (3)
ApiResponse(7-117)error(28-46)success(12-23)app/Http/Requests/Api/Auth/VerifyEmailRequest.php (1)
VerifyEmailRequest(7-44)app/Models/User.php (2)
User(14-70)sendEmailVerificationNotification(66-69)
app/Http/Controllers/Api/Auth/RegisterController.php (4)
app/Actions/Fortify/CreateNewUser.php (2)
CreateNewUser(8-25)create(17-24)app/Helpers/ApiResponse.php (3)
ApiResponse(7-117)success(12-23)error(28-46)app/Http/Requests/Api/Auth/RegisterRequest.php (1)
RegisterRequest(7-49)app/OpenApi/Operation.php (1)
__construct(13-17)
app/Http/Controllers/Api/Auth/AccountController.php (4)
app/Helpers/ApiResponse.php (3)
ApiResponse(7-117)error(28-46)success(12-23)app/Http/Requests/Api/Auth/DeactivateAccountRequest.php (1)
DeactivateAccountRequest(7-44)app/Http/Requests/Api/Auth/DeleteAccountRequest.php (1)
DeleteAccountRequest(7-42)app/Services/UserDeletionService.php (2)
UserDeletionService(19-142)deleteUser(30-100)
app/Http/Controllers/Api/Auth/LogoutController.php (1)
app/Helpers/ApiResponse.php (3)
ApiResponse(7-117)success(12-23)error(28-46)
app/Http/Controllers/Settings/ProfileController.php (1)
app/Services/UserDeletionService.php (2)
UserDeletionService(19-142)deleteUser(30-100)
app/Http/Controllers/Api/Auth/PasswordController.php (5)
app/Helpers/ApiResponse.php (2)
ApiResponse(7-117)success(12-23)app/Http/Requests/Api/Auth/ForgotPasswordRequest.php (1)
ForgotPasswordRequest(7-41)app/Http/Requests/Api/Auth/ResetPasswordRequest.php (1)
ResetPasswordRequest(7-47)app/Http/Requests/Api/Auth/UpdatePasswordRequest.php (1)
UpdatePasswordRequest(7-50)app/Models/User.php (1)
User(14-70)
tests/Feature/ApiTest.php (2)
app/Models/User.php (1)
User(14-70)app/Actions/Fortify/CreateNewUser.php (1)
create(17-24)
app/Http/Controllers/Api/Auth/SessionController.php (1)
app/Helpers/ApiResponse.php (3)
ApiResponse(7-117)success(12-23)error(28-46)
app/Services/UserDeletionService.php (3)
app/Models/User.php (1)
User(14-70)app/Http/Controllers/Api/Auth/AccountController.php (1)
delete(69-94)app/Helpers/ApiResponse.php (1)
error(28-46)
app/Http/Middleware/ApiThrottle.php (1)
app/Helpers/ApiResponse.php (2)
ApiResponse(7-117)rateLimited(88-108)
app/Http/Controllers/Api/Auth/TwoFactorController.php (4)
app/Helpers/ApiResponse.php (3)
ApiResponse(7-117)error(28-46)success(12-23)app/Http/Requests/Api/Auth/ConfirmTwoFactorRequest.php (1)
ConfirmTwoFactorRequest(7-47)app/Http/Requests/Api/Auth/DisableTwoFactorRequest.php (1)
DisableTwoFactorRequest(7-42)app/Http/Requests/Api/Auth/EnableTwoFactorRequest.php (1)
EnableTwoFactorRequest(7-44)
app/Http/Controllers/Api/Auth/LoginController.php (3)
app/Helpers/ApiResponse.php (2)
ApiResponse(7-117)success(12-23)app/Http/Requests/Api/Auth/LoginRequest.php (1)
LoginRequest(7-45)app/Models/User.php (1)
User(14-70)
app/Http/Requests/Api/Auth/UpdateProfileRequest.php (3)
app/Http/Requests/Api/Auth/DisableTwoFactorRequest.php (2)
authorize(15-18)rules(25-30)app/Http/Requests/Api/Auth/UpdatePasswordRequest.php (2)
authorize(15-18)rules(25-31)app/Http/Requests/Api/Auth/VerifyEmailRequest.php (2)
authorize(12-15)rules(22-30)
app/Helpers/ApiResponse.php (1)
app/Support/TimeFormatter.php (2)
secondsToHuman(15-60)TimeFormatter(5-61)
🪛 Checkov (3.2.334)
storage/openapi.yaml
[high] 1-433: Ensure that the global security field has rules defined
(CKV_OPENAPI_4)
[high] 1-433: Ensure that security operations is not empty.
(CKV_OPENAPI_5)
🪛 dotenv-linter (4.0.0)
.env.example
[warning] 67-67: [ExtraBlankLine] Extra blank line detected
(ExtraBlankLine)
[warning] 75-75: [UnorderedKey] The API_THROTTLE_DECAY_MINUTES key should go before the API_THROTTLE_MAX_ATTEMPTS key
(UnorderedKey)
[warning] 81-81: [UnorderedKey] The API_THROTTLE_AUTH_DECAY_MINUTES key should go before the API_THROTTLE_AUTH_MAX_ATTEMPTS key
(UnorderedKey)
[warning] 87-87: [UnorderedKey] The API_THROTTLE_PASSWORD_RESET_DECAY_MINUTES key should go before the API_THROTTLE_PASSWORD_RESET_MAX_ATTEMPTS key
(UnorderedKey)
🪛 PHPMD (2.15.0)
app/Http/Controllers/Api/Auth/ProfileController.php
35-42: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'update'. (undefined)
(StaticAccess)
app/Http/Controllers/Api/Auth/UserController.php
16-23: Avoid using static access to class '\App\Helpers\ApiResponse' in method '__invoke'. (undefined)
(StaticAccess)
app/Support/TimeFormatter.php
15-60: The method secondsToHuman() has a Cyclomatic Complexity of 13. The configured cyclomatic complexity threshold is 10. (undefined)
(CyclomaticComplexity)
15-60: The method secondsToHuman() has an NPath complexity of 972. The configured NPath complexity threshold is 200. (undefined)
(NPathComplexity)
app/Http/Controllers/Api/Auth/EmailVerificationController.php
19-19: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'sendVerificationEmail'. (undefined)
(StaticAccess)
25-25: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'sendVerificationEmail'. (undefined)
(StaticAccess)
29-29: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'sendVerificationEmail'. (undefined)
(StaticAccess)
35-35: Avoid using static access to class '\App\Models\User' in method 'verifyEmail'. (undefined)
(StaticAccess)
38-38: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'verifyEmail'. (undefined)
(StaticAccess)
43-43: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'verifyEmail'. (undefined)
(StaticAccess)
51-51: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'verifyEmail'. (undefined)
(StaticAccess)
59-59: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'verifyEmail'. (undefined)
(StaticAccess)
72-76: Avoid using static access to class '\Illuminate\Http\Request' in method 'verifyEmail'. (undefined)
(StaticAccess)
79-79: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'verifyEmail'. (undefined)
(StaticAccess)
85-92: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'verifyEmail'. (undefined)
(StaticAccess)
app/Http/Controllers/Api/Auth/RegisterController.php
28-36: Avoid using static access to class '\App\Helpers\ApiResponse' in method '__invoke'. (undefined)
(StaticAccess)
54-54: Avoid using static access to class '\App\Helpers\ApiResponse' in method '__invoke'. (undefined)
(StaticAccess)
app/Http/Controllers/Api/Auth/AccountController.php
23-23: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'deactivate'. (undefined)
(StaticAccess)
36-36: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'deactivate'. (undefined)
(StaticAccess)
48-48: Avoid using static access to class '\Illuminate\Support\Facades\Hash' in method 'reactivate'. (undefined)
(StaticAccess)
49-51: Avoid using static access to class '\Illuminate\Validation\ValidationException' in method 'reactivate'. (undefined)
(StaticAccess)
56-56: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'reactivate'. (undefined)
(StaticAccess)
66-66: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'reactivate'. (undefined)
(StaticAccess)
85-90: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'delete'. (undefined)
(StaticAccess)
93-93: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'delete'. (undefined)
(StaticAccess)
app/Http/Controllers/Api/Auth/LogoutController.php
26-26: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logout'. (undefined)
(StaticAccess)
29-33: Avoid using static access to class '\Log' in method 'logout'. (undefined)
(StaticAccess)
35-40: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logout'. (undefined)
(StaticAccess)
42-62: The method logout uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)
(ElseExpression)
47-47: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logout'. (undefined)
(StaticAccess)
49-53: Avoid using static access to class '\Log' in method 'logout'. (undefined)
(StaticAccess)
55-60: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logout'. (undefined)
(StaticAccess)
63-71: The method logout uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)
(ElseExpression)
65-70: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logout'. (undefined)
(StaticAccess)
83-83: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logout'. (undefined)
(StaticAccess)
85-88: Avoid using static access to class '\Log' in method 'logout'. (undefined)
(StaticAccess)
90-95: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logout'. (undefined)
(StaticAccess)
100-105: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logout'. (undefined)
(StaticAccess)
112-117: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logoutFromAllDevices'. (undefined)
(StaticAccess)
124-124: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logoutFromAllDevices'. (undefined)
(StaticAccess)
126-129: Avoid using static access to class '\Log' in method 'logoutFromAllDevices'. (undefined)
(StaticAccess)
131-136: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logoutFromAllDevices'. (undefined)
(StaticAccess)
app/Http/Controllers/Settings/ProfileController.php
73-73: Avoid using static access to class '\Illuminate\Support\Facades\Auth' in method 'destroy'. (undefined)
(StaticAccess)
82-86: Avoid using static access to class '\Illuminate\Support\Facades\Log' in method 'destroy'. (undefined)
(StaticAccess)
app/Http/Controllers/Api/Auth/PasswordController.php
18-20: Avoid using static access to class '\Illuminate\Support\Facades\Password' in method 'forgotPassword'. (undefined)
(StaticAccess)
23-23: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'forgotPassword'. (undefined)
(StaticAccess)
26-28: Avoid using static access to class '\Illuminate\Validation\ValidationException' in method 'forgotPassword'. (undefined)
(StaticAccess)
33-43: Avoid using static access to class '\Illuminate\Support\Facades\Password' in method 'resetPassword'. (undefined)
(StaticAccess)
46-46: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'resetPassword'. (undefined)
(StaticAccess)
49-51: Avoid using static access to class '\Illuminate\Validation\ValidationException' in method 'resetPassword'. (undefined)
(StaticAccess)
68-71: The method updatePassword uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)
(ElseExpression)
73-73: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'updatePassword'. (undefined)
(StaticAccess)
app/Http/Controllers/Api/Auth/SessionController.php
32-35: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'index'. (undefined)
(StaticAccess)
46-46: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'destroy'. (undefined)
(StaticAccess)
51-51: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'destroy'. (undefined)
(StaticAccess)
56-56: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'destroy'. (undefined)
(StaticAccess)
app/Services/UserDeletionService.php
32-99: Avoid using static access to class '\Illuminate\Support\Facades\DB' in method 'deleteUser'. (undefined)
(StaticAccess)
35-41: Avoid using static access to class '\Illuminate\Support\Facades\Log' in method 'deleteUser'. (undefined)
(StaticAccess)
56-56: Avoid using static access to class '\Illuminate\Support\Facades\Schema' in method 'deleteUser'. (undefined)
(StaticAccess)
64-64: Avoid using static access to class '\Illuminate\Support\Facades\Schema' in method 'deleteUser'. (undefined)
(StaticAccess)
79-83: Avoid using static access to class '\Illuminate\Support\Facades\Log' in method 'deleteUser'. (undefined)
(StaticAccess)
89-94: Avoid using static access to class '\Illuminate\Support\Facades\Log' in method 'deleteUser'. (undefined)
(StaticAccess)
112-122: Avoid using static access to class '\Illuminate\Support\Facades\DB' in method 'softDeleteUser'. (undefined)
(StaticAccess)
124-128: Avoid using static access to class '\Illuminate\Support\Facades\Log' in method 'softDeleteUser'. (undefined)
(StaticAccess)
133-137: Avoid using static access to class '\Illuminate\Support\Facades\Log' in method 'softDeleteUser'. (undefined)
(StaticAccess)
app/Http/Middleware/ApiThrottle.php
25-68: The method handle() has a Cyclomatic Complexity of 10. The configured cyclomatic complexity threshold is 10. (undefined)
(CyclomaticComplexity)
32-32: Avoid excessively long variable names like $hasRouteLevelThrottle. Keep variable name length under 20. (undefined)
(LongVariable)
32-32: Avoid unused local variables such as '$hasRouteLevelThrottle'. (undefined)
(UnusedLocalVariable)
45-50: The method handle uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)
(ElseExpression)
59-64: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'handle'. (undefined)
(StaticAccess)
app/Http/Controllers/Api/Auth/TwoFactorController.php
24-24: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'enable'. (undefined)
(StaticAccess)
43-47: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'enable'. (undefined)
(StaticAccess)
58-58: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'confirm'. (undefined)
(StaticAccess)
61-61: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'confirm'. (undefined)
(StaticAccess)
69-69: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'confirm'. (undefined)
(StaticAccess)
81-84: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'confirm'. (undefined)
(StaticAccess)
93-93: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'recoveryCodes'. (undefined)
(StaticAccess)
100-100: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'recoveryCodes'. (undefined)
(StaticAccess)
104-104: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'recoveryCodes'. (undefined)
(StaticAccess)
109-109: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'recoveryCodes'. (undefined)
(StaticAccess)
112-115: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'recoveryCodes'. (undefined)
(StaticAccess)
124-124: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'regenerateRecoveryCodes'. (undefined)
(StaticAccess)
135-138: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'regenerateRecoveryCodes'. (undefined)
(StaticAccess)
147-147: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'disable'. (undefined)
(StaticAccess)
157-157: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'disable'. (undefined)
(StaticAccess)
app/Http/Controllers/Api/Auth/LoginController.php
28-28: Avoid using static access to class '\Illuminate\Support\Facades\Hash' in method '__invoke'. (undefined)
(StaticAccess)
29-31: Avoid using static access to class '\Illuminate\Validation\ValidationException' in method '__invoke'. (undefined)
(StaticAccess)
35-35: Avoid using static access to class '\Illuminate\Support\Facades\Hash' in method '__invoke'. (undefined)
(StaticAccess)
36-36: Avoid using static access to class '\Illuminate\Support\Facades\Hash' in method '__invoke'. (undefined)
(StaticAccess)
47-55: Avoid using static access to class '\App\Helpers\ApiResponse' in method '__invoke'. (undefined)
(StaticAccess)
app/Console/Commands/GenerateOpenApiSpec.php
13-743: The class GenerateOpenApiSpec has an overall complexity of 102 which is very high. The configured complexity threshold is 50. (undefined)
(ExcessiveClassComplexity)
38-38: Avoid using static access to class '\Symfony\Component\Yaml\Yaml' in method 'handle'. (undefined)
(StaticAccess)
74-74: Avoid using static access to class '\Illuminate\Support\Facades\Route' in method 'getApiRoutes'. (undefined)
(StaticAccess)
258-260: The method getSummaryFromController uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)
(ElseExpression)
413-413: Avoid using static access to class '\Illuminate\Support\Facades\Route' in method 'getControllerForUri'. (undefined)
(StaticAccess)
487-527: The method convertFieldRulesToOpenApiProperty() has a Cyclomatic Complexity of 12. The configured cyclomatic complexity threshold is 10. (undefined)
(CyclomaticComplexity)
493-493: Avoid unused local variables such as '$isRequired'. (undefined)
(UnusedLocalVariable)
496-496: Avoid unused local variables such as '$minLength'. (undefined)
(UnusedLocalVariable)
497-497: Avoid unused local variables such as '$maxLength'. (undefined)
(UnusedLocalVariable)
608-611: The method getRequestProperties uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)
(ElseExpression)
644-644: Missing class import via use statement (line '644', column '32'). (undefined)
(MissingImport)
674-689: Avoid unused private methods such as 'createMockFormRequest'. (undefined)
(UnusedPrivateMethod)
674-674: Avoid unused parameters such as '$formRequestClass'. (undefined)
(UnusedFormalParameter)
718-721: The method getRequiredFields uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)
(ElseExpression)
app/Helpers/ApiResponse.php
115-115: Avoid using static access to class '\App\Support\TimeFormatter' in method 'secondsToHuman'. (undefined)
(StaticAccess)
🪛 YAMLlint (1.37.1)
storage/openapi.yaml
[error] 26-26: too many spaces inside braces
(braces)
[error] 26-26: too many spaces inside braces
(braces)
[error] 27-27: too many spaces inside braces
(braces)
[error] 27-27: too many spaces inside braces
(braces)
[error] 28-28: too many spaces inside braces
(braces)
[error] 28-28: too many spaces inside braces
(braces)
[error] 29-29: too many spaces inside braces
(braces)
[error] 29-29: too many spaces inside braces
(braces)
[error] 29-29: too many spaces inside braces
(braces)
[error] 29-29: too many spaces inside braces
(braces)
[error] 29-29: too many spaces inside braces
(braces)
[error] 29-29: too many spaces inside braces
(braces)
[error] 29-29: too many spaces inside braces
(braces)
[error] 29-29: too many spaces inside braces
(braces)
[error] 47-47: too many spaces inside braces
(braces)
[error] 47-47: too many spaces inside braces
(braces)
[error] 48-48: too many spaces inside braces
(braces)
[error] 48-48: too many spaces inside braces
(braces)
[error] 49-49: too many spaces inside braces
(braces)
[error] 49-49: too many spaces inside braces
(braces)
[error] 50-50: too many spaces inside braces
(braces)
[error] 50-50: too many spaces inside braces
(braces)
[error] 50-50: too many spaces inside braces
(braces)
[error] 50-50: too many spaces inside braces
(braces)
[error] 50-50: too many spaces inside braces
(braces)
[error] 50-50: too many spaces inside braces
(braces)
[error] 50-50: too many spaces inside braces
(braces)
[error] 50-50: too many spaces inside braces
(braces)
[error] 66-66: too many spaces inside braces
(braces)
[error] 66-66: too many spaces inside braces
(braces)
[error] 67-67: too many spaces inside braces
(braces)
[error] 67-67: too many spaces inside braces
(braces)
[error] 68-68: too many spaces inside braces
(braces)
[error] 68-68: too many spaces inside braces
(braces)
[error] 69-69: too many spaces inside braces
(braces)
[error] 69-69: too many spaces inside braces
(braces)
[error] 69-69: too many spaces inside braces
(braces)
[error] 69-69: too many spaces inside braces
(braces)
[error] 69-69: too many spaces inside braces
(braces)
[error] 69-69: too many spaces inside braces
(braces)
[error] 69-69: too many spaces inside braces
(braces)
[error] 69-69: too many spaces inside braces
(braces)
[error] 85-85: too many spaces inside braces
(braces)
[error] 85-85: too many spaces inside braces
(braces)
[error] 86-86: too many spaces inside braces
(braces)
[error] 86-86: too many spaces inside braces
(braces)
[error] 87-87: too many spaces inside braces
(braces)
[error] 87-87: too many spaces inside braces
(braces)
[error] 88-88: too many spaces inside braces
(braces)
[error] 88-88: too many spaces inside braces
(braces)
[error] 88-88: too many spaces inside braces
(braces)
[error] 88-88: too many spaces inside braces
(braces)
[error] 88-88: too many spaces inside braces
(braces)
[error] 88-88: too many spaces inside braces
(braces)
[error] 88-88: too many spaces inside braces
(braces)
[error] 88-88: too many spaces inside braces
(braces)
[error] 102-102: too many spaces inside braces
(braces)
[error] 102-102: too many spaces inside braces
(braces)
[error] 103-103: too many spaces inside braces
(braces)
[error] 103-103: too many spaces inside braces
(braces)
[error] 104-104: too many spaces inside braces
(braces)
[error] 104-104: too many spaces inside braces
(braces)
[error] 105-105: too many spaces inside braces
(braces)
[error] 105-105: too many spaces inside braces
(braces)
[error] 105-105: too many spaces inside braces
(braces)
[error] 105-105: too many spaces inside braces
(braces)
[error] 105-105: too many spaces inside braces
(braces)
[error] 105-105: too many spaces inside braces
(braces)
[error] 105-105: too many spaces inside braces
(braces)
[error] 105-105: too many spaces inside braces
(braces)
[error] 119-119: too many spaces inside braces
(braces)
[error] 119-119: too many spaces inside braces
(braces)
[error] 120-120: too many spaces inside braces
(braces)
[error] 120-120: too many spaces inside braces
(braces)
[error] 121-121: too many spaces inside braces
(braces)
[error] 121-121: too many spaces inside braces
(braces)
[error] 122-122: too many spaces inside braces
(braces)
[error] 122-122: too many spaces inside braces
(braces)
[error] 122-122: too many spaces inside braces
(braces)
[error] 122-122: too many spaces inside braces
(braces)
[error] 122-122: too many spaces inside braces
(braces)
[error] 122-122: too many spaces inside braces
(braces)
[error] 122-122: too many spaces inside braces
(braces)
[error] 122-122: too many spaces inside braces
(braces)
[error] 125-125: too many spaces inside empty braces
(braces)
[error] 139-139: too many spaces inside braces
(braces)
[error] 139-139: too many spaces inside braces
(braces)
[error] 140-140: too many spaces inside braces
(braces)
[error] 140-140: too many spaces inside braces
(braces)
[error] 141-141: too many spaces inside braces
(braces)
[error] 141-141: too many spaces inside braces
(braces)
[error] 142-142: too many spaces inside braces
(braces)
[error] 142-142: too many spaces inside braces
(braces)
[error] 142-142: too many spaces inside braces
(braces)
[error] 142-142: too many spaces inside braces
(braces)
[error] 142-142: too many spaces inside braces
(braces)
[error] 142-142: too many spaces inside braces
(braces)
[error] 142-142: too many spaces inside braces
(braces)
[error] 142-142: too many spaces inside braces
(braces)
[error] 145-145: too many spaces inside empty braces
(braces)
[error] 159-159: too many spaces inside braces
(braces)
[error] 159-159: too many spaces inside braces
(braces)
[error] 160-160: too many spaces inside braces
(braces)
[error] 160-160: too many spaces inside braces
(braces)
[error] 161-161: too many spaces inside braces
(braces)
[error] 161-161: too many spaces inside braces
(braces)
[error] 162-162: too many spaces inside braces
(braces)
[error] 162-162: too many spaces inside braces
(braces)
[error] 162-162: too many spaces inside braces
(braces)
[error] 162-162: too many spaces inside braces
(braces)
[error] 162-162: too many spaces inside braces
(braces)
[error] 162-162: too many spaces inside braces
(braces)
[error] 162-162: too many spaces inside braces
(braces)
[error] 162-162: too many spaces inside braces
(braces)
[error] 165-165: too many spaces inside empty braces
(braces)
[error] 179-179: too many spaces inside braces
(braces)
[error] 179-179: too many spaces inside braces
(braces)
[error] 180-180: too many spaces inside braces
(braces)
[error] 180-180: too many spaces inside braces
(braces)
[error] 181-181: too many spaces inside braces
(braces)
[error] 181-181: too many spaces inside braces
(braces)
[error] 182-182: too many spaces inside braces
(braces)
[error] 182-182: too many spaces inside braces
(braces)
[error] 182-182: too many spaces inside braces
(braces)
[error] 182-182: too many spaces inside braces
(braces)
[error] 182-182: too many spaces inside braces
(braces)
[error] 182-182: too many spaces inside braces
(braces)
[error] 182-182: too many spaces inside braces
(braces)
[error] 182-182: too many spaces inside braces
(braces)
[error] 185-185: too many spaces inside empty braces
(braces)
[error] 199-199: too many spaces inside braces
(braces)
[error] 199-199: too many spaces inside braces
(braces)
[error] 200-200: too many spaces inside braces
(braces)
[error] 200-200: too many spaces inside braces
(braces)
[error] 201-201: too many spaces inside braces
(braces)
[error] 201-201: too many spaces inside braces
(braces)
[error] 202-202: too many spaces inside braces
(braces)
[error] 202-202: too many spaces inside braces
(braces)
[error] 202-202: too many spaces inside braces
(braces)
[error] 202-202: too many spaces inside braces
(braces)
[error] 202-202: too many spaces inside braces
(braces)
[error] 202-202: too many spaces inside braces
(braces)
[error] 202-202: too many spaces inside braces
(braces)
[error] 202-202: too many spaces inside braces
(braces)
[error] 205-205: too many spaces inside empty braces
(braces)
[error] 219-219: too many spaces inside braces
(braces)
[error] 219-219: too many spaces inside braces
(braces)
[error] 220-220: too many spaces inside braces
(braces)
[error] 220-220: too many spaces inside braces
(braces)
[error] 221-221: too many spaces inside braces
(braces)
[error] 221-221: too many spaces inside braces
(braces)
[error] 222-222: too many spaces inside braces
(braces)
[error] 222-222: too many spaces inside braces
(braces)
[error] 222-222: too many spaces inside braces
(braces)
[error] 222-222: too many spaces inside braces
(braces)
[error] 222-222: too many spaces inside braces
(braces)
[error] 222-222: too many spaces inside braces
(braces)
[error] 222-222: too many spaces inside braces
(braces)
[error] 222-222: too many spaces inside braces
(braces)
[error] 225-225: too many spaces inside empty braces
(braces)
[error] 239-239: too many spaces inside braces
(braces)
[error] 239-239: too many spaces inside braces
(braces)
[error] 240-240: too many spaces inside braces
(braces)
[error] 240-240: too many spaces inside braces
(braces)
[error] 241-241: too many spaces inside braces
(braces)
[error] 241-241: too many spaces inside braces
(braces)
[error] 242-242: too many spaces inside braces
(braces)
[error] 242-242: too many spaces inside braces
(braces)
[error] 242-242: too many spaces inside braces
(braces)
[error] 242-242: too many spaces inside braces
(braces)
[error] 242-242: too many spaces inside braces
(braces)
[error] 242-242: too many spaces inside braces
(braces)
[error] 242-242: too many spaces inside braces
(braces)
[error] 242-242: too many spaces inside braces
(braces)
[error] 245-245: too many spaces inside empty braces
(braces)
[error] 259-259: too many spaces inside braces
(braces)
[error] 259-259: too many spaces inside braces
(braces)
[error] 260-260: too many spaces inside braces
(braces)
[error] 260-260: too many spaces inside braces
(braces)
[error] 261-261: too many spaces inside braces
(braces)
[error] 261-261: too many spaces inside braces
(braces)
[error] 262-262: too many spaces inside braces
(braces)
[error] 262-262: too many spaces inside braces
(braces)
[error] 262-262: too many spaces inside braces
(braces)
[error] 262-262: too many spaces inside braces
(braces)
[error] 262-262: too many spaces inside braces
(braces)
[error] 262-262: too many spaces inside braces
(braces)
[error] 262-262: too many spaces inside braces
(braces)
[error] 262-262: too many spaces inside braces
(braces)
[error] 265-265: too many spaces inside empty braces
(braces)
[error] 279-279: too many spaces inside braces
(braces)
[error] 279-279: too many spaces inside braces
(braces)
[error] 280-280: too many spaces inside braces
(braces)
[error] 280-280: too many spaces inside braces
(braces)
[error] 281-281: too many spaces inside braces
(braces)
[error] 281-281: too many spaces inside braces
(braces)
[error] 282-282: too many spaces inside braces
(braces)
[error] 282-282: too many spaces inside braces
(braces)
[error] 282-282: too many spaces inside braces
(braces)
[error] 282-282: too many spaces inside braces
(braces)
[error] 282-282: too many spaces inside braces
(braces)
[error] 282-282: too many spaces inside braces
(braces)
[error] 282-282: too many spaces inside braces
(braces)
[error] 282-282: too many spaces inside braces
(braces)
[error] 285-285: too many spaces inside empty braces
(braces)
[error] 299-299: too many spaces inside braces
(braces)
[error] 299-299: too many spaces inside braces
(braces)
[error] 300-300: too many spaces inside braces
(braces)
[error] 300-300: too many spaces inside braces
(braces)
[error] 301-301: too many spaces inside braces
(braces)
[error] 301-301: too many spaces inside braces
(braces)
[error] 302-302: too many spaces inside braces
(braces)
[error] 302-302: too many spaces inside braces
(braces)
[error] 302-302: too many spaces inside braces
(braces)
[error] 302-302: too many spaces inside braces
(braces)
[error] 302-302: too many spaces inside braces
(braces)
[error] 302-302: too many spaces inside braces
(braces)
[error] 302-302: too many spaces inside braces
(braces)
[error] 302-302: too many spaces inside braces
(braces)
[error] 305-305: too many spaces inside empty braces
(braces)
[error] 319-319: too many spaces inside braces
(braces)
[error] 319-319: too many spaces inside braces
(braces)
[error] 320-320: too many spaces inside braces
(braces)
[error] 320-320: too many spaces inside braces
(braces)
[error] 321-321: too many spaces inside braces
(braces)
[error] 321-321: too many spaces inside braces
(braces)
[error] 322-322: too many spaces inside braces
(braces)
[error] 322-322: too many spaces inside braces
(braces)
[error] 322-322: too many spaces inside braces
(braces)
[error] 322-322: too many spaces inside braces
(braces)
[error] 322-322: too many spaces inside braces
(braces)
[error] 322-322: too many spaces inside braces
(braces)
[error] 322-322: too many spaces inside braces
(braces)
[error] 322-322: too many spaces inside braces
(braces)
[error] 325-325: too many spaces inside empty braces
(braces)
[error] 338-338: too many spaces inside braces
(braces)
[error] 338-338: too many spaces inside braces
(braces)
[error] 339-339: too many spaces inside braces
(braces)
[error] 339-339: too many spaces inside braces
(braces)
[error] 340-340: too many spaces inside braces
(braces)
[error] 340-340: too many spaces inside braces
(braces)
[error] 341-341: too many spaces inside braces
(braces)
[error] 341-341: too many spaces inside braces
(braces)
[error] 341-341: too many spaces inside braces
(braces)
[error] 341-341: too many spaces inside braces
(braces)
[error] 341-341: too many spaces inside braces
(braces)
[error] 341-341: too many spaces inside braces
(braces)
[error] 341-341: too many spaces inside braces
(braces)
[error] 341-341: too many spaces inside braces
(braces)
[error] 344-344: too many spaces inside empty braces
(braces)
[error] 358-358: too many spaces inside braces
(braces)
[error] 358-358: too many spaces inside braces
(braces)
[error] 359-359: too many spaces inside braces
(braces)
[error] 359-359: too many spaces inside braces
(braces)
[error] 360-360: too many spaces inside braces
(braces)
[error] 360-360: too many spaces inside braces
(braces)
[error] 361-361: too many spaces inside braces
(braces)
[error] 361-361: too many spaces inside braces
(braces)
[error] 361-361: too many spaces inside braces
(braces)
[error] 361-361: too many spaces inside braces
(braces)
[error] 361-361: too many spaces inside braces
(braces)
[error] 361-361: too many spaces inside braces
(braces)
[error] 361-361: too many spaces inside braces
(braces)
[error] 361-361: too many spaces inside braces
(braces)
[error] 364-364: too many spaces inside empty braces
(braces)
[error] 378-378: too many spaces inside braces
(braces)
[error] 378-378: too many spaces inside braces
(braces)
[error] 379-379: too many spaces inside braces
(braces)
[error] 379-379: too many spaces inside braces
(braces)
[error] 380-380: too many spaces inside braces
(braces)
[error] 380-380: too many spaces inside braces
(braces)
[error] 381-381: too many spaces inside braces
(braces)
[error] 381-381: too many spaces inside braces
(braces)
[error] 381-381: too many spaces inside braces
(braces)
[error] 381-381: too many spaces inside braces
(braces)
[error] 381-381: too many spaces inside braces
(braces)
[error] 381-381: too many spaces inside braces
(braces)
[error] 381-381: too many spaces inside braces
(braces)
[error] 381-381: too many spaces inside braces
(braces)
[error] 384-384: too many spaces inside empty braces
(braces)
[error] 398-398: too many spaces inside braces
(braces)
[error] 398-398: too many spaces inside braces
(braces)
[error] 399-399: too many spaces inside braces
(braces)
[error] 399-399: too many spaces inside braces
(braces)
[error] 400-400: too many spaces inside braces
(braces)
[error] 400-400: too many spaces inside braces
(braces)
[error] 401-401: too many spaces inside braces
(braces)
[error] 401-401: too many spaces inside braces
(braces)
[error] 401-401: too many spaces inside braces
(braces)
[error] 401-401: too many spaces inside braces
(braces)
[error] 401-401: too many spaces inside braces
(braces)
[error] 401-401: too many spaces inside braces
(braces)
[error] 401-401: too many spaces inside braces
(braces)
[error] 401-401: too many spaces inside braces
(braces)
[error] 404-404: too many spaces inside empty braces
(braces)
[error] 418-418: too many spaces inside braces
(braces)
[error] 418-418: too many spaces inside braces
(braces)
[error] 419-419: too many spaces inside braces
(braces)
[error] 419-419: too many spaces inside braces
(braces)
[error] 420-420: too many spaces inside braces
(braces)
[error] 420-420: too many spaces inside braces
(braces)
[error] 421-421: too many spaces inside braces
(braces)
[error] 421-421: too many spaces inside braces
(braces)
[error] 421-421: too many spaces inside braces
(braces)
[error] 421-421: too many spaces inside braces
(braces)
[error] 421-421: too many spaces inside braces
(braces)
[error] 421-421: too many spaces inside braces
(braces)
[error] 421-421: too many spaces inside braces
(braces)
[error] 421-421: too many spaces inside braces
(braces)
[error] 424-424: too many spaces inside empty braces
(braces)
[error] 426-426: too many spaces inside empty braces
(braces)
|
|
||
| // Note: We cannot verify database deletion here because RefreshDatabase | ||
| // rolls back all changes. The UserDeletionService logs confirm deletion occurs. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bad: You punted on actually testing deletion.
"We cannot verify database deletion because RefreshDatabase" is a cop-out. If you're using SoftDeletes (which you are, based on the User model changes), you should be able to ->assertSoftDeleted($user) right here.
The whole point of soft deletes is they survive transaction rollbacks because they're just a flag flip. Saying "trust the logs" is not testing - it's hoping.
Apply this diff to actually test the deletion:
$this->assertGuest();
-
- // Note: We cannot verify database deletion here because RefreshDatabase
- // rolls back all changes. The UserDeletionService logs confirm deletion occurs.
+ $this->assertSoftDeleted($user);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Note: We cannot verify database deletion here because RefreshDatabase | |
| // rolls back all changes. The UserDeletionService logs confirm deletion occurs. | |
| $this->assertGuest(); | |
| $this->assertSoftDeleted($user); |
🤖 Prompt for AI Agents
In tests/Feature/Settings/ProfileUpdateTest.php around lines 67-69, the test
currently skips asserting deletion citing RefreshDatabase; instead, since the
User model uses SoftDeletes, replace the comment with an actual assertion that
the user was soft-deleted: call the deletion flow in the test, then use
$this->assertSoftDeleted($user) (or assertSoftDeleted('users', ['id' =>
$user->id'])) to verify the deleted_at flag was set; ensure $user is the same
persisted record (refresh or reload if needed) and keep RefreshDatabase—soft
deletes are visible within the transaction so no further DB setup is required.
## 📚 Automated Documentation Summary
|
| Variable | Default | Description |
|---|---|---|
API_THROTTLE_MAX_ATTEMPTS |
100 | Max requests per minute for general endpoints |
API_THROTTLE_DECAY_MINUTES |
1 | Reset window for general endpoints (minutes) |
API_THROTTLE_AUTH_MAX_ATTEMPTS |
5 | Max auth attempts per minute (login/register) |
API_THROTTLE_AUTH_DECAY_MINUTES |
1 | Reset auth window (minutes) |
API_THROTTLE_PASSWORD_RESET_MAX_ATTEMPTS |
3 | Max password reset requests per period |
API_THROTTLE_PASSWORD_RESET_DECAY_MINUTES |
15 | Reset window for password resets (minutes) |
# .env.example excerpt
API_THROTTLE_MAX_ATTEMPTS=100
API_THROTTLE_DECAY_MINUTES=1
API_THROTTLE_AUTH_MAX_ATTEMPTS=5
API_THROTTLE_AUTH_DECAY_MINUTES=1
API_THROTTLE_PASSWORD_RESET_MAX_ATTEMPTS=3
API_THROTTLE_PASSWORD_RESET_DECAY_MINUTES=15Dependency List Updates
Several project docs (.junie/guidelines.md, AGENTS.md, CLAUDE.md, GEMINI.md) now include:
- rector/rector (RECTOR) - v2
Action: CreateNewUser Simplification
Removed explicit validation in app/Actions/Fortify/CreateNewUser.php. Validation now lives in FormRequest classes.
// Before: explicit Validator::make(...)->validate();
// After: directly create User from validated input
return User::create([
'name' => $input['name'],
'email' => $input['email'],
'password' => Hash::make($input['password']),
]);Console Command: GenerateOpenApiSpec 📄
A new Artisan command openapi:generate inspects all api.v1.* routes and outputs an OpenAPI 3.0.3 YAML file.
- Discovers routes by name, middleware, or URI.
- Reflects controller docblocks or custom
#[Operation]attributes for summaries. - Extracts request schemas from
FormRequestvalidation rules. - Builds
paths,components.schemas, andsecuritySchemes.bearerAuth. - Usage:
php artisan openapi:generate --output=storage/openapi.yaml
- Fails with clear error messages on I/O issues.
Helper: ApiResponse 💡
Located in app/Helpers/ApiResponse.php, this class standardizes JSON responses:
- success(data, message, statusCode)
- error(message, errors, statusCode, errorCode)
- validationError(errors)
- unauthorized(), forbidden(), notFound(), rateLimited()
Example:
return ApiResponse::success(
['user' => $user],
'Login successful',
200
);Middleware: ApiThrottle 🐢
New app/Http/Middleware/ApiThrottle.php extends Laravel’s ThrottleRequests:
- Applies different rate limits based on route name:
- Auth endpoints: uses
api.throttle.auth.*config. - Password reset: uses
api.throttle.password_reset.*. - General API: uses
api.throttle.default.*.
- Auth endpoints: uses
- Returns standardized 429 JSON via
ApiResponse::rateLimited().
API Endpoints
Base URL: https://api.example.com
All endpoints are under /api/v1. Each block below documents a single endpoint with examples.
POST /api/v1/user/deactivate
Deactivate authenticated user account.
{
"title": "Deactivate Account",
"description": "Deactivate current user account and revoke all tokens.",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/deactivate",
"headers": [
{ "key": "Authorization", "value": "Bearer <token>", "required": true }
],
"bodyType": "json",
"requestBody": "{\n \"current_password\": \"password123\",\n \"reason\": \"No longer needed\"\n}",
"responses": {
"200": {
"description": "Account deactivated successfully",
"body": "{ \"success\": true, \"message\": \"Account has been deactivated successfully. All sessions have been terminated.\", \"data\": null, \"errors\": null }"
},
"400": {
"description": "Bad request or already deactivated",
"body": "{ \"success\": false, \"message\": \"Account is already deactivated.\", \"error_code\": \"ACCOUNT_ALREADY_DEACTIVATED\" }"
},
"401": {
"description": "Unauthorized",
"body": "{ \"success\": false, \"message\": \"Unauthorized access\", \"error_code\": \"UNAUTHORIZED\" }"
}
}
}
POST /api/v1/user/reactivate
Reactivate a deactivated account with current password.
{
"title": "Reactivate Account",
"description": "Reactivate user account after deactivation.",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/reactivate",
"headers": [
{ "key": "Authorization", "value": "Bearer <token>", "required": true }
],
"bodyType": "json",
"requestBody": "{\n \"current_password\": \"password123\"\n}",
"responses": {
"200": {
"description": "Reactivation successful",
"body": "{ \"success\": true, \"message\": \"Account has been reactivated successfully.\", \"data\": null, \"errors\": null }"
},
"400": {
"description": "Invalid password or already active",
"body": "{ \"success\": false, \"message\": \"Account is already active.\", \"error_code\": \"ACCOUNT_ALREADY_ACTIVE\" }"
},
"422": {
"description": "Validation failed",
"body": "{ \"success\": false, \"message\": \"Validation failed\", \"errors\": { \"current_password\": [ ... ] }, \"error_code\": \"VALIDATION_ERROR\" }"
}
}
}
DELETE /api/v1/user/account
Delete authenticated user account GDPR-compliantly.
{
"title": "Delete Account",
"description": "Permanently delete user account with cleanup.",
"method": "DELETE",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/account",
"headers": [
{ "key": "Authorization", "value": "Bearer <token>", "required": true }
],
"bodyType": "json",
"requestBody": "{\n \"current_password\": \"password123\"\n}",
"responses": {
"200": {
"description": "Account deleted successfully",
"body": "{ \"success\": true, \"message\": \"Account has been deleted successfully.\", \"data\": null, \"errors\": null }"
},
"400": {
"description": "Bad request",
"body": "{ \"success\": false, \"message\": \"The provided password does not match your current password.\", \"error_code\": \"VALIDATION_ERROR\" }"
},
"500": {
"description": "Server error",
"body": "{ \"success\": false, \"message\": \"ACCOUNT_DELETION_FAILED\", \"error_code\": \"ACCOUNT_DELETION_FAILED\" }"
}
}
}
POST /api/v1/email/verification-notification
Send email verification link.
{
"title": "Send Verification Email",
"description": "Send verification link to user email.",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/email/verification-notification",
"headers": [
{ "key": "Authorization", "value": "Bearer <token>", "required": true }
],
"bodyType": "none",
"responses": {
"200": {
"description": "Link sent",
"body": "{ \"success\": true, \"message\": \"Email verification link sent successfully.\", \"data\": null, \"errors\": null }"
},
"400": {
"description": "Already verified",
"body": "{ \"success\": false, \"message\": \"EMAIL_ALREADY_VERIFIED\", \"error_code\": \"EMAIL_ALREADY_VERIFIED\" }"
}
}
}
GET /api/v1/email/verify
Verify user email via signed link.
{
"title": "Verify Email",
"description": "Verify user email from link query params.",
"method": "GET",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/email/verify?id={id}&hash={hash}&signature={sig}",
"pathParams": [
{ "key": "id", "value": "User ID", "required": true },
{ "key": "hash", "value": "Verification hash", "required": true }
],
"headers": [],
"bodyType": "none",
"responses": {
"200": {
"description": "Email verified",
"body": "{ \"success\": true, \"message\": \"Email verified successfully.\", \"data\": { \"user\": { ... } } }"
},
"400": {
"description": "Invalid or expired link",
"body": "{ \"success\": false, \"message\": \"EXPIRED_VERIFICATION_LINK\", \"error_code\": \"EXPIRED_VERIFICATION_LINK\" }"
},
"404": {
"description": "User not found",
"body": "{ \"success\": false, \"message\": \"USER_NOT_FOUND\", \"error_code\": \"USER_NOT_FOUND\" }"
}
}
}
POST /api/v1/login
User authentication.
{
"title": "User Login",
"description": "Authenticate user and return token.",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/login",
"bodyType": "json",
"requestBody": "{\n \"email\": \"[email protected]\",\n \"password\": \"password123\",\n \"device\": \"web\"\n}",
"responses": {
"200": {
"description": "Login successful",
"body": "{ \"success\": true, \"message\": \"Login successful\", \"data\": { \"user\": { ... }, \"token\": \"<token>\", \"token_type\": \"Bearer\" } }"
},
"422": {
"description": "Invalid credentials",
"body": "{ \"success\": false, \"message\": \"Validation failed\", \"errors\": {\"email\": [\"The provided credentials are incorrect.\"]}, \"error_code\":\"VALIDATION_ERROR\" }"
}
}
}
POST /api/v1/logout
Revoke current token or session.
{
"title": "Logout",
"description": "Logout from current device (token or session).",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/logout",
"headers": [
{ "key": "Authorization", "value": "Bearer <token>", "required": true }
],
"bodyType": "none",
"responses": {
"200": {
"description": "Logout successful",
"body": "{ \"success\": true, \"message\": \"Successfully logged out from this device.\", \"data\": null }"
},
"401": {
"description": "No authentication found",
"body": "{ \"success\": false, \"message\": \"NO_AUTHENTICATION\", \"error_code\": \"NO_AUTHENTICATION\" }"
}
}
}
POST /api/v1/logout-all
Revoke all tokens (token‐only).
{
"title": "Logout All Devices",
"description": "Revoke all Sanctum tokens for user.",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/logout-all",
"headers": [
{ "key": "Authorization", "value": "Bearer <token>", "required": true }
],
"bodyType": "none",
"responses": {
"200": {
"description": "All sessions revoked",
"body": "{ \"success\": true, \"message\": \"Successfully logged out from all devices.\", \"data\": null }"
},
"400": {
"description": "Session‐based auth not supported",
"body": "{ \"success\": false, \"message\": \"TOKEN_AUTH_REQUIRED\", \"error_code\": \"TOKEN_AUTH_REQUIRED\" }"
}
}
}
POST /api/v1/forgot-password
Request password reset link.
{
"title": "Forgot Password",
"description": "Send password reset link via email.",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/forgot-password",
"bodyType": "json",
"requestBody": "{\n \"email\": \"[email protected]\"\n}",
"responses": {
"200": {
"description": "Link sent",
"body": "{ \"success\": true, \"message\": \"Password reset link sent to your email address.\", \"data\": null }"
},
"422": {
"description": "Validation failed",
"body": "{ \"success\": false, \"message\": \"Validation failed\", \"errors\": {\"email\":[...]} }"
}
}
}
POST /api/v1/reset-password
Reset password with token.
{
"title": "Reset Password",
"description": "Reset user password using token.",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/reset-password",
"bodyType": "json",
"requestBody": "{\n \"email\": \"[email protected]\",\n \"token\": \"reset-token-here\",\n \"password\": \"newPass123\",\n \"password_confirmation\": \"newPass123\"\n}",
"responses": {
"200": {
"description": "Password reset successful",
"body": "{ \"success\": true, \"message\": \"Password has been reset successfully.\", \"data\": null }"
},
"422": {
"description": "Validation failed",
"body": "{ \"success\": false, \"message\": \"Validation failed\", \"errors\": { ... } }"
}
}
}
PUT /api/v1/user/password
Update password when authenticated.
{
"title": "Update Password",
"description": "Change password for authenticated user.",
"method": "PUT",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/password",
"headers": [
{ "key": "Authorization", "value": "Bearer <token>", "required": true }
],
"bodyType": "json",
"requestBody": "{\n \"current_password\": \"oldPass123\",\n \"password\": \"newPass123\",\n \"password_confirmation\": \"newPass123\"\n}",
"responses": {
"200": {
"description": "Password updated",
"body": "{ \"success\": true, \"message\": \"Password updated successfully.\", \"data\": null }"
},
"422": {
"description": "Validation failed",
"body": "{ \"success\": false, \"message\": \"Validation failed\", \"errors\": { ... } }"
}
}
}
PUT /api/v1/user/profile
Update user profile.
{
"title": "Update Profile",
"description": "Update name or email for authenticated user.",
"method": "PUT",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/profile",
"headers": [
{ "key": "Authorization", "value": "Bearer <token>", "required": true }
],
"bodyType": "json",
"requestBody": "{\n \"name\": \"John Doe\",\n \"email\": \"[email protected]\"\n}",
"responses": {
"200": {
"description": "Profile updated",
"body": "{ \"success\": true, \"message\": \"Profile updated successfully.\", \"data\": { \"user\": { ... } } }"
},
"422": {
"description": "Validation failed",
"body": "{ \"success\": false, \"message\": \"Validation failed\", \"errors\": { ... } }"
}
}
}
POST /api/v1/register
Create new user and return token.
{
"title": "User Registration",
"description": "Register new user and issue token.",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/register",
"bodyType": "json",
"requestBody": "{\n \"name\": \"John Doe\",\n \"email\": \"[email protected]\",\n \"password\": \"password123\",\n \"password_confirmation\": \"password123\"\n}",
"responses": {
"201": {
"description": "Registration successful",
"body": "{ \"success\": true, \"message\": \"User registered successfully\", \"data\": { \"user\": {...}, \"token\": \"<token>\", \"token_type\":\"Bearer\" } }"
},
"422": {
"description": "Validation failed",
"body": "{ \"success\": false, \"message\": \"Validation failed\", \"errors\": {...} }"
},
"500": {
"description": "Server error",
"body": "{ \"success\": false, \"message\": \"Registration failed\", \"error_code\":\"REGISTRATION_ERROR\" }"
}
}
}
GET /api/v1/user/sessions
List active sessions (tokens).
{
"title": "List Sessions",
"description": "Retrieve all active API tokens for user.",
"method": "GET",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/sessions",
"headers": [
{ "key": "Authorization", "value": "Bearer <token>", "required": true }
],
"bodyType": "none",
"responses": {
"200": {
"description": "Sessions listed",
"body": "{ \"success\": true, \"message\": \"\", \"data\": { \"sessions\": [...], \"total\": 3 } }"
}
}
}
DELETE /api/v1/user/sessions/{sessionId}
Revoke a specific session.
{
"title": "Revoke Session",
"description": "Delete a specific API token.",
"method": "DELETE",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/sessions/{sessionId}",
"pathParams": [
{ "key": "sessionId", "value": "Token ID", "required": true }
],
"headers": [
{ "key": "Authorization", "value": "Bearer <token>", "required": true }
],
"bodyType": "none",
"responses": {
"200": {
"description": "Session revoked",
"body": "{ \"success\": true, \"message\": \"Session revoked successfully.\", \"data\": null }"
},
"404": {
"description": "Session not found",
"body": "{ \"success\": false, \"message\": \"SESSION_NOT_FOUND\", \"error_code\":\"SESSION_NOT_FOUND\" }"
}
}
}
POST /api/v1/user/two-factor/enable
Initiate 2FA setup.
{
"title": "Enable Two-Factor",
"description": "Start two-factor authentication setup.",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/two-factor/enable",
"headers": [
{ "key": "Authorization", "value": "Bearer <token>", "required": true }
],
"bodyType": "json",
"requestBody": "{\n \"current_password\": \"password123\"\n}",
"responses": {
"200": {
"description": "Setup initiated",
"body": "{ \"success\": true, \"message\": \"Two-factor authentication setup initiated.\", \"data\": { \"secret\":\"...\",\"qr_code_url\":\"...\",\"next_step\":\"...\" } }"
},
"400": {
"description": "Already enabled",
"body": "{ \"success\": false, \"message\": \"TWO_FACTOR_ALREADY_ENABLED\", \"error_code\":\"TWO_FACTOR_ALREADY_ENABLED\" }"
}
}
}
POST /api/v1/user/two-factor/confirm
Confirm 2FA code.
{
"title": "Confirm Two-Factor",
"description": "Verify 2FA code and enable.",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/two-factor/confirm",
"headers": [
{ "key": "Authorization", "value": "Bearer <token>", "required": true }
],
"bodyType": "json",
"requestBody": "{\n \"code\": \"123456\",\n \"current_password\": \"password123\"\n}",
"responses": {
"200": {
"description": "2FA enabled",
"body": "{ \"success\": true, \"message\": \"Two-factor authentication has been enabled successfully.\", \"data\": { \"recovery_codes\": [...], \"warning\":\"...\" } }"
},
"422": {
"description": "Invalid code or not initiated",
"body": "{ \"success\": false, \"message\": \"INVALID_TWO_FACTOR_CODE\", \"error_code\":\"INVALID_TWO_FACTOR_CODE\" }"
}
}
}
GET /api/v1/user/two-factor/recovery-codes
Retrieve current recovery codes.
{
"title": "Get Recovery Codes",
"description": "Fetch two-factor recovery codes.",
"method": "GET",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/two-factor/recovery-codes",
"headers": [
{ "key": "Authorization", "value": "Bearer <token>", "required": true }
],
"bodyType": "none",
"responses": {
"200": {
"description": "Codes retrieved",
"body": "{ \"success\": true, \"message\": \"Recovery codes retrieved successfully.\", \"data\": { \"recovery_codes\": [...] } }"
},
"400": {
"description": "2FA not enabled",
"body": "{ \"success\": false, \"message\": \"TWO_FACTOR_NOT_ENABLED\", \"error_code\":\"TWO_FACTOR_NOT_ENABLED\" }"
}
}
}
POST /api/v1/user/two-factor/recovery-codes/regenerate
Regenerate recovery codes.
{
"title": "Regenerate Recovery Codes",
"description": "Invalidate old and create new recovery codes.",
"method": "POST",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/two-factor/recovery-codes/regenerate",
"headers": [
{ "key": "Authorization", "value": "Bearer <token>", "required": true }
],
"bodyType": "none",
"responses": {
"200": {
"description": "Codes regenerated",
"body": "{ \"success\": true, \"message\": \"Recovery codes regenerated successfully.\", \"data\": { \"recovery_codes\": [...] } }"
},
"400": {
"description": "2FA not enabled",
"body": "{ \"success\": false, \"message\": \"TWO_FACTOR_NOT_ENABLED\", \"error_code\":\"TWO_FACTOR_NOT_ENABLED\" }"
}
}
}
DELETE /api/v1/user/two-factor/disable
Disable two-factor.
{
"title": "Disable Two-Factor",
"description": "Turn off two-factor authentication.",
"method": "DELETE",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user/two-factor/disable",
"headers": [
{ "key": "Authorization", "value": "Bearer <token>", "required": true }
],
"bodyType": "json",
"requestBody": "{\n \"current_password\": \"password123\"\n}",
"responses": {
"200": {
"description": "2FA disabled",
"body": "{ \"success\": true, \"message\": \"Two-factor authentication has been disabled successfully.\", \"data\": null }"
},
"400": {
"description": "2FA not enabled",
"body": "{ \"success\": false, \"message\": \"TWO_FACTOR_NOT_ENABLED\", \"error_code\":\"TWO_FACTOR_NOT_ENABLED\" }"
}
}
}
GET /api/v1/user
Fetch authenticated user details.
{
"title": "Get Authenticated User",
"description": "Retrieve current user information.",
"method": "GET",
"baseUrl": "https://api.example.com",
"endpoint": "/api/v1/user",
"headers": [
{ "key": "Authorization", "value": "Bearer <token>", "required": true }
],
"bodyType": "none",
"responses": {
"200": {
"description": "User data",
"body": "{ \"success\": true, \"message\": \"\", \"data\": { \"user\": { ... } } }"
}
}
}
All new API routes are now documented with examples, request/response formats, and proper status codes. The OpenAPI generator can now produce an up-to-date spec automatically.
This documentation was automatically generated by DocuWriter.ai 🚀
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 14
♻️ Duplicate comments (14)
storage/postman/api_collection (3)
80-84: Sample payload still missingpassword_confirmationfield.This was flagged in a previous review. Registration endpoints typically require password confirmation for user-facing flows. The payload should include
"password_confirmation": "password123".
158-162: Reset-password payload missingpassword_confirmation.As noted in a previous review, this endpoint should include
password_confirmationto match typical password reset validation rules.
377-381: Password update also missingpassword_confirmation.Same story as register and reset-password - flagged in previous review. Add
"password_confirmation": "password123"to the payload..github/workflows/tests.yml (1)
67-68: This migrate step is still migrating the wrong database.Your CI job runs
php artisan migrate --forcewhich reads.env(probably MySQL). Then your tests run with Pest, which usesphpunit.xml's sqlite:memory:config. You're migrating one database and testing against a completely different one.This was already flagged in the previous review. The fix is simple: either remove this migrate step entirely and let Laravel's
RefreshDatabasetrait handle migrations per test, or configure the DB env vars in the workflow to point to sqlite before running migrate.Suggested fix:
- - name: Run Migrations - run: php artisan migrate --forceAnd ensure your tests use
RefreshDatabasetrait. Or, if you really want a global migrate step:+ - name: Configure Test Database + run: | + echo "DB_CONNECTION=sqlite" >> $GITHUB_ENV + echo "DB_DATABASE=:memory:" >> $GITHUB_ENV + - name: Run Migrations - run: php artisan migrate --force + run: php artisan migrate --force --database=sqliteapp/Services/UserDeletionService.php (1)
30-41: Still logging raw emails in a “GDPR-compliant” deletion serviceYou’re back to writing
user_emailinto logs in both hard delete and soft delete paths. For something explicitly branded as GDPR-friendly, immortalizing PII in log storage is… not that.Use a stable non-PII identifier (ID, or a hash of the email) or just drop the email entirely. Same story for the error logs.
Example fix:
- Log::info('User account deletion initiated', [ - 'user_id' => $user->id, - 'user_email' => $user->email, + Log::info('User account deletion initiated', [ + 'user_id' => $user->id, + 'user_email_hash' => hash('sha256', (string) $user->email), 'reason' => $reason, 'deleted_at' => now(), 'gdpr_compliant' => true, ]); @@ - Log::info('User account deletion completed', [ - 'user_id' => $user->id, - 'user_email' => $user->email, + Log::info('User account deletion completed', [ + 'user_id' => $user->id, + 'user_email_hash' => hash('sha256', (string) $user->email), 'cleanup_completed' => true, ]); @@ - Log::error('User account deletion failed', [ - 'user_id' => $user->id, - 'user_email' => $user->email, + Log::error('User account deletion failed', [ + 'user_id' => $user->id, + 'user_email_hash' => hash('sha256', (string) $user->email), 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); @@ - Log::info('User account soft deleted', [ - 'user_id' => $user->id, - 'user_email' => $user->email, + Log::info('User account soft deleted', [ + 'user_id' => $user->id, 'reason' => $reason, ]); @@ - Log::error('User account soft deletion failed', [ - 'user_id' => $user->id, - 'user_email' => $user->email, + Log::error('User account soft deletion failed', [ + 'user_id' => $user->id, 'error' => $e->getMessage(), ]);Also applies to: 75-79, 85-90, 105-135
app/Console/Commands/GenerateOpenApiSpec.php (3)
382-406: Route → FormRequest resolution is doing extra work and can be wrongYou already have the
Route $route, but you throw it away, re-scan the router by URI ingetControllerForUri, then repeat that dance separately for properties and required fields.Two problems:
- Performance/complexity: every schema extraction walks the entire route collection again.
- Correctness: matching only on
$uriignores HTTP method. AnyGET /api/v1/foo+POST /api/v1/foopair with different FormRequests will be mapped to whichever route happens to be “first”, so the POST body schema can easily be wrong.Refactor to pass the actual
Routethrough:
- Change
getRequestBodySpecto call something likegetRulesForRoute(Route $route)once.- Implement
getRulesForRouteusing$route->getAction()['controller']to resolve[controller, method].- Have
getRequestPropertiesandgetRequiredFieldswork off that rules array instead of re-computing via URI.Net result: less magic, less over-engineering, and your OpenAPI won’t lie when you eventually add multi-method endpoints.
Also applies to: 411-435, 584-627, 685-725
487-527: Rule → OpenAPI mapping still drops min/max and mislabelsnumeric
convertFieldRulesToOpenApiPropertyis half-finished:
- You parse
min:andmax:into$minLength/$maxLengthand track$isRequired, then throw them all away.- You map
'numeric'to OpenAPIinteger, which is just wrong for floats.The result is a spec that under-documents constraints and lies about numeric fields.
Tighten this up, e.g.:
- $isRequired = false; $isEmail = false; $isPassword = false; - $minLength = null; - $maxLength = null; @@ - if ($rule === 'required') { - $isRequired = true; - } elseif ($rule === 'email') { + if ($rule === 'email') { $isEmail = true; $property['format'] = 'email'; @@ } elseif (str_starts_with($rule, 'min:')) { - $minLength = (int) substr($rule, 4); + $min = (int) substr($rule, 4); + if (($property['type'] ?? 'string') === 'string') { + $property['minLength'] = $min; + } else { + $property['minimum'] = $min; + } } elseif (str_starts_with($rule, 'max:')) { - $maxLength = (int) substr($rule, 4); + $max = (int) substr($rule, 4); + if (($property['type'] ?? 'string') === 'string') { + $property['maxLength'] = $max; + } else { + $property['maximum'] = $max; + } - } elseif ($rule === 'integer' || $rule === 'numeric') { - $property['type'] = 'integer'; + } elseif ($rule === 'integer') { + $property['type'] = 'integer'; + } elseif ($rule === 'numeric') { + $property['type'] = 'number';Required-ness is already handled in
getRequiredFieldsFromRules, so just drop$isRequiredentirely.Right now consumers of your OpenAPI will be in “trust issues” mode when they hit numeric fields.
Also applies to: 564-579, 532-559
662-680: Dead “backward compatibility” helper that’s never been used
createMockFormRequest()doesn’t use its$formRequestClassparameter, isn’t called anywhere, and the “backward compatibility” comment is hilarious given this is a brand-new generator.Delete it and let git be your time machine if you ever need it back.
- /** - * Create a mock FormRequest instance without triggering validation. - */ - private function createMockFormRequest(string $formRequestClass): \Illuminate\Foundation\Http\FormRequest - { - // This method is kept for backward compatibility but we now use extractRulesWithoutValidation - return new class extends \Illuminate\Foundation\Http\FormRequest - { - public function authorize(): bool - { - return true; - } - - public function rules(): array - { - return []; - } - }; - }tests/Feature/ApiTest.php (5)
141-141: You're still asserting the token equals itself. Galaxy brain moment.Line 141:
'token' => $response->json('data.token')- you're literally saying "the token should be whatever the token is." This passes for any non-null value. It's like testingexpect(x).toBe(x). Either nuke it or actually validate it's a non-empty string.$response->assertOk() ->assertJson([ 'success' => true, 'message' => 'Login successful', 'data' => [ 'user' => [ 'id' => $user->id, 'name' => $user->name, 'email' => '[email protected]', ], - 'token' => $response->json('data.token'), 'token_type' => 'Bearer', ], 'errors' => null, - ]); + ]) + ->assertJsonPath('data.token', fn ($token) => is_string($token) && strlen($token) > 0);
567-576: Both logins get the same device name - your "multiple tokens" test isn't testing multiple distinct tokens.You're logging in twice without specifying a device, so both get
api-token-web. Your assertion at line 581-585 just verifies one exists. You have no proof there are actually two tokens with different identities.// Login multiple times to create multiple tokens $this->postJson('/api/v1/login', [ 'email' => '[email protected]', 'password' => 'password123', + 'device' => 'device1', ]); $loginResponse2 = $this->postJson('/api/v1/login', [ 'email' => '[email protected]', 'password' => 'password123', + 'device' => 'device2', ]);
846-849: URL parsing duplicated - extract a helper already.You're doing the
parse_url+parse_strdance twice (lines 846-849 and 921-923). Copy-paste is not DRY. Extract it:function getSignedUrlParams(string $signedUrl): array { $urlParts = parse_url($signedUrl); parse_str($urlParts['query'], $queryParams); return $queryParams; }Then:
$params = getSignedUrlParams($signedUrl);and grab$params['signature'],$params['expires'].Also applies to: 921-923
1074-1074:'test-secret'is not a valid TOTP secret, and you keep using it.Lines 1074, 1123, 1188, 1220 all use
'two_factor_secret' => 'test-secret'. This isn't a valid base32-encoded secret. These tests pass because you're only testing code retrieval, not OTP validation. Someone will copy this pattern for an OTP validation test and waste hours debugging.Use a real base32 secret:
'JBSWY3DPEHPK3PXP'or generate one properly.
246-256: Same self-referential assertion disease spreading.Lines 251-252:
'secret' => $response->json('data.secret')and'qr_code_url' => $response->json('data.qr_code_url')- you're testing that values equal themselves. This tests nothing. The structure check atassertJsonStructurewould be sufficient, or validate that secret matches a base32 pattern.$response->assertOk() ->assertJson([ 'success' => true, 'message' => 'Two-factor authentication setup initiated.', 'data' => [ - 'secret' => $response->json('data.secret'), - 'qr_code_url' => $response->json('data.qr_code_url'), 'next_step' => 'Scan the QR code with your authenticator app and confirm with the generated code.', ], 'errors' => null, - ]); + ]) + ->assertJsonPath('data.secret', fn ($s) => is_string($s) && preg_match('/^[A-Z2-7]{16,}$/', $s)) + ->assertJsonPath('data.qr_code_url', fn ($url) => str_starts_with($url, 'otpauth://'));app/Http/Controllers/Api/Auth/TwoFactorController.php (1)
43-47: You're still shipping the raw TOTP secret in the response.Line 44:
'secret' => $secret- this plaintext secret will end up in logs, network captures, and whoever else is watching. The QR code URL already contains the secret (it's in theotpauth://URI), which is unavoidable for setup.Most apps only return the QR code and have a "Can't scan? Show manual entry key" button that reveals the secret on demand via a separate endpoint. If you must return it here, at minimum document that this endpoint's response should NEVER be logged.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
Cache: Disabled due to Reviews > Disable Cache setting
📒 Files selected for processing (24)
.github/workflows/tests.yml(3 hunks)app/Console/Commands/GenerateOpenApiSpec.php(1 hunks)app/Http/Controllers/Api/Auth/EmailVerificationController.php(1 hunks)app/Http/Controllers/Api/Auth/LoginController.php(1 hunks)app/Http/Controllers/Api/Auth/LogoutController.php(1 hunks)app/Http/Controllers/Api/Auth/ProfileController.php(1 hunks)app/Http/Controllers/Api/Auth/RegisterController.php(1 hunks)app/Http/Controllers/Api/Auth/TwoFactorController.php(1 hunks)app/Http/Controllers/Api/Auth/UserController.php(1 hunks)app/Http/Middleware/ApiThrottle.php(1 hunks)app/Http/Requests/Api/Auth/ConfirmTwoFactorRequest.php(1 hunks)app/Http/Requests/Api/Auth/DeactivateAccountRequest.php(1 hunks)app/Http/Requests/Api/Auth/DeleteAccountRequest.php(1 hunks)app/Http/Requests/Api/Auth/DisableTwoFactorRequest.php(1 hunks)app/Http/Requests/Api/Auth/EnableTwoFactorRequest.php(1 hunks)app/Http/Requests/Api/Auth/UpdatePasswordRequest.php(1 hunks)app/Http/Requests/Api/Auth/UpdateProfileRequest.php(1 hunks)app/Http/Requests/Api/Auth/VerifyEmailRequest.php(1 hunks)app/Http/Resources/UserResource.php(1 hunks)app/Services/UserDeletionService.php(1 hunks)app/Support/TimeFormatter.php(1 hunks)storage/postman/api_collection(2 hunks)tests/Feature/ApiTest.php(1 hunks)tests/Unit/ExampleTest.php(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (16)
app/Http/Controllers/Api/Auth/ProfileController.php (4)
app/Helpers/ApiResponse.php (2)
ApiResponse(7-117)success(12-23)app/Http/Requests/Api/Auth/UpdateProfileRequest.php (1)
UpdateProfileRequest(8-86)app/Http/Resources/UserResource.php (1)
UserResource(8-24)app/Models/User.php (1)
sendEmailVerificationNotification(66-69)
tests/Feature/ApiTest.php (1)
app/Http/Requests/Api/Auth/UpdateProfileRequest.php (2)
UpdateProfileRequest(8-86)authorize(15-40)
app/Http/Requests/Api/Auth/DisableTwoFactorRequest.php (5)
app/Http/Requests/Api/Auth/ConfirmTwoFactorRequest.php (3)
authorize(15-18)rules(25-31)messages(38-46)app/Http/Requests/Api/Auth/DeactivateAccountRequest.php (3)
authorize(15-18)rules(25-31)messages(36-43)app/Http/Requests/Api/Auth/DeleteAccountRequest.php (3)
authorize(15-18)rules(25-30)messages(35-41)app/Http/Requests/Api/Auth/EnableTwoFactorRequest.php (3)
authorize(15-18)rules(25-30)messages(37-43)app/Http/Requests/Api/Auth/UpdateProfileRequest.php (3)
authorize(15-40)rules(47-66)messages(73-85)
app/Http/Controllers/Api/Auth/EmailVerificationController.php (4)
app/Helpers/ApiResponse.php (3)
ApiResponse(7-117)error(28-46)success(12-23)app/Http/Requests/Api/Auth/VerifyEmailRequest.php (1)
VerifyEmailRequest(7-43)app/Http/Resources/UserResource.php (1)
UserResource(8-24)app/Models/User.php (2)
User(14-70)sendEmailVerificationNotification(66-69)
app/Http/Requests/Api/Auth/UpdateProfileRequest.php (3)
app/Http/Requests/Api/Auth/DeleteAccountRequest.php (2)
authorize(15-18)rules(25-30)app/Http/Requests/Api/Auth/UpdatePasswordRequest.php (2)
authorize(15-18)rules(25-31)app/Http/Requests/Api/Auth/ForgotPasswordRequest.php (2)
authorize(12-15)rules(22-27)
app/Http/Middleware/ApiThrottle.php (1)
app/Helpers/ApiResponse.php (2)
ApiResponse(7-117)rateLimited(88-108)
app/Http/Requests/Api/Auth/EnableTwoFactorRequest.php (5)
app/Http/Requests/Api/Auth/ConfirmTwoFactorRequest.php (3)
authorize(15-18)rules(25-31)messages(38-46)app/Http/Requests/Api/Auth/DeleteAccountRequest.php (3)
authorize(15-18)rules(25-30)messages(35-41)app/Http/Requests/Api/Auth/DisableTwoFactorRequest.php (3)
authorize(15-18)rules(25-30)messages(35-41)app/Http/Requests/Api/Auth/UpdatePasswordRequest.php (3)
authorize(15-18)rules(25-31)messages(38-49)app/Http/Requests/Api/Auth/ForgotPasswordRequest.php (3)
authorize(12-15)rules(22-27)messages(34-40)
app/Http/Requests/Api/Auth/DeactivateAccountRequest.php (5)
app/Console/Commands/GenerateOpenApiSpec.php (2)
authorize(670-673)rules(675-678)app/Http/Requests/Api/Auth/DeleteAccountRequest.php (3)
authorize(15-18)rules(25-30)messages(35-41)app/Http/Requests/Api/Auth/EnableTwoFactorRequest.php (3)
authorize(15-18)rules(25-30)messages(37-43)app/Http/Requests/Api/Auth/VerifyEmailRequest.php (3)
authorize(12-15)rules(22-30)messages(35-42)app/Http/Requests/Api/Auth/ForgotPasswordRequest.php (3)
authorize(12-15)rules(22-27)messages(34-40)
app/Http/Requests/Api/Auth/DeleteAccountRequest.php (3)
app/Http/Requests/Api/Auth/DeactivateAccountRequest.php (3)
authorize(15-18)rules(25-31)messages(36-43)app/Http/Requests/Api/Auth/DisableTwoFactorRequest.php (3)
authorize(15-18)rules(25-30)messages(35-41)app/Http/Requests/Api/Auth/EnableTwoFactorRequest.php (3)
authorize(15-18)rules(25-30)messages(37-43)
tests/Unit/ExampleTest.php (2)
app/Support/TimeFormatter.php (2)
TimeFormatter(5-69)secondsToHuman(23-68)app/Helpers/ApiResponse.php (1)
secondsToHuman(113-116)
app/Console/Commands/GenerateOpenApiSpec.php (11)
tests/Feature/ApiTest.php (1)
route(716-719)app/Http/Requests/Api/Auth/DeactivateAccountRequest.php (1)
rules(25-31)app/Http/Requests/Api/Auth/DisableTwoFactorRequest.php (1)
rules(25-30)app/Http/Requests/Api/Auth/EnableTwoFactorRequest.php (1)
rules(25-30)app/Http/Requests/Api/Auth/UpdatePasswordRequest.php (1)
rules(25-31)app/Http/Requests/Api/Auth/UpdateProfileRequest.php (1)
rules(47-66)app/Http/Requests/Api/Auth/VerifyEmailRequest.php (1)
rules(22-30)app/Http/Requests/Api/Auth/RegisterRequest.php (1)
rules(22-29)app/Http/Requests/Api/Auth/ForgotPasswordRequest.php (1)
rules(22-27)app/Http/Requests/Api/Auth/LoginRequest.php (1)
rules(22-30)app/Http/Requests/Api/Auth/ResetPasswordRequest.php (1)
rules(22-29)
app/Http/Requests/Api/Auth/ConfirmTwoFactorRequest.php (4)
app/Http/Requests/Api/Auth/DeleteAccountRequest.php (2)
authorize(15-18)rules(25-30)app/Http/Requests/Api/Auth/EnableTwoFactorRequest.php (2)
authorize(15-18)rules(25-30)app/Http/Requests/Api/Auth/VerifyEmailRequest.php (2)
authorize(12-15)rules(22-30)app/Http/Requests/Api/Auth/ForgotPasswordRequest.php (2)
authorize(12-15)rules(22-27)
app/Http/Controllers/Api/Auth/UserController.php (3)
app/Helpers/ApiResponse.php (2)
ApiResponse(7-117)success(12-23)app/Http/Resources/UserResource.php (1)
UserResource(8-24)app/Http/Controllers/Api/Auth/LoginController.php (1)
__invoke(21-60)
app/Http/Controllers/Api/Auth/LogoutController.php (1)
app/Helpers/ApiResponse.php (3)
ApiResponse(7-117)success(12-23)error(28-46)
app/Http/Controllers/Api/Auth/TwoFactorController.php (5)
app/Helpers/ApiResponse.php (3)
ApiResponse(7-117)error(28-46)success(12-23)app/Http/Requests/Api/Auth/ConfirmTwoFactorRequest.php (1)
ConfirmTwoFactorRequest(7-47)app/Http/Requests/Api/Auth/DisableTwoFactorRequest.php (1)
DisableTwoFactorRequest(7-42)app/Http/Requests/Api/Auth/EnableTwoFactorRequest.php (1)
EnableTwoFactorRequest(7-44)tests/Feature/ApiTest.php (1)
user(702-705)
app/Support/TimeFormatter.php (1)
app/Helpers/ApiResponse.php (1)
secondsToHuman(113-116)
🪛 PHPMD (2.15.0)
app/Http/Controllers/Api/Auth/ProfileController.php
36-38: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'update'. (undefined)
(StaticAccess)
37-37: Avoid using static access to class '\App\Http\Resources\UserResource' in method 'update'. (undefined)
(StaticAccess)
app/Http/Controllers/Api/Auth/EmailVerificationController.php
20-20: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'sendVerificationEmail'. (undefined)
(StaticAccess)
26-26: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'sendVerificationEmail'. (undefined)
(StaticAccess)
30-30: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'sendVerificationEmail'. (undefined)
(StaticAccess)
34-94: The method verifyEmail() has an NPath complexity of 256. The configured NPath complexity threshold is 200. (undefined)
(NPathComplexity)
36-36: Avoid using static access to class '\App\Models\User' in method 'verifyEmail'. (undefined)
(StaticAccess)
39-39: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'verifyEmail'. (undefined)
(StaticAccess)
44-44: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'verifyEmail'. (undefined)
(StaticAccess)
52-52: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'verifyEmail'. (undefined)
(StaticAccess)
60-60: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'verifyEmail'. (undefined)
(StaticAccess)
78-82: Avoid using static access to class '\Illuminate\Http\Request' in method 'verifyEmail'. (undefined)
(StaticAccess)
85-85: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'verifyEmail'. (undefined)
(StaticAccess)
91-93: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'verifyEmail'. (undefined)
(StaticAccess)
92-92: Avoid using static access to class '\App\Http\Resources\UserResource' in method 'verifyEmail'. (undefined)
(StaticAccess)
app/Http/Middleware/ApiThrottle.php
42-47: The method handle uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)
(ElseExpression)
56-61: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'handle'. (undefined)
(StaticAccess)
app/Services/UserDeletionService.php
32-95: Avoid using static access to class '\Illuminate\Support\Facades\DB' in method 'deleteUser'. (undefined)
(StaticAccess)
35-41: Avoid using static access to class '\Illuminate\Support\Facades\Log' in method 'deleteUser'. (undefined)
(StaticAccess)
56-56: Avoid using static access to class '\Illuminate\Support\Facades\Schema' in method 'deleteUser'. (undefined)
(StaticAccess)
75-79: Avoid using static access to class '\Illuminate\Support\Facades\Log' in method 'deleteUser'. (undefined)
(StaticAccess)
85-90: Avoid using static access to class '\Illuminate\Support\Facades\Log' in method 'deleteUser'. (undefined)
(StaticAccess)
108-118: Avoid using static access to class '\Illuminate\Support\Facades\DB' in method 'softDeleteUser'. (undefined)
(StaticAccess)
120-124: Avoid using static access to class '\Illuminate\Support\Facades\Log' in method 'softDeleteUser'. (undefined)
(StaticAccess)
129-133: Avoid using static access to class '\Illuminate\Support\Facades\Log' in method 'softDeleteUser'. (undefined)
(StaticAccess)
app/Http/Controllers/Api/Auth/LoginController.php
29-29: Avoid using static access to class '\Illuminate\Support\Facades\Hash' in method '__invoke'. (undefined)
(StaticAccess)
30-32: Avoid using static access to class '\Illuminate\Validation\ValidationException' in method '__invoke'. (undefined)
(StaticAccess)
37-39: Avoid using static access to class '\Illuminate\Validation\ValidationException' in method '__invoke'. (undefined)
(StaticAccess)
43-43: Avoid using static access to class '\Illuminate\Support\Facades\Hash' in method '__invoke'. (undefined)
(StaticAccess)
44-44: Avoid using static access to class '\Illuminate\Support\Facades\Hash' in method '__invoke'. (undefined)
(StaticAccess)
55-59: Avoid using static access to class '\App\Helpers\ApiResponse' in method '__invoke'. (undefined)
(StaticAccess)
56-56: Avoid using static access to class '\App\Http\Resources\UserResource' in method '__invoke'. (undefined)
(StaticAccess)
app/Console/Commands/GenerateOpenApiSpec.php
13-734: The class GenerateOpenApiSpec has an overall complexity of 99 which is very high. The configured complexity threshold is 50. (undefined)
(ExcessiveClassComplexity)
38-38: Avoid using static access to class '\Symfony\Component\Yaml\Yaml' in method 'handle'. (undefined)
(StaticAccess)
74-74: Avoid using static access to class '\Illuminate\Support\Facades\Route' in method 'getApiRoutes'. (undefined)
(StaticAccess)
258-260: The method getSummaryFromController uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)
(ElseExpression)
413-413: Avoid using static access to class '\Illuminate\Support\Facades\Route' in method 'getControllerForUri'. (undefined)
(StaticAccess)
487-527: The method convertFieldRulesToOpenApiProperty() has a Cyclomatic Complexity of 12. The configured cyclomatic complexity threshold is 10. (undefined)
(CyclomaticComplexity)
493-493: Avoid unused local variables such as '$isRequired'. (undefined)
(UnusedLocalVariable)
496-496: Avoid unused local variables such as '$minLength'. (undefined)
(UnusedLocalVariable)
497-497: Avoid unused local variables such as '$maxLength'. (undefined)
(UnusedLocalVariable)
608-611: The method getRequestProperties uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)
(ElseExpression)
643-643: Avoid using static access to class '\Illuminate\Http\Request' in method 'extractRulesWithoutValidation'. (undefined)
(StaticAccess)
665-680: Avoid unused private methods such as 'createMockFormRequest'. (undefined)
(UnusedPrivateMethod)
665-665: Avoid unused parameters such as '$formRequestClass'. (undefined)
(UnusedFormalParameter)
709-712: The method getRequiredFields uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)
(ElseExpression)
app/Http/Resources/UserResource.php
15-15: Avoid unused parameters such as '$request'. (undefined)
(UnusedFormalParameter)
app/Http/Controllers/Api/Auth/UserController.php
17-19: Avoid using static access to class '\App\Helpers\ApiResponse' in method '__invoke'. (undefined)
(StaticAccess)
18-18: Avoid using static access to class '\App\Http\Resources\UserResource' in method '__invoke'. (undefined)
(StaticAccess)
app/Http/Controllers/Api/Auth/LogoutController.php
13-122: The method logout() has 110 lines of code. Current threshold is set to 100. Avoid really long methods. (undefined)
(ExcessiveMethodLength)
26-26: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logout'. (undefined)
(StaticAccess)
29-33: Avoid using static access to class '\Log' in method 'logout'. (undefined)
(StaticAccess)
35-40: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logout'. (undefined)
(StaticAccess)
42-78: The method logout uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)
(ElseExpression)
48-48: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logout'. (undefined)
(StaticAccess)
50-54: Avoid using static access to class '\Log' in method 'logout'. (undefined)
(StaticAccess)
56-61: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logout'. (undefined)
(StaticAccess)
63-77: The method logout uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)
(ElseExpression)
65-69: Avoid using static access to class '\Log' in method 'logout'. (undefined)
(StaticAccess)
71-76: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logout'. (undefined)
(StaticAccess)
79-87: The method logout uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)
(ElseExpression)
81-86: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logout'. (undefined)
(StaticAccess)
99-99: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logout'. (undefined)
(StaticAccess)
101-104: Avoid using static access to class '\Log' in method 'logout'. (undefined)
(StaticAccess)
106-111: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logout'. (undefined)
(StaticAccess)
116-121: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logout'. (undefined)
(StaticAccess)
128-133: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logoutFromAllDevices'. (undefined)
(StaticAccess)
140-140: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logoutFromAllDevices'. (undefined)
(StaticAccess)
142-145: Avoid using static access to class '\Log' in method 'logoutFromAllDevices'. (undefined)
(StaticAccess)
147-152: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logoutFromAllDevices'. (undefined)
(StaticAccess)
app/Http/Controllers/Api/Auth/TwoFactorController.php
24-24: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'enable'. (undefined)
(StaticAccess)
43-47: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'enable'. (undefined)
(StaticAccess)
58-58: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'confirm'. (undefined)
(StaticAccess)
61-61: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'confirm'. (undefined)
(StaticAccess)
69-69: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'confirm'. (undefined)
(StaticAccess)
81-84: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'confirm'. (undefined)
(StaticAccess)
93-93: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'recoveryCodes'. (undefined)
(StaticAccess)
100-100: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'recoveryCodes'. (undefined)
(StaticAccess)
104-104: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'recoveryCodes'. (undefined)
(StaticAccess)
109-109: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'recoveryCodes'. (undefined)
(StaticAccess)
112-115: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'recoveryCodes'. (undefined)
(StaticAccess)
124-124: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'regenerateRecoveryCodes'. (undefined)
(StaticAccess)
135-138: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'regenerateRecoveryCodes'. (undefined)
(StaticAccess)
147-147: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'disable'. (undefined)
(StaticAccess)
157-157: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'disable'. (undefined)
(StaticAccess)
app/Http/Controllers/Api/Auth/RegisterController.php
29-33: Avoid using static access to class '\App\Helpers\ApiResponse' in method '__invoke'. (undefined)
(StaticAccess)
30-30: Avoid using static access to class '\App\Http\Resources\UserResource' in method '__invoke'. (undefined)
(StaticAccess)
51-51: Avoid using static access to class '\App\Helpers\ApiResponse' in method '__invoke'. (undefined)
(StaticAccess)
🔇 Additional comments (33)
storage/postman/api_collection (6)
12-48: LGTM!Login endpoint is properly structured with the right method, headers, and payload fields.
89-127: LGTM!Forgot-password endpoint correctly requires only the email field.
395-477: Session management endpoints look solid.GET for listing sessions, DELETE for revoking a specific session with proper
{sessionId}path param. Auth is wired up correctly. No complaints.
479-709: 2FA flow coverage is comprehensive.GET recovery codes, POST regenerate, POST enable, PATCH confirm, DELETE disable - all the operations are there with proper auth. This is how you do a 2FA API surface.
854-939: Logout endpoints properly structured.Both single-device logout and logout-all require bearer auth as expected. Clean.
943-964: Collection auth and variables well configured.Having
auth_tokenas a collection variable with collection-level bearer auth as fallback is the right pattern. Individual endpoints can override when needed. Good DX for whoever's actually using this collection.app/Http/Controllers/Api/Auth/UserController.php (1)
13-20: LGTM. Clean fix for the previous raw user exposure.You wrapped the user in
UserResource, which explicitly projects only the safe fields. No more accidental leakage of 2FA secrets or recovery codes. This is the right pattern.app/Http/Requests/Api/Auth/DeactivateAccountRequest.php (1)
15-18: Both previous issues addressed correctly.You fixed the password validation (added
current_password:sanctum) and simplified authorization (now justreturn truetrusting route middleware). Clean.Also applies to: 27-30
app/Http/Requests/Api/Auth/EnableTwoFactorRequest.php (1)
15-18: Validation is correct.You're using
current_password:sanctumwhich explicitly tells Laravel to check against the sanctum guard. Theauth()->check()inauthorize()is redundant since the route middleware already enforces it, but it's harmless defense-in-depth.Also applies to: 27-29
app/Http/Requests/Api/Auth/DisableTwoFactorRequest.php (1)
15-18: Field naming is consistent, validation is correct.This request uses
current_passwordconsistently with the other account management endpoints, and correctly validates against thesanctumguard.Also applies to: 27-29
app/Support/TimeFormatter.php (1)
23-68: Solid implementation despite the linter crying.The logic is clean: break down time units, skip zeros, format with Oxford comma. PHPMD whines about cyclomatic complexity, but you already extracted
formatTimeUnit()to keep it readable. The code is easy to follow and handles all edge cases correctly.Only microscopic nitpick:
floor()returnsfloat, but you're using it asintin arithmetic. PHP coerces it fine, but if you want to make static analysis shut up:- $days = floor($seconds / 86400); + $days = (int) floor($seconds / 86400);Same for
$hoursand$minutes. But honestly? Ship it as-is.app/Http/Requests/Api/Auth/DeleteAccountRequest.php (1)
15-18: DeleteAccountRequest now properly enforces password verificationThis finally uses
current_password:sanctumand an auth check, matching your other sensitive flows. No correctness gaps spotted here.Also applies to: 25-30
app/Http/Requests/Api/Auth/VerifyEmailRequest.php (1)
22-29: VerifyEmailRequest is lean and consistent with the controller flowValidation matches how
EmailVerificationControlleractually uses the fields, and the earlier bogusid.existsmessage is gone. Nothing to fix here.Also applies to: 35-41
app/Http/Requests/Api/Auth/UpdatePasswordRequest.php (1)
15-18: UpdatePasswordRequest closes the previous validation holesGuard-qualified
current_password:sanctumplus a sane max length and “different from current” is exactly what you want here. This request layer looks solid.Also applies to: 25-30, 38-48
app/Http/Controllers/Api/Auth/RegisterController.php (1)
15-52: Registration flow is clean, token naming + logging look saneNice cleanup: reuse
CreateNewUser, generate a device‑tagged token name, and log only allow‑listed fields with stack traces gated byapp.debug. This is how you do an auth endpoint without turning logs into a data breach.app/Http/Controllers/Api/Auth/ProfileController.php (1)
13-38: Profile update finally stops flirting with “store current_password in DB”Good fix: you now whitelist updatable fields and keep
current_passwordout of the mass-assignment path, while still handling email-change verification correctly and usingUserResourcefor the response. This is the right level of paranoia for a profile endpoint.app/Http/Middleware/ApiThrottle.php (1)
25-65: Clean implementation - solid work after the refactor.The middleware now:
- Properly uses null coalescing for config fallbacks (route params take precedence)
- Delegates actual throttling to parent
- Returns standardized
ApiResponse::rateLimited()for 429 responsesThe static access warnings from PHPMD are false positives - this is the established pattern in this codebase for helper classes. The else expression is readable and appropriate here.
app/Http/Requests/Api/Auth/UpdateProfileRequest.php (1)
47-66: Validation rules look solid.Good defensive implementation:
- Email uniqueness properly ignores current user
current_passwordconditionally required only when email changes (security win)- Uses
current_password:sanctumguard for proper Sanctum token validationapp/Http/Requests/Api/Auth/ConfirmTwoFactorRequest.php (1)
7-47: Clean FormRequest - follows the established pattern.Consistent with sibling request classes. The
digits:6rule ensures exactly 6 numeric characters, and requiringcurrent_passwordbefore enabling 2FA is proper security hygiene. The docblock reminder aboutauth:sanctummiddleware is a nice touch for defense in depth.app/Http/Controllers/Api/Auth/LoginController.php (1)
21-60: Login flow is solid after the refactor.Previous issues addressed:
- No longer using stateful
auth('web')->attempt()- Deactivation check blocks deactivated users from getting tokens
- Password rehash on login keeps hashes fresh
The static access warnings are false positives -
Hash,ValidationException, andApiResponseare standard Laravel/project patterns.app/Http/Controllers/Api/Auth/LogoutController.php (1)
124-154:logoutFromAllDevicesis clean.Properly requires token auth, wraps the bulk delete in try-catch, and returns appropriate error codes. The bearer token check addresses the past review concern about inconsistency with
logout().tests/Feature/ApiTest.php (7)
9-35: LGTM! Clean auth guard tests.Basic authentication checks are solid - you're testing both the negative (unauthenticated) and positive (authenticated) paths. Structure assertions are appropriate.
37-118: Registration tests are comprehensive.Good coverage: happy path, validation errors, and duplicate email handling. The
'token' => trueassertion pattern (line 56) correctly verifies token presence without the self-referential assertion anti-pattern.
156-197: Multi-device token test done right.Unlike certain other tests in this file, you actually specify different device names here. Both tokens are properly verified with distinct names. Good job.
1474-1482: Good fix on the token verification.You're now querying
PersonalAccessTokendirectly instead of calling$user->tokens()on a deleted model. That past comment was addressed properly.
682-775: Type juggling authorization test is thorough.Good security-conscious testing. You're validating that string "2" and int 2 are both rejected when they don't match the authenticated user, and that string "1" and int 1 are both accepted when they do match. The edge case with ID=0 is a nice touch - PHP's loose comparisons with zero are notorious footguns.
463-528: Password update tests are solid.Testing both success and failure paths with proper credential validation. Good coverage.
1275-1450: Account lifecycle tests (deactivate/reactivate) are comprehensive.You're testing:
- Successful deactivation with token revocation
- Wrong password rejection
- Already deactivated account handling
- Reactivation flows
- State verification after each operation
This is the level of coverage these sensitive operations need.
app/Http/Controllers/Api/Auth/TwoFactorController.php (5)
50-85: Confirm flow is solid.Good defensive coding:
- DecryptException handling for corrupted secrets
- Verification window of 1 allows reasonable clock drift (~30s each way)
- Recovery codes generated atomically with confirmation
One thing:
verifyKeywith window=1 means codes from -30s to +30s are valid. That's industry standard.
87-116: Recovery codes retrieval looks good.The json_decode error handling is correct now - checking
json_last_error()instead of wrapping in a useless try-catch. DecryptException handling is appropriate since the model cast could fail on corrupted data.
141-158: Disable method now properly requires password verification.Using
DisableTwoFactorRequestaddresses the past security concern. All three 2FA fields are properly nulled out.
160-167: Recovery code generation looks good.9 codes with 64 bits of entropy each (two 4-byte random hex segments). The
XXXX-XXXXformat is much more human-readable than the raw 10-char hex that was suggested previously. Total entropy of ~576 bits for the recovery set is plenty.
1-15: PHPMD warnings about static access are noise - ignore them.
ApiResponse::success()andApiResponse::error()are static helpers by design. This is standard Laravel practice for utility classes. PHPMD flagging every static call is like a linter complaining you're usingMath.max()in JavaScript.
| public function verifyEmail(VerifyEmailRequest $request): JsonResponse | ||
| { | ||
| $user = User::find($request->id); | ||
|
|
||
| if (! $user) { | ||
| return ApiResponse::error('User not found.', null, 404, 'USER_NOT_FOUND'); | ||
| } | ||
|
|
||
| // Check if email is already verified | ||
| if ($user->hasVerifiedEmail()) { | ||
| return ApiResponse::error('Email is already verified.', null, 400, 'EMAIL_ALREADY_VERIFIED'); | ||
| } | ||
|
|
||
| // Verify the hash matches the user's email | ||
| if (! hash_equals( | ||
| sha1((string) $user->getEmailForVerification()), | ||
| (string) $request->hash | ||
| )) { | ||
| return ApiResponse::error('Invalid verification link.', null, 400, 'INVALID_VERIFICATION_LINK'); | ||
| } | ||
|
|
||
| // CRITICAL SECURITY: Require signed request to prevent unauthorized verification | ||
| // The request must include a valid signature to prove it came from a legitimate verification link | ||
| $signature = $request->get('signature') ?: $request->query('signature'); | ||
|
|
||
| if (! $signature) { | ||
| return ApiResponse::error('Verification signature required.', null, 400, 'SIGNATURE_REQUIRED'); | ||
| } | ||
|
|
||
| // Validate the signature by creating a GET request with the same parameters | ||
| $queryParams = $request->query(); | ||
|
|
||
| // Ensure we have the required parameters for signature validation | ||
| // Only populate from request body if absent from query parameters | ||
| if (empty($queryParams['id'])) { | ||
| $queryParams['id'] = $request->id; | ||
| } | ||
| if (empty($queryParams['hash'])) { | ||
| $queryParams['hash'] = $request->hash; | ||
| } | ||
|
|
||
| // Sort parameters alphabetically as required for signature validation | ||
| ksort($queryParams); | ||
|
|
||
| $getRequest = \Illuminate\Http\Request::create( | ||
| route('api.v1.email.verify'), | ||
| 'GET', | ||
| $queryParams | ||
| ); | ||
|
|
||
| if (! $getRequest->hasValidSignature()) { | ||
| return ApiResponse::error('Invalid or expired verification link.', null, 400, 'EXPIRED_VERIFICATION_LINK'); | ||
| } | ||
|
|
||
| // Mark email as verified | ||
| $user->markEmailAsVerified(); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
verifyEmail does the right security checks, but it’s turning into a “god method”
The flow (user lookup, hash check, signature validation, then mark verified) is solid and avoids the earlier signature bugs. But this method is doing everything itself, which is why PHPMD is screaming about NPath=256.
Consider extracting helpers like resolveVerifiableUser(), assertValidVerificationHash(), and assertValidSignature() to keep this thing from becoming the next AuthService::doEverything(). Behavior can stay identical; just push the branches into private methods.
🧰 Tools
🪛 PHPMD (2.15.0)
34-94: The method verifyEmail() has an NPath complexity of 256. The configured NPath complexity threshold is 200. (undefined)
(NPathComplexity)
36-36: Avoid using static access to class '\App\Models\User' in method 'verifyEmail'. (undefined)
(StaticAccess)
39-39: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'verifyEmail'. (undefined)
(StaticAccess)
44-44: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'verifyEmail'. (undefined)
(StaticAccess)
52-52: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'verifyEmail'. (undefined)
(StaticAccess)
60-60: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'verifyEmail'. (undefined)
(StaticAccess)
78-82: Avoid using static access to class '\Illuminate\Http\Request' in method 'verifyEmail'. (undefined)
(StaticAccess)
85-85: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'verifyEmail'. (undefined)
(StaticAccess)
🤖 Prompt for AI Agents
In app/Http/Controllers/Api/Auth/EmailVerificationController.php around lines 34
to 90, this verifyEmail method is too large and triggers PHPMD NPath complexity;
extract the logical blocks into private helper methods: create private
resolveVerifiableUser(VerifyEmailRequest $request): User|null to encapsulate
user lookup and the "User not found" response logic (or throw an exception you
handle), assertValidVerificationHash(User $user, string $hash): void to
encapsulate the sha1/ hash_equals validation and throw or return a standardized
error, and assertValidSignature(VerifyEmailRequest $request): void to
encapsulate signature extraction, query param population, request creation and
hasValidSignature() check (again throw or return standardized error); refactor
verifyEmail to call these helpers in order and keep the original responses and
behavior unchanged.
| // Final fallback if sanitization made it empty | ||
| return $sanitized === '' || $sanitized === '0' ? 'web' : $sanitized; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
What's with the '0' check?
After trim($sanitized, '-_'), if the user sent just -_-_-, you get ''. But when would you get '0'? If the input was literally "0", which is... a valid device name? This check would reject it and return 'web' instead.
If '0' is intentionally blocked, add a comment explaining why. Otherwise, this looks like a copy-paste artifact:
- return $sanitized === '' || $sanitized === '0' ? 'web' : $sanitized;
+ return $sanitized === '' ? 'web' : $sanitized;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Final fallback if sanitization made it empty | |
| return $sanitized === '' || $sanitized === '0' ? 'web' : $sanitized; | |
| } | |
| // Final fallback if sanitization made it empty | |
| return $sanitized === '' ? 'web' : $sanitized; | |
| } |
🤖 Prompt for AI Agents
In app/Http/Controllers/Api/Auth/LoginController.php around lines 92 to 94, the
ternary check treats the string '0' as equivalent to empty and returns 'web';
this will incorrectly reject a literal device name "0". Either remove the "'0'
||" part from the condition so only empty strings fall back to 'web', or if '0'
must be prohibited, keep the check and add a clear inline comment explaining why
"0" is invalid; update tests if any to cover the "0" input behavior.
| public function logout(Request $request): JsonResponse | ||
| { | ||
| // Check if this is token-based authentication (Sanctum) | ||
| if ($request->bearerToken()) { | ||
| // Token-based authentication: delete the current token | ||
| $token = $request->user()->currentAccessToken(); | ||
|
|
||
| if ($token) { | ||
| // Validate token type and attempt deletion | ||
| if ($token instanceof \Laravel\Sanctum\PersonalAccessToken) { | ||
| try { | ||
| $token->delete(); | ||
|
|
||
| return ApiResponse::success(null, 'Successfully logged out from this device.'); | ||
| } catch (\Exception $e) { | ||
| // Log the error but don't expose internal details | ||
| \Log::error('Failed to delete personal access token', [ | ||
| 'token_id' => $token->id, | ||
| 'user_id' => $request->user()->id, | ||
| 'error' => $e->getMessage(), | ||
| ]); | ||
|
|
||
| return ApiResponse::error( | ||
| 'Failed to revoke token. Please try again.', | ||
| null, | ||
| 500, | ||
| 'TOKEN_REVOKE_FAILED' | ||
| ); | ||
| } | ||
| } else { | ||
| // Token exists but is not a PersonalAccessToken - check if revoke method exists | ||
| if (method_exists($token, 'revoke')) { | ||
| try { | ||
| $token->revoke(); | ||
|
|
||
| return ApiResponse::success(null, 'Successfully logged out from this device.'); | ||
| } catch (\Exception $e) { | ||
| \Log::error('Failed to revoke non-personal access token', [ | ||
| 'token_type' => $token::class, | ||
| 'user_id' => $request->user()->id, | ||
| 'error' => $e->getMessage(), | ||
| ]); | ||
|
|
||
| return ApiResponse::error( | ||
| 'Unable to revoke authentication token.', | ||
| null, | ||
| 403, | ||
| 'TOKEN_REVOKE_UNSUPPORTED' | ||
| ); | ||
| } | ||
| } else { | ||
| // Token type does not support revoke method | ||
| \Log::warning('Attempted to revoke token without revoke method', [ | ||
| 'token_type' => $token::class, | ||
| 'user_id' => $request->user()->id, | ||
| 'token_id' => $token->id ?? 'unknown', | ||
| ]); | ||
|
|
||
| return ApiResponse::error( | ||
| 'Unable to revoke authentication token.', | ||
| null, | ||
| 403, | ||
| 'TOKEN_REVOKE_UNSUPPORTED' | ||
| ); | ||
| } | ||
| } | ||
| } else { | ||
| // Token was provided in request but not found in database | ||
| return ApiResponse::error( | ||
| 'Invalid or expired authentication token.', | ||
| null, | ||
| 401, | ||
| 'INVALID_TOKEN' | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| // Check if this is session-based authentication (web guard) | ||
| if (Auth::guard('web')->check()) { | ||
| try { | ||
| Auth::guard('web')->logout(); | ||
|
|
||
| // Invalidate the session and regenerate CSRF token | ||
| $request->session()->invalidate(); | ||
| $request->session()->regenerateToken(); | ||
|
|
||
| return ApiResponse::success(null, 'Successfully logged out from this device.'); | ||
| } catch (\Exception $e) { | ||
| \Log::error('Failed to logout from web session', [ | ||
| 'user_id' => $request->user()->id ?? 'unknown', | ||
| 'error' => $e->getMessage(), | ||
| ]); | ||
|
|
||
| return ApiResponse::error( | ||
| 'Failed to logout. Please try again.', | ||
| null, | ||
| 500, | ||
| 'SESSION_LOGOUT_FAILED' | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| // No valid authentication method found | ||
| return ApiResponse::error( | ||
| 'No valid authentication found. Please authenticate first.', | ||
| null, | ||
| 401, | ||
| 'NO_AUTHENTICATION' | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
This method is 110 lines of nested if-else spaghetti.
I get it - you're handling token auth, session auth, different token types, and error cases. But this is the kind of method where a bug can hide for months. PHPMD isn't wrong to flag it.
Extract the token handling into a private method:
public function logout(Request $request): JsonResponse
{
// Check if this is token-based authentication (Sanctum)
if ($request->bearerToken()) {
- // Token-based authentication: delete the current token
- $token = $request->user()->currentAccessToken();
- // ... 70 lines of token handling ...
+ return $this->handleTokenLogout($request);
}
// Check if this is session-based authentication (web guard)
if (Auth::guard('web')->check()) {
- try {
- // ... session handling ...
- } catch (\Exception $e) {
- // ... error handling ...
- }
+ return $this->handleSessionLogout($request);
}
// No valid authentication method found
return ApiResponse::error(
'No valid authentication found. Please authenticate first.',
null,
401,
'NO_AUTHENTICATION'
);
}
+
+private function handleTokenLogout(Request $request): JsonResponse
+{
+ $token = $request->user()->currentAccessToken();
+ // ... token handling logic ...
+}
+
+private function handleSessionLogout(Request $request): JsonResponse
+{
+ // ... session handling logic ...
+}Committable suggestion skipped: line range outside the PR's diff.
🧰 Tools
🪛 PHPMD (2.15.0)
13-122: The method logout() has 110 lines of code. Current threshold is set to 100. Avoid really long methods. (undefined)
(ExcessiveMethodLength)
26-26: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logout'. (undefined)
(StaticAccess)
29-33: Avoid using static access to class '\Log' in method 'logout'. (undefined)
(StaticAccess)
35-40: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logout'. (undefined)
(StaticAccess)
42-78: The method logout uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)
(ElseExpression)
48-48: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logout'. (undefined)
(StaticAccess)
50-54: Avoid using static access to class '\Log' in method 'logout'. (undefined)
(StaticAccess)
56-61: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logout'. (undefined)
(StaticAccess)
63-77: The method logout uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)
(ElseExpression)
65-69: Avoid using static access to class '\Log' in method 'logout'. (undefined)
(StaticAccess)
71-76: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logout'. (undefined)
(StaticAccess)
79-87: The method logout uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)
(ElseExpression)
81-86: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logout'. (undefined)
(StaticAccess)
99-99: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logout'. (undefined)
(StaticAccess)
101-104: Avoid using static access to class '\Log' in method 'logout'. (undefined)
(StaticAccess)
106-111: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logout'. (undefined)
(StaticAccess)
116-121: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logout'. (undefined)
(StaticAccess)
| return ApiResponse::error( | ||
| 'Unable to revoke authentication token.', | ||
| null, | ||
| 403, | ||
| 'TOKEN_REVOKE_UNSUPPORTED' | ||
| ); | ||
| } | ||
| } else { | ||
| // Token type does not support revoke method | ||
| \Log::warning('Attempted to revoke token without revoke method', [ | ||
| 'token_type' => $token::class, | ||
| 'user_id' => $request->user()->id, | ||
| 'token_id' => $token->id ?? 'unknown', | ||
| ]); | ||
|
|
||
| return ApiResponse::error( | ||
| 'Unable to revoke authentication token.', | ||
| null, | ||
| 403, | ||
| 'TOKEN_REVOKE_UNSUPPORTED' | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
DRY violation - same error response duplicated.
Lines 56-61 and 71-76 return identical TOKEN_REVOKE_UNSUPPORTED responses. The only difference is the log message (one says "revoke" failed, one says "no revoke method"). Consolidate:
- } else {
- // Token exists but is not a PersonalAccessToken - check if revoke method exists
- if (method_exists($token, 'revoke')) {
- try {
- $token->revoke();
-
- return ApiResponse::success(null, 'Successfully logged out from this device.');
- } catch (\Exception $e) {
- \Log::error('Failed to revoke non-personal access token', [
- 'token_type' => $token::class,
- 'user_id' => $request->user()->id,
- 'error' => $e->getMessage(),
- ]);
-
- return ApiResponse::error(
- 'Unable to revoke authentication token.',
- null,
- 403,
- 'TOKEN_REVOKE_UNSUPPORTED'
- );
- }
- } else {
- // Token type does not support revoke method
- \Log::warning('Attempted to revoke token without revoke method', [
- 'token_type' => $token::class,
- 'user_id' => $request->user()->id,
- 'token_id' => $token->id ?? 'unknown',
- ]);
-
- return ApiResponse::error(
- 'Unable to revoke authentication token.',
- null,
- 403,
- 'TOKEN_REVOKE_UNSUPPORTED'
- );
- }
- }
+ } elseif (method_exists($token, 'revoke')) {
+ try {
+ $token->revoke();
+ return ApiResponse::success(null, 'Successfully logged out from this device.');
+ } catch (\Exception $e) {
+ // Fall through to unsupported response
+ }
+ }
+
+ // Token type not supported or revoke failed
+ \Log::warning('Unable to revoke token', [
+ 'token_type' => $token::class,
+ 'user_id' => $request->user()->id,
+ 'token_id' => $token->id ?? 'unknown',
+ ]);
+
+ return ApiResponse::error(
+ 'Unable to revoke authentication token.',
+ null,
+ 403,
+ 'TOKEN_REVOKE_UNSUPPORTED'
+ );Committable suggestion skipped: line range outside the PR's diff.
🧰 Tools
🪛 PHPMD (2.15.0)
56-61: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logout'. (undefined)
(StaticAccess)
65-69: Avoid using static access to class '\Log' in method 'logout'. (undefined)
(StaticAccess)
71-76: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'logout'. (undefined)
(StaticAccess)
🤖 Prompt for AI Agents
In app/Http/Controllers/Api/Auth/LogoutController.php around lines 56-76, the
same ApiResponse::error for 'TOKEN_REVOKE_UNSUPPORTED' is duplicated in two
branches; consolidate by returning the error once after handling both cases and
keep distinct logging within each branch. Update the control flow so each branch
only logs its specific warning (failed revoke vs. missing revoke method) and
then fall through to a single shared return of the ApiResponse::error('Unable to
revoke authentication token.', null, 403, 'TOKEN_REVOKE_UNSUPPORTED') placed
after the conditional blocks.
| test('time formatter converts seconds correctly', function (): void { | ||
| // Test seconds | ||
| expect(TimeFormatter::secondsToHuman(30))->toBe('30 seconds'); | ||
| expect(TimeFormatter::secondsToHuman(1))->toBe('1 second'); | ||
|
|
||
| // Test minutes and seconds | ||
| expect(TimeFormatter::secondsToHuman(90))->toBe('1 minute and 30 seconds'); | ||
| expect(TimeFormatter::secondsToHuman(60))->toBe('1 minute'); | ||
|
|
||
| // Test hours, minutes, seconds | ||
| expect(TimeFormatter::secondsToHuman(3661))->toBe('1 hour, 1 minute, and 1 second'); | ||
|
|
||
| // Test days | ||
| expect(TimeFormatter::secondsToHuman(86400))->toBe('1 day'); | ||
| expect(TimeFormatter::secondsToHuman(172800))->toBe('2 days'); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Tests cover the happy path, but where are the edge cases?
You test seconds, minutes, hours, and days. You test pluralization. You test Oxford comma. That's good. But what about:
0 seconds(should return "0 seconds")- Negative input (should throw or handle gracefully)
- Mixed zero values (e.g., 3600 seconds = "1 hour" not "1 hour and 0 minutes")
- Large numbers (e.g., 31536000 = "365 days" - does it handle years?)
Your current implementation probably handles zeros correctly (they're skipped), but test it to prove it.
// Add edge case tests
expect(TimeFormatter::secondsToHuman(0))->toBe('0 seconds');
expect(TimeFormatter::secondsToHuman(3600))->toBe('1 hour'); // No minutes
expect(TimeFormatter::secondsToHuman(3601))->toBe('1 hour and 1 second'); // No minutes🤖 Prompt for AI Agents
In tests/Unit/ExampleTest.php around lines 9–24, add edge-case assertions:
assert secondsToHuman(0) returns "0 seconds"; assert secondsToHuman(3600)
returns "1 hour" (no minutes shown); assert secondsToHuman(3601) returns "1 hour
and 1 second" (no zero minutes); assert secondsToHuman(-1) throws an
InvalidArgumentException (or the project's chosen error type) to guard against
negative input; and assert secondsToHuman(31536000) returns "365 days" (or
adjust to "1 year" if the implementation documents year support) so large-number
behavior is covered.
📚 Automated Documentation SummaryPull Request: das-22-implement-missing-api-routes-author-openapi-specificationThis PR adds comprehensive API controllers, request throttling, an OpenAPI spec generator, standardized JSON responses, and missing middleware. It also updates environment settings and developer guidelines to support these features. Environment ConfigurationNew
# Example
API_THROTTLE_MAX_ATTEMPTS=100
API_THROTTLE_DECAY_MINUTES=1Developer Tools UpdateThe following configuration and agent files now include
Action: CreateNewUser SimplificationRemoved manual validation. Now relies on Fortify’s built‐in rules: // Before: explicit Validator::make() calls
// After: direct User::create(), using PasswordValidationRules trait
return User::create([
'name' => $input['name'],
'email' => $input['email'],
'password' => bcrypt($input['password']),
]);Command: GenerateOpenApiSpec 📜Generates a YAML OpenAPI 3.0.3 spec by scanning
php artisan openapi:generate --output=docs/openapi.yamlHelper: ApiResponse 🎨Standardizes JSON responses across all APIs. use App\Helpers\ApiResponse;
// Success
return ApiResponse::success($data, 'Operation successful', 200);
// Error
return ApiResponse::error('Not found', null, 404, 'NOT_FOUND');
// Validation
return ApiResponse::validationError(['email' => 'Invalid'], 'Validation failed');Middleware: ApiThrottle ⏱️Extends
On 429, returns standardized rate-limit JSON: {
"success": false,
"message": "Too many requests. Please try again later.",
"error_code": "RATE_LIMIT_EXCEEDED",
"data": {
"retry_after_seconds": 60,
"retry_after_human": "1 minute"
}
}API EndpointsBase URL: Deactivate AccountReactivate AccountDelete AccountSend Verification EmailVerify EmailLoginLogoutLogout All DevicesForgot PasswordReset PasswordUpdate PasswordUpdate ProfileRegisterList SessionsRevoke SessionEnable Two-Factor AuthenticationConfirm Two-Factor AuthenticationGet Recovery CodesRegenerate Recovery CodesDisable Two-Factor AuthenticationGet Authenticated UserAll new APIs use consistent response shapes, enforce validation via FormRequest classes, and apply This documentation was automatically generated by DocuWriter.ai 🚀 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
♻️ Duplicate comments (10)
.github/workflows/tests.yml (2)
49-50: Remove frontend builds from a PHP test job.You're running
pnpm install --frozen-lockfile(line 50) andpnpm run build(line 62) inside a job that only executes./vendor/bin/pest(line 68). Unless you're running Dusk E2E tests, this is just burning CI minutes for nothing.If your tests don't need compiled JS:
- - name: Install Node Dependencies - run: pnpm install --frozen-lockfile - - name: Install Dependencies run: composer install --no-interaction --prefer-dist --optimize-autoloader - name: Copy Environment File run: cp .env.example .env - name: Generate Application Key run: php artisan key:generate - - - name: Build Assets - run: pnpm run buildIf you do need frontend artifacts elsewhere (docs, API specs, migrations, etc.), create a separate
buildjob that only installs pnpm and builds, or guard these steps with a conditional:- name: Build Assets if: contains(github.event.head_commit.modified, 'resources/') run: pnpm run buildAlso applies to: 61-62
64-65: ☠️ The migrate step makes the database mismatch problem WORSE, not better.Previous reviews flagged that this workflow runs tests against a different database than where migrations execute:
php artisan migrate --force(line 65) reads.env, which defaults to MySQL (or whatever your production DB is)../vendor/bin/pest(line 68) honorsphpunit.xmland uses sqlite:memory:.Result: You're migrating one database and testing against another. Either:
- The migrate step fails hard (no MySQL service in CI), or
- It "succeeds" but tests are meaningless because schema doesn't match.
Pick ONE of these sane options:
Option A (Recommended): Use RefreshDatabase trait, drop the global migrate
- - name: Run Migrations - run: php artisan migrate --force - - name: Tests run: ./vendor/bin/pestThen in your test base class, use
RefreshDatabasetrait to migrate per test.Option B: Configure DB env vars in the workflow before migrate
- name: Generate Application Key run: php artisan key:generate + + - name: Configure Testing Database + run: | + echo "DB_CONNECTION=sqlite" >> .env + echo "DB_DATABASE=:memory:" >> .env - name: Run Migrations run: php artisan migrate --forceOption C: Run migrate with testing environment
- - name: Run Migrations - run: php artisan migrate --force + - name: Run Migrations + run: php artisan migrate --env=testing --forceThen ensure
config/database.phptesting env uses the same sqlite connection.app/Http/Controllers/Api/Auth/TwoFactorController.php (1)
46-50: Still exposing raw TOTP secret in the response.The past review flagged this - returning
'secret' => $secretmeans if anyone logs API responses (and they will), your 2FA secrets are compromised. The QR code URL is necessary, but the raw secret? That's begging to show up in Splunk.Consider making the secret opt-in via a separate endpoint or a query param, and document that this endpoint's response should NEVER be logged.
tests/Unit/ExampleTest.php (1)
11-26: Missing edge cases from prior review.The happy path is covered, but what about:
0seconds (should return "0 seconds")3600exactly (should be "1 hour" not "1 hour and 0 minutes")- Negative input (should throw or handle gracefully)
The prior review flagged these. Add them to prove your implementation handles boundaries:
// Add these edge case tests expect(TimeFormatter::secondsToHuman(0))->toBe('0 seconds'); expect(TimeFormatter::secondsToHuman(3600))->toBe('1 hour'); // No trailing "and 0 minutes" expect(TimeFormatter::secondsToHuman(3601))->toBe('1 hour and 1 second');storage/postman/api_collection (4)
80-84: Still missingpassword_confirmationin register payload.This was flagged before. If your
RegisterRequestvalidatespassword_confirmation(and it should), this sample will 422 every time.-"raw": "{\n \"name\": \"Test User\",\n \"email\": \"[email protected]\",\n \"password\": \"password123\"\n}", +"raw": "{\n \"name\": \"Test User\",\n \"email\": \"[email protected]\",\n \"password\": \"password123\",\n \"password_confirmation\": \"password123\"\n}",
174-206: GET request with a JSON body is still here. HTTP doesn't work that way.This was called out twice before. The method is GET (correct), but:
- The body is still present (browsers/clients will ignore it)
Content-Type: application/jsonis pointless for GET- The params need to be query parameters, not body
Fix the URL and nuke the body:
"url": { - "raw": "{{base_url}}/api/v1/email/verify", + "raw": "{{base_url}}/api/v1/email/verify?id=1&hash=sample_hash&signature=sample_signature&expires=1234567890", ... + "query": [ + {"key": "id", "value": "1"}, + {"key": "hash", "value": "sample_hash"}, + {"key": "signature", "value": "sample_signature"}, + {"key": "expires", "value": "1234567890"} + ] }, - "body": { - "mode": "raw", - "raw": "{\n \"id\": 5,\n \"hash\": \"sample_text\",\n \"signature\": \"sample_text\",\n \"expires\": 0\n}", - "options": [] - }Also remove the
Content-Typeheader at lines 184-187.
158-162: Reset-password also missingpassword_confirmation.Same issue as register - if your FormRequest requires confirmation, this sample is DOA.
-"raw": "{\n \"token\": \"sample_text\",\n \"email\": \"[email protected]\",\n \"password\": \"password123\"\n}", +"raw": "{\n \"token\": \"sample_text\",\n \"email\": \"[email protected]\",\n \"password\": \"password123\",\n \"password_confirmation\": \"password123\"\n}",
377-380: Password update missingpassword_confirmation.Pattern continues. Your
UpdatePasswordRequestlikely requires confirmation.-"raw": "{\n \"current_password\": \"sample_text\",\n \"password\": \"password123\"\n}", +"raw": "{\n \"current_password\": \"sample_text\",\n \"password\": \"password123\",\n \"password_confirmation\": \"password123\"\n}",tests/Feature/ApiTest.php (2)
1032-1034: Token names still identical.Past review flagged this - both tokens are
'api-token'. The test works because you're revoking by ID, but other tests (like logout-all) use distinct names. Consider'api-token-1'and'api-token-2'for consistency.
1105-1128: Still using fake 2FA secret.Past review flagged this -
'two_factor_secret' => 'test-secret'isn't a valid base32 secret. It works because you're only testing recovery code retrieval, not OTP validation. If someone copies this pattern for a test that needs to validate actual OTPs, they'll have a bad time.Use
'JBSWY3DPEHPK3PXP'(valid base32) for consistency across tests.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
Cache: Disabled due to Reviews > Disable Cache setting
📒 Files selected for processing (8)
.github/workflows/tests.yml(3 hunks)app/Http/Controllers/Api/Auth/TwoFactorController.php(1 hunks)app/Http/Requests/Api/Auth/RegenerateRecoveryCodesRequest.php(1 hunks)app/Http/Requests/Api/Auth/UpdateProfileRequest.php(1 hunks)app/Http/Resources/UserResource.php(1 hunks)storage/postman/api_collection(2 hunks)tests/Feature/ApiTest.php(1 hunks)tests/Unit/ExampleTest.php(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
tests/Feature/ApiTest.php (8)
app/Models/User.php (1)
User(14-70)app/Http/Requests/Api/Auth/UpdateProfileRequest.php (2)
UpdateProfileRequest(8-92)authorize(15-46)app/Http/Requests/Api/Auth/RegenerateRecoveryCodesRequest.php (1)
authorize(15-18)app/Http/Requests/Api/Auth/DisableTwoFactorRequest.php (1)
authorize(15-18)app/Http/Requests/Api/Auth/EnableTwoFactorRequest.php (1)
authorize(15-18)app/Http/Requests/Api/Auth/DeleteAccountRequest.php (1)
authorize(15-18)app/Http/Requests/Api/Auth/VerifyEmailRequest.php (1)
authorize(12-15)app/Http/Requests/Api/Auth/UpdatePasswordRequest.php (1)
authorize(15-18)
tests/Unit/ExampleTest.php (3)
app/Http/Resources/UserResource.php (2)
UserResource(8-24)toArray(15-23)app/Support/TimeFormatter.php (2)
TimeFormatter(5-69)secondsToHuman(23-68)app/Helpers/ApiResponse.php (1)
secondsToHuman(113-116)
app/Http/Requests/Api/Auth/RegenerateRecoveryCodesRequest.php (1)
app/Http/Requests/Api/Auth/UpdateProfileRequest.php (3)
authorize(15-46)rules(53-72)messages(79-91)
app/Http/Controllers/Api/Auth/TwoFactorController.php (4)
app/Helpers/ApiResponse.php (3)
ApiResponse(7-117)error(28-46)success(12-23)app/Http/Requests/Api/Auth/ConfirmTwoFactorRequest.php (1)
ConfirmTwoFactorRequest(7-47)app/Http/Requests/Api/Auth/DisableTwoFactorRequest.php (1)
DisableTwoFactorRequest(7-42)app/Http/Requests/Api/Auth/EnableTwoFactorRequest.php (1)
EnableTwoFactorRequest(7-44)
🪛 PHPMD (2.15.0)
app/Http/Controllers/Api/Auth/TwoFactorController.php
27-27: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'enable'. (undefined)
(StaticAccess)
46-50: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'enable'. (undefined)
(StaticAccess)
61-61: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'confirm'. (undefined)
(StaticAccess)
64-64: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'confirm'. (undefined)
(StaticAccess)
72-72: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'confirm'. (undefined)
(StaticAccess)
84-87: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'confirm'. (undefined)
(StaticAccess)
96-96: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'recoveryCodes'. (undefined)
(StaticAccess)
103-103: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'recoveryCodes'. (undefined)
(StaticAccess)
107-107: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'recoveryCodes'. (undefined)
(StaticAccess)
112-112: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'recoveryCodes'. (undefined)
(StaticAccess)
115-118: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'recoveryCodes'. (undefined)
(StaticAccess)
127-127: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'regenerateRecoveryCodes'. (undefined)
(StaticAccess)
138-141: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'regenerateRecoveryCodes'. (undefined)
(StaticAccess)
150-150: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'disable'. (undefined)
(StaticAccess)
160-160: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'disable'. (undefined)
(StaticAccess)
app/Http/Resources/UserResource.php
15-15: Avoid unused parameters such as '$request'. (undefined)
(UnusedFormalParameter)
🔇 Additional comments (21)
app/Http/Requests/Api/Auth/RegenerateRecoveryCodesRequest.php (2)
25-30: Solid validation for a sensitive operation.Requiring
current_passwordto regenerate recovery codes is the right call—prevents some clown who yoinked an unlocked device from nuking the user's 2FA safety net. Thecurrent_password:sanctumguard is chef's kiss for API routes.
37-43: Clean, no notes.Custom validation messages are clear and user-friendly. This is the unglamorous bread-and-butter work that makes APIs not suck.
app/Http/Requests/Api/Auth/UpdateProfileRequest.php (2)
15-46: Authorization logic is solid - good job addressing the UUID concerns.The string comparison fix from prior reviews is in place. The defensive programming with both route and body ID checks is a nice belt-and-suspenders approach.
79-91: Custom messages are clean.Standard Laravel pattern, nothing fancy but gets the job done.
app/Http/Controllers/Api/Auth/TwoFactorController.php (4)
163-177: Recovery code generation looks good now.8-char alphanumeric with 47+ bits of entropy per code. The math checks out:
62^8 ≈ 218 trillion. Much better UX than raw hex, and the entropy is solid for recovery codes.
21-51: Enable flow is clean aside from the secret exposure.The check for
two_factor_confirmed_atbefore allowing re-enable is good.forceFillwith immediate save is the right call for sensitive fields.
53-88: Confirm flow is solid.Proper DecryptException handling, code verification with 1-window tolerance, and atomic save of recovery codes + confirmation timestamp. Good stuff.
144-161: Disable now requires password - security concern addressed.Using
DisableTwoFactorRequestwith password validation. Proper cleanup of all 2FA fields.app/Http/Resources/UserResource.php (1)
15-23: Date formatting is now explicit - past concern addressed.Using
toISOString()locks down the API contract. The null-safe operator handles unverified users cleanly. The PHPMD warning about unused$requestis a false positive - it's required by Laravel'sJsonResourceinterface.storage/postman/api_collection (1)
948-969: Collection-level auth and variables are properly configured.Bearer token setup with
{{auth_token}}variable. Per-request auth overrides where needed. This is the right pattern for Postman.tests/Feature/ApiTest.php (11)
21-47: LGTM!Clean, focused tests. Unauthenticated gets 401, authenticated gets the goods. No nonsense.
49-94: LGTM!Registration test is thorough - creates user, checks DB, verifies hashing. The
'token' => truetrick on line 68 is a bit galaxy-brained but it works since you're also validating structure below. Password hash verification is chef's kiss.
132-212: LGTM!The login tests are solid now. Love that you fixed the tautological token assertion -
expect($token)->toBeString()->not->toBeEmpty()is the way. Multi-device token test actually proves the tokens are different. Database assertions verify the token names. No complaints.
384-400: Recovery codes validation is actually useful now.Nice - you're checking it's an array, not empty, and each code matches the
^[A-Za-z0-9]{8}$pattern. This actually validates structure instead of just echoing the response back. Good fix.
492-527: LGTM!Password update test does the right thing - changes password, then proves it works by logging in with the new creds. That's the kind of end-to-end validation that actually catches bugs. Wrong password rejection is properly tested too.
590-639: LGTM!Fixed the duplicate device name issue - now using
device1anddevice2. Thelogout-alltest properly creates distinct tokens, verifies they exist, nukes them all, and confirms the apocalypse viaassertDatabaseMissing. Clean.
719-812: Respect for the type juggling test.This is actually important security testing. PHP's loose comparison can let
"0" == 0pass, and testing that yourauthorize()method handles string/int ID comparisons correctly is the kind of paranoid testing that prevents IDOR vulnerabilities. The anonymous class approach is verbose but it isolates the authorization logic perfectly.
865-908: LGTM!Email verification test is solid. You extracted the URL parsing to
getSignedUrlParamshelper as suggested in past reviews. Tests the full flow: generate signed URL, extract params, verify endpoint, confirmemail_verified_atis set. Error cases covered too.
1183-1195: Good use of the constant.
TwoFactorController::RECOVERY_CODE_COUNTinstead of magic9. This keeps tests and implementation in sync. If someone changes the count, both break together instead of silently diverging. That's the way.
1534-1542: LGTM!Fixed the weird "query relationships on a deleted model" issue. Now you're doing a proper direct query against
PersonalAccessTokentable instead of calling$user->tokens()on a ghost. Much cleaner and semantically correct.
1335-1366: LGTM!Account deactivation test is thorough - verifies the flag is set, reason is saved, timestamp exists, and all tokens are nuked. Good coverage of the business logic.
| public function recoveryCodes(Request $request): JsonResponse | ||
| { | ||
| $user = $request->user(); | ||
|
|
||
| // Check if 2FA is enabled | ||
| if (! $user->two_factor_confirmed_at) { | ||
| return ApiResponse::error('Two-factor authentication is not enabled.', null, 400, 'TWO_FACTOR_NOT_ENABLED'); | ||
| } | ||
|
|
||
| try { | ||
| // Recovery codes are automatically decrypted by the encrypted cast | ||
| $recoveryCodes = $user->two_factor_recovery_codes; | ||
| } catch (DecryptException) { | ||
| return ApiResponse::error('Unable to decrypt recovery codes. The data may be corrupted.', null, 422, 'RECOVERY_CODES_DECRYPTION_FAILED'); | ||
| } | ||
|
|
||
| if (! $recoveryCodes) { | ||
| return ApiResponse::error('No recovery codes available.', null, 400, 'NO_RECOVERY_CODES'); | ||
| } | ||
|
|
||
| $decodedCodes = json_decode($recoveryCodes, true); | ||
| if (json_last_error() !== JSON_ERROR_NONE) { | ||
| return ApiResponse::error('Recovery codes data is corrupted.', null, 422, 'RECOVERY_CODES_CORRUPTED'); | ||
| } | ||
|
|
||
| return ApiResponse::success([ | ||
| 'recovery_codes' => $decodedCodes, | ||
| 'warning' => 'Make sure to save your recovery codes in a secure place.', | ||
| ], 'Recovery codes retrieved successfully.'); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
recoveryCodes() is the odd one out - no password verification.
You've got EnableTwoFactorRequest, ConfirmTwoFactorRequest, DisableTwoFactorRequest, and RegenerateRecoveryCodesRequest all requiring current_password. But viewing recovery codes? Just need a session.
An attacker with session access can view (and screenshot/copy) recovery codes without any additional verification. This is arguably as sensitive as regenerating them.
Create a ViewRecoveryCodesRequest with password validation:
- public function recoveryCodes(Request $request): JsonResponse
+ public function recoveryCodes(ViewRecoveryCodesRequest $request): JsonResponseCommittable suggestion skipped: line range outside the PR's diff.
🧰 Tools
🪛 PHPMD (2.15.0)
96-96: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'recoveryCodes'. (undefined)
(StaticAccess)
103-103: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'recoveryCodes'. (undefined)
(StaticAccess)
107-107: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'recoveryCodes'. (undefined)
(StaticAccess)
112-112: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'recoveryCodes'. (undefined)
(StaticAccess)
115-118: Avoid using static access to class '\App\Helpers\ApiResponse' in method 'recoveryCodes'. (undefined)
(StaticAccess)
🤖 Prompt for AI Agents
In app/Http/Controllers/Api/Auth/TwoFactorController.php around lines 90 to 119,
the recoveryCodes() action returns sensitive recovery codes without re-checking
the user's current password; create a new Form Request (e.g.,
ViewRecoveryCodesRequest) that validates the current_password against the
authenticated user (same rules as the other two-factor requests), change the
controller method signature to accept that request instead of generic Request,
import the new request class, and rely on its validated authorization to ensure
only users who re-enter their password can retrieve recovery codes; also update
any route or controller binding to use the new request.
| public function authorize(): bool | ||
| { | ||
| return auth()->check(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
rg -n "regenerate.*recovery|recovery.*regenerate" routes/ -B 5 -A 5Repository: dash-soft/DashOps
Length of output: 1278
🏁 Script executed:
rg -n "auth:sanctum" routes/ -B 2 -A 2Repository: dash-soft/DashOps
Length of output: 520
🏁 Script executed:
sed -n '20,60p' routes/api.phpRepository: dash-soft/DashOps
Length of output: 2708
The "defense in depth" framing is overstated, but the code is fine.
The regenerate recovery codes route is already wrapped in the auth:sanctum middleware group (routes/api.php:26), so auth()->check() in the authorize() method is redundant—you're checking that the middleware did its job. That's not really "defense in depth"; it's just belt and suspenders on the same defensive layer.
That said, for this use case it's perfectly acceptable. Recovery code regeneration operates on the authenticated user implicitly (no route parameter to validate), and the extra auth()->check() adds negligible overhead while providing a safety net if something goes wrong with middleware configuration.
🤖 Prompt for AI Agents
In app/Http/Requests/Api/Auth/RegenerateRecoveryCodesRequest.php around lines 15
to 18, the authorize() method redundantly calls auth()->check() even though the
route is already protected by auth:sanctum; replace the auth()->check() call
with a simple return true (or remove the method entirely if not needed) so the
request class doesn't re-check middleware-level authentication, keeping the
route protection responsibility with the middleware.
| public function rules(): array | ||
| { | ||
| $rules = [ | ||
| 'name' => ['required', 'string', 'max:255'], | ||
| 'email' => [ | ||
| 'required', | ||
| 'string', | ||
| 'email', | ||
| 'max:255', | ||
| Rule::unique('users')->ignore($this->user()?->id), | ||
| ], | ||
| ]; | ||
|
|
||
| // Require current password when changing email for security | ||
| if ($this->has('email') && $this->email !== $this->user()?->email) { | ||
| $rules['current_password'] = ['required', 'string', 'current_password:sanctum']; | ||
| } | ||
|
|
||
| return $rules; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Minor inconsistency: ->id vs ->getKey() usage.
You're using getKey() for comparisons in authorize() but ->id in the unique rule ignore. If you ever switch to UUIDs with a different primary key name, this could bite you.
'email' => [
'required',
'string',
'email',
'max:255',
- Rule::unique('users')->ignore($this->user()?->id),
+ Rule::unique('users')->ignore($this->user()?->getKey()),
],🤖 Prompt for AI Agents
In app/Http/Requests/Api/Auth/UpdateProfileRequest.php around lines 53 to 72,
the unique rule currently ignores the user by ->id which is inconsistent with
authorize() using getKey(); change the ignore value to use
$this->user()?->getKey() (preserving the null-safe operator) so the rule works
correctly with non-standard PK names (e.g., UUIDs) and remains consistent across
the class.
| function getSignedUrlParams(string $signedUrl): array | ||
| { | ||
| $urlParts = parse_url($signedUrl); | ||
| parse_str($urlParts['query'], $queryParams); | ||
|
|
||
| return $queryParams; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Helper could blow up on malformed URLs.
If someone passes a URL without a query string, $urlParts['query'] will be undefined and parse_str gets null. In test code this is probably fine since you're always passing signed URLs, but a null coalesce would be trivially defensive:
function getSignedUrlParams(string $signedUrl): array
{
$urlParts = parse_url($signedUrl);
- parse_str($urlParts['query'], $queryParams);
+ parse_str($urlParts['query'] ?? '', $queryParams);
return $queryParams;
}🤖 Prompt for AI Agents
In tests/Feature/ApiTest.php around lines 13-19, the helper assumes
$urlParts['query'] exists and will pass null to parse_str for malformed URLs;
change it to defensively handle missing query by using a null-coalescing default
(e.g. $urlParts['query'] ?? '') when calling parse_str, and ensure $queryParams
is initialized or return an empty array when no query string is present.
| $response->assertOk() | ||
| ->assertJson([ | ||
| 'success' => true, | ||
| 'message' => 'Two-factor authentication setup initiated.', | ||
| 'data' => [ | ||
| 'secret' => $response->json('data.secret'), | ||
| 'qr_code_url' => $response->json('data.qr_code_url'), | ||
| 'next_step' => 'Scan the QR code with your authenticator app and confirm with the generated code.', | ||
| ], | ||
| 'errors' => null, | ||
| ]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Self-referential assertions strike again.
Lines 266-267 assert that secret and qr_code_url equal... themselves. This passes for literally any non-null value. You're not validating anything meaningful here.
$response->assertOk()
->assertJson([
'success' => true,
'message' => 'Two-factor authentication setup initiated.',
'data' => [
- 'secret' => $response->json('data.secret'),
- 'qr_code_url' => $response->json('data.qr_code_url'),
'next_step' => 'Scan the QR code with your authenticator app and confirm with the generated code.',
],
'errors' => null,
- ]);
+ ])
+ ->assertJsonPath('data.secret', fn ($s) => is_string($s) && strlen($s) >= 16)
+ ->assertJsonPath('data.qr_code_url', fn ($url) => str_starts_with($url, 'otpauth://'));| test('user resource formats email_verified_at correctly', function (): void { | ||
| $mockRequest = new \Illuminate\Http\Request; | ||
|
|
||
| // Create a mock user with null email_verified_at | ||
| $unverifiedUser = new class | ||
| { | ||
| public $id = 1; | ||
|
|
||
| public $name = 'Test User'; | ||
|
|
||
| public $email = '[email protected]'; | ||
|
|
||
| public $email_verified_at = null; | ||
| }; | ||
|
|
||
| $resource = UserResource::make($unverifiedUser); | ||
| $data = $resource->toArray($mockRequest); | ||
|
|
||
| expect($data['email_verified_at'])->toBeNull(); | ||
|
|
||
| // Create a mock user with verified email | ||
| $verifiedAt = now(); | ||
| $verifiedUser = new class($verifiedAt) | ||
| { | ||
| public $id = 2; | ||
|
|
||
| public $name = 'Verified User'; | ||
|
|
||
| public $email = '[email protected]'; | ||
|
|
||
| public $email_verified_at; | ||
|
|
||
| public function __construct($verifiedAt) | ||
| { | ||
| $this->email_verified_at = $verifiedAt; | ||
| } | ||
| }; | ||
|
|
||
| $resource = UserResource::make($verifiedUser); | ||
| $data = $resource->toArray($mockRequest); | ||
|
|
||
| expect($data['email_verified_at'])->toBeString(); | ||
| expect($data['email_verified_at'])->toMatch('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/'); // ISO8601 format |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
UserResource tests are solid.
Anonymous classes for mocks is a bit unconventional but works. The ISO8601 regex validation is a smart way to verify format without being brittle to exact timestamps.
One small improvement - use Carbon::createFromTimestamp(0) or a fixed date for deterministic tests instead of now().
- $verifiedAt = now();
+ $verifiedAt = \Carbon\Carbon::parse('2024-01-15T10:30:00Z');🤖 Prompt for AI Agents
In tests/Unit/ExampleTest.php around lines 28 to 70, the test uses now() to set
$verifiedAt which makes the test non-deterministic; replace the dynamic now()
with a fixed deterministic Carbon instance (e.g. Carbon::createFromTimestamp(0)
or Carbon::parse('2020-01-01 00:00:00')) and update the mock creation to use
that fixed value so assertions against the ISO8601 formatted string are stable
across runs.

No description provided.