-
Notifications
You must be signed in to change notification settings - Fork 29
Stability fixes, Laravel compatibility updates, EPO OPS improvements & initial implementation of USPTO ODP #180
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?
Changes from all commits
afc7aa0
280eac9
cbab030
0ed826f
5da49a0
bc4199d
e0db37e
e34c4a9
0f79768
9aa9000
c671652
7fb0096
4540049
f50ddcf
05b0f3a
0db3e67
64b54fe
85fce5f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,29 +3,18 @@ | |
| 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\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. | ||
| * | ||
|
|
@@ -45,14 +34,117 @@ 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); | ||
|
|
||
| if (! Auth::attempt($this->credentials($request), $request->has('remember'))) { | ||
| return $this->sendFailedLoginResponse($request); | ||
| } | ||
|
Comment on lines
+54
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The manual implementation of the login logic lacks rate limiting, which was previously provided by the |
||
|
|
||
| $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')], | ||
| ]); | ||
| } | ||
| } | ||
| 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'] ?? [], | ||
| ]; | ||
| } | ||
| } | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -165,14 +165,28 @@ public function getFamilyMembers(string $docnum): array | |
| ->last()['$']; | ||
|
|
||
| // Each inventor is under [i]['inventor-name']['name']['$'] both in "epodoc" and "original" format | ||
| $inventors = collect($member[0]['exchange-document']['bibliographic-data']['parties']['inventors']['inventor']) | ||
| ->where('@data-format', 'original'); | ||
| $apps[0]['inventors'] = $inventors->values()->pluck('inventor-name.name.$'); | ||
| $inventors = collect($member[0]['exchange-document']['bibliographic-data']['parties']['inventors']['inventor'] ?? []) | ||
| ->where('@data-format', 'original') | ||
| ->values() | ||
| ->pluck('inventor-name.name.$') | ||
| ->filter() | ||
| ->values() | ||
| ->all(); | ||
|
Comment on lines
+168
to
+174
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The EPO OPS API may return a single object instead of an array when there is only one inventor. In such cases, $inventorData = $member[0]['exchange-document']['bibliographic-data']['parties']['inventors']['inventor'] ?? [];
$inventors = collect(Arr::isList($inventorData) ? $inventorData : [$inventorData])
->where('@data-format', 'original')
->values()
->pluck('inventor-name.name.$')
->filter()
->values()
->all(); |
||
| if (!empty($inventors)) { | ||
| $apps[0]['inventors'] = $inventors; | ||
| } | ||
|
|
||
| // Each applicant is under [i]['applicant-name']['name']['$'] | ||
| $applicants = collect($member[0]['exchange-document']['bibliographic-data']['parties']['applicants']['applicant']) | ||
| ->where('@data-format', 'original'); | ||
| $apps[0]['applicants'] = $applicants->values()->pluck('applicant-name.name.$'); | ||
| $applicants = collect($member[0]['exchange-document']['bibliographic-data']['parties']['applicants']['applicant'] ?? []) | ||
| ->where('@data-format', 'original') | ||
| ->values() | ||
| ->pluck('applicant-name.name.$') | ||
| ->filter() | ||
| ->values() | ||
| ->all(); | ||
|
Comment on lines
+180
to
+186
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to the inventors collection, the applicants data can also be returned as a single object by the API. It should be normalized to a list before filtering to avoid issues with single-applicant patents. $applicantData = $member[0]['exchange-document']['bibliographic-data']['parties']['applicants']['applicant'] ?? [];
$applicants = collect(Arr::isList($applicantData) ? $applicantData : [$applicantData])
->where('@data-format', 'original')
->values()
->pluck('applicant-name.name.$')
->filter()
->values()
->all(); |
||
| if (!empty($applicants)) { | ||
| $apps[0]['applicants'] = $applicants; | ||
| } | ||
|
|
||
| $procedureSteps = $this->getProceduralSteps($app_number); | ||
| if (!empty($procedureSteps)) { | ||
|
|
||
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.
This check introduces a "look-before-you-leap" pattern which is susceptible to race conditions. While it reduces the likelihood of duplicate entries, it doesn't prevent them if two requests occur simultaneously. To truly handle unique constraint violations gracefully as mentioned in the PR description, consider wrapping the eventual insertion logic in a
try-catchblock forUniqueConstraintViolationExceptionor using atomic operations likefirstOrCreateif the subsequent logic allows.