Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
afc7aa0
Fix OPS family import when applicants/inventors are missing
srdco Apr 23, 2026
280eac9
Merge pull request #16 from srdco/codex/fix-error-when-creating-matte…
srdco Apr 23, 2026
cbab030
Fix MySQL SSL option constant for PHP compatibility
srdco Apr 23, 2026
0ed826f
Merge pull request #17 from srdco/codex/fix-error-when-creating-matte…
srdco Apr 23, 2026
5da49a0
Replace Auth::routes macro with explicit auth routes
srdco Apr 23, 2026
bc4199d
Merge pull request #18 from srdco/codex/fix-error-when-creating-matte…
srdco Apr 23, 2026
e0db37e
Add USPTO ODP setup and usage manual
srdco Apr 23, 2026
e34c4a9
Clarify ODP manual scope and remove PDO troubleshooting
srdco Apr 23, 2026
0f79768
Refocus USPTO guide on end-user usage only
srdco Apr 23, 2026
9aa9000
Merge pull request #19 from srdco/codex/fix-error-when-creating-matte…
srdco Apr 23, 2026
c671652
Merge branch 'jjdejong:master' into master
srdco Apr 23, 2026
7fb0096
Default USPTO ODP endpoints and remove mandatory endpoint setup
srdco Apr 23, 2026
4540049
Merge branch 'master' into codex/fix-error-when-creating-matter-from-…
srdco Apr 23, 2026
f50ddcf
Merge pull request #20 from srdco/codex/fix-error-when-creating-matte…
srdco Apr 23, 2026
05b0f3a
Make login controller version-agnostic across Laravel releases
srdco Apr 24, 2026
0db3e67
Merge pull request #21 from srdco/codex/fix-login-error-in-logincontr…
srdco Apr 24, 2026
64b54fe
Handle duplicate matter-actor links when adding actors
srdco May 5, 2026
85fce5f
Merge pull request #22 from srdco/codex/fix-unique-constraint-violati…
srdco May 5, 2026
e04d802
Merge pull request #23 from jjdejong/master
srdco Jul 1, 2026
ef160d9
Add GitHub Actions workflow for OpenCode
srdco Jul 1, 2026
594d72c
Delete .github directory
srdco Jul 1, 2026
e2e8702
Document USPTO ODP beta status
srdco Jul 1, 2026
71a4319
Merge pull request #24 from srdco/codex/update-readme-and-wiki-for-ne…
srdco Jul 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion app/Http/Controllers/ActorPivotController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use App\Models\Actor;
use App\Models\ActorPivot;
use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
Expand Down Expand Up @@ -58,7 +59,14 @@ public function store(Request $request)
'date' => Now(),
]);

return ActorPivot::create($request->except(['_token', '_method']));
try {
return ActorPivot::create($request->except(['_token', '_method']));
} catch (UniqueConstraintViolationException $exception) {
return ActorPivot::where('matter_id', $request->matter_id)
->where('role', $request->role)
->where('actor_id', $request->actor_id)
->firstOrFail();
}
}

/**
Expand Down
169 changes: 151 additions & 18 deletions app/Http/Controllers/Auth/LoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,20 @@
namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;

/**
* Handles user authentication and login.
*
* Uses Laravel's AuthenticatesUsers trait to provide standard login functionality.
* Configured to use the 'login' field instead of 'email' for authentication.
* This implementation intentionally avoids framework-internal auth traits so it
* remains stable across Laravel versions.
*/
class LoginController extends Controller
{
/*
|--------------------------------------------------------------------------
| Login Controller
|--------------------------------------------------------------------------
|
| This controller handles authenticating users for the application and
| redirecting them to your home screen. The controller uses a trait
| to conveniently provide its functionality to your applications.
|
*/

use AuthenticatesUsers;

/**
* Where to redirect users after login.
*
Expand All @@ -45,14 +36,156 @@ public function __construct()
}

/**
* Get the login username field.
* Display the login form.
*
* @return \Illuminate\View\View
*/
public function showLoginForm()
{
return view('auth.login');
}

/**
* Handle an authentication attempt.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Illuminate\Validation\ValidationException
*/
public function login(Request $request)
{
$this->validateLogin($request);
$this->ensureIsNotRateLimited($request);

if (! Auth::attempt($this->credentials($request), $request->has('remember'))) {
RateLimiter::hit($this->throttleKey($request));

return $this->sendFailedLoginResponse($request);
}
Comment on lines +56 to +65

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The manual implementation of the login logic lacks rate limiting, which was previously provided by the AuthenticatesUsers trait (via the ThrottlesLogins trait it uses). This leaves the login endpoint vulnerable to brute-force attacks. Since you are avoiding framework traits for stability, consider implementing rate limiting manually using Laravel's RateLimiter facade within the login method to maintain security parity.


RateLimiter::clear($this->throttleKey($request));
$request->session()->regenerate();

if (method_exists($this, 'authenticated')) {
$response = $this->authenticated($request, Auth::user());

if ($response) {
return $response;
}
}

return redirect()->intended($this->redirectPath());
}

/**
* Log the user out of the application.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse
*/
public function logout(Request $request)
{
Auth::logout();

$request->session()->invalidate();
$request->session()->regenerateToken();

return redirect('/');
}

/**
* Validate the user login request.
*
* @param \Illuminate\Http\Request $request
* @return void
*/
protected function validateLogin(Request $request)
{
$request->validate([
$this->username() => ['required', 'string'],
'password' => ['required', 'string'],
]);
}

/**
* Get the needed authorization credentials from the request.
*
* Uses the 'login' column instead of Laravel's default 'email' field.
* @param \Illuminate\Http\Request $request
* @return array
*/
protected function credentials(Request $request)
{
return $request->only($this->username(), 'password');
}

/**
* Get the login username field.
*
* @return string
*/
public function username()
{
return 'login';
}

/**
* Get the post-login redirect path.
*
* @return string
*/
protected function redirectPath()
{
return property_exists($this, 'redirectTo') ? $this->redirectTo : '/home';
}

/**
* Get the failed login response instance.
*
* @param \Illuminate\Http\Request $request
* @return never
*
* @throws \Illuminate\Validation\ValidationException
*/
protected function sendFailedLoginResponse(Request $request)
{
throw ValidationException::withMessages([
$this->username() => [trans('auth.failed')],
]);
}

/**
* Ensure the login request has not exceeded the allowed attempt count.
*
* @param \Illuminate\Http\Request $request
* @return void
*
* @throws \Illuminate\Validation\ValidationException
*/
protected function ensureIsNotRateLimited(Request $request)
{
if (! RateLimiter::tooManyAttempts($this->throttleKey($request), 5)) {
return;
}

$seconds = RateLimiter::availableIn($this->throttleKey($request));

throw ValidationException::withMessages([
$this->username() => [trans('auth.throttle', [
'seconds' => $seconds,
'minutes' => ceil($seconds / 60),
])],
])->status(429);
}

/**
* Get the rate limiting key for the request.
*
* @param \Illuminate\Http\Request $request
* @return string
*/
protected function throttleKey(Request $request)
{
return Str::lower($request->input($this->username(), '')) . '|' . $request->ip();
}
}
18 changes: 9 additions & 9 deletions app/Http/Controllers/MatterController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
use App\Models\ActorPivot;
use App\Models\Matter;
use App\Services\DocumentMergeService;
use App\Services\FamilyDataService;
use App\Services\MatterExportService;
use App\Services\OPSService;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
Expand All @@ -27,24 +27,24 @@ class MatterController extends Controller
{
protected DocumentMergeService $documentMergeService;
protected MatterExportService $matterExportService;
protected OPSService $opsService;
protected FamilyDataService $familyDataService;

/**
* Initialize the controller with required services.
*
* @param DocumentMergeService $documentMergeService Service for merging matter data into documents.
* @param MatterExportService $matterExportService Service for exporting matters to CSV.
* @param OPSService $opsService Service for interacting with EPO OPS API.
* @param FamilyDataService $familyDataService Service for retrieving family data from OPS/USPTO.
*/
public function __construct(
DocumentMergeService $documentMergeService,
MatterExportService $matterExportService,
OPSService $opsService
FamilyDataService $familyDataService
)
{
$this->documentMergeService = $documentMergeService;
$this->matterExportService = $matterExportService;
$this->opsService = $opsService;
$this->familyDataService = $familyDataService;
}

/**
Expand Down Expand Up @@ -378,7 +378,7 @@ public function storeFamily(Request $request)
'client_id' => 'required',
]);

$apps = collect($this->opsService->getFamilyMembers($request->docnum));
$apps = collect($this->familyDataService->getFamilyMembers($request->docnum));
if ($apps->has('errors') || $apps->has('exception')) {
return response()->json($apps);
}
Expand Down Expand Up @@ -463,7 +463,7 @@ public function storeFamily(Request $request)
$new_matter->classifiersNative()->create(['type_code' => 'TIT', 'value' => $app['title']]);
}
$new_matter->actorPivot()->create(['actor_id' => $request->client_id, 'role' => 'CLI', 'shared' => 1]);
if (array_key_exists('applicants', $app)) {
if (array_key_exists('applicants', $app) && !empty($app['applicants'])) {
if (strtolower($app['applicants'][0]) == strtolower(Actor::find($request->client_id)->name)) {
$new_matter->actorPivot()->create(
[
Expand Down Expand Up @@ -508,7 +508,7 @@ public function storeFamily(Request $request)
}
$new_matter->notes = 'Applicants: ' . collect($app['applicants'])->implode('; ');
}
if (array_key_exists('inventors', $app)) {
if (array_key_exists('inventors', $app) && !empty($app['inventors'])) {
foreach ($app['inventors'] as $inventor) {
// Search for phonetically equivalent in the actor table, and take first
if (substr($inventor, -1) == ',') {
Expand Down Expand Up @@ -815,7 +815,7 @@ public function mergeFile(Matter $matter, MergeFileRequest $request)
*/
public function getOPSfamily(string $docnum)
{
return $this->opsService->getFamilyMembers($docnum);
return $this->familyDataService->getFamilyMembers($docnum);
}

/**
Expand Down
80 changes: 80 additions & 0 deletions app/Services/FamilyDataService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

namespace App\Services;

/**
* Family data orchestrator with dynamic provider selection.
*
* Default behavior uses OPS for full family retrieval, then enriches US members
* from USPTO ODP when enabled/configured.
*/
class FamilyDataService
{
public function __construct(
private OPSService $opsService,
private USPTOService $usptoService
) {
}

/**
* Get family members with OPS primary source and USPTO fallback/enrichment.
*
* @param string $docnum
* @return array
*/
public function getFamilyMembers(string $docnum): array
{
$apps = $this->opsService->getFamilyMembers($docnum);
if (array_key_exists('errors', $apps) || array_key_exists('exception', $apps)) {
// If the requested number looks US, return a synthetic single-member family
// from USPTO ODP when possible.
if ($this->isUSDocument($docnum)) {
$member = $this->buildUSMemberFromODP($docnum);
if (!empty($member)) {
return [$member];
}
}

return $apps;
}

return $this->usptoService->enrichFamilyMembers($apps);
}

private function isUSDocument(string $docnum): bool
{
return str_starts_with(strtoupper(trim($docnum)), 'US');
}

private function buildUSMemberFromODP(string $docnum): array
{
$number = preg_replace('/\D/', '', $docnum);
if (!$number) {
return [];
}

$odData = $this->usptoService->getApplicationData($number);
if (empty($odData)) {
return [];
}

return [
'id' => 'US' . $number,
'app' => [
'country' => 'US',
'number' => ltrim($number, '0'),
'kind' => 'A',
'date' => null,
],
'pri' => [],
'pct' => null,
'div' => null,
'cnt' => null,
'title' => $odData['title'] ?? null,
'applicants' => $odData['applicants'] ?? [],
'inventors' => $odData['inventors'] ?? [],
'procedure' => $odData['procedure'] ?? [],
];
}
}

Loading