diff --git a/.env.example b/.env.example index 2d7f02fd..c3d44a68 100644 --- a/.env.example +++ b/.env.example @@ -57,3 +57,13 @@ VITE_PUSHER_HOST="${PUSHER_HOST}" VITE_PUSHER_PORT="${PUSHER_PORT}" VITE_PUSHER_SCHEME="${PUSHER_SCHEME}" VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" + +# CMS (ChemInformatics Microservice) API Configuration +# API_URL: Internal/Docker network URL for backend requests +# CM_PUBLIC_API: Public URL for frontend/proxy requests +# CMS_INTERNAL_AUTH_TOKEN: Authentication token to bypass rate limits +# CMS_RATE_LIMIT: Maximum requests per minute to /cms/depict2d endpoint (default: 200, set to 0 to disable) +API_URL=https://api.cheminf.studio/latest/ +CM_PUBLIC_API=https://api.cheminf.studio/latest/ +CMS_INTERNAL_AUTH_TOKEN= +CMS_RATE_LIMIT=200 diff --git a/app/Console/Commands/GenerateCoordinates.php b/app/Console/Commands/GenerateCoordinates.php index a0a55489..155e5348 100644 --- a/app/Console/Commands/GenerateCoordinates.php +++ b/app/Console/Commands/GenerateCoordinates.php @@ -5,9 +5,9 @@ use App\Models\Collection; use App\Models\Molecule; use App\Models\Structure; +use App\Services\CmsClient; use Illuminate\Console\Command; use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; use Throwable; @@ -63,21 +63,18 @@ public function handle() $progressBar->start(); // Process molecules in chunks using the static list of IDs. - $moleculeIds->chunk(10000)->each(function ($idsChunk) use ($progressBar) { + $cmsClient = app(CmsClient::class); + + $moleculeIds->chunk(10000)->each(function ($idsChunk) use ($progressBar, $cmsClient) { $mols = Molecule::whereIn('id', $idsChunk)->select('id', 'canonical_smiles')->get(); $data = []; foreach ($mols as $mol) { $id = $mol->id; $canonical_smiles = $mol->canonical_smiles; - // Build endpoints. - $apiUrl = env('API_URL', 'https://api.cheminf.studio/latest/'); - $d2Endpoint = $apiUrl.'convert/mol2D?smiles='.urlencode($canonical_smiles).'&toolkit=rdkit'; - $d3Endpoint = $apiUrl.'convert/mol3D?smiles='.urlencode($canonical_smiles).'&toolkit=rdkit'; - // Fetch coordinates from API. - $d2 = $this->fetchFromApi($d2Endpoint, $canonical_smiles); - $d3 = $this->fetchFromApi($d3Endpoint, $canonical_smiles); + $d2 = $this->fetchFromApi($cmsClient, 'convert/mol2D', $canonical_smiles); + $d3 = $this->fetchFromApi($cmsClient, 'convert/mol3D', $canonical_smiles); // Accumulate data for batch insertion. $data[] = [ @@ -103,9 +100,9 @@ public function handle() /** * Make an HTTP GET request with basic retry/backoff handling (e.g. 429 Too Many Requests). * - * @return mixed (array|null) Returns the JSON-decoded response or null on failure. + * @return mixed (string|null) Returns the response body or null on failure. */ - private function fetchFromApi(string $endpoint, string $smiles) + private function fetchFromApi(CmsClient $cmsClient, string $endpoint, string $smiles) { $maxRetries = 3; $attempt = 0; @@ -113,7 +110,10 @@ private function fetchFromApi(string $endpoint, string $smiles) while ($attempt < $maxRetries) { try { - $response = Http::timeout(600)->get($endpoint); + $response = $cmsClient->get($endpoint, [ + 'smiles' => $smiles, + 'toolkit' => 'rdkit', + ], false); if ($response->successful()) { return $response->body(); diff --git a/app/Console/Commands/SubmissionsAutoProcess/FetchCitationMetadataAuto.php b/app/Console/Commands/SubmissionsAutoProcess/FetchCitationMetadataAuto.php index dd5ead55..3d8fc55a 100644 --- a/app/Console/Commands/SubmissionsAutoProcess/FetchCitationMetadataAuto.php +++ b/app/Console/Commands/SubmissionsAutoProcess/FetchCitationMetadataAuto.php @@ -146,7 +146,7 @@ private function fetchCitationFromAPIs($doi): ?array $citationResponse = null; // Try EuropePMC first - $europemcUrl = env('EUROPEPMC_WS_API'); + $europemcUrl = config('services.citation.europepmc_url'); $europemcParams = [ 'query' => 'DOI:'.$doi, 'format' => 'json', @@ -161,14 +161,14 @@ private function fetchCitationFromAPIs($doi): ?array $citationResponse = $this->formatCitationResponse($europemcResponse['resultList']['result'][0], 'europemc'); } else { // Try CrossRef - $crossrefUrl = env('CROSSREF_WS_API').$doi; + $crossrefUrl = config('services.citation.crossref_url').$doi; $response = $this->makeRequest($crossrefUrl); $crossrefResponse = ($response && method_exists($response, 'json')) ? $response->json() : null; if ($crossrefResponse && isset($crossrefResponse['message'])) { $citationResponse = $this->formatCitationResponse($crossrefResponse['message'], 'crossref'); } else { // Try DataCite as last resort - $dataciteUrl = env('DATACITE_WS_API').$doi; + $dataciteUrl = config('services.citation.datacite_url').$doi; $response = $this->makeRequest($dataciteUrl); $dataciteResponse = ($response && method_exists($response, 'json')) ? $response->json() : null; if ($dataciteResponse && isset($dataciteResponse['data'])) { diff --git a/app/Filament/Dashboard/Resources/CollectionResource/RelationManagers/EntriesRelationManager.php b/app/Filament/Dashboard/Resources/CollectionResource/RelationManagers/EntriesRelationManager.php index 9cef396f..7d1569a2 100644 --- a/app/Filament/Dashboard/Resources/CollectionResource/RelationManagers/EntriesRelationManager.php +++ b/app/Filament/Dashboard/Resources/CollectionResource/RelationManagers/EntriesRelationManager.php @@ -99,21 +99,21 @@ public function infolist(Infolist $infolist): Infolist ]) ->schema([ ImageEntry::make('parent_canonical_smiles')->state(function ($record) { - return env('CM_PUBLIC_API', 'https://api.cheminf.studio/latest/').'depict/2D?smiles='.urlencode($record->parent_canonical_smiles).'&height=300&width=300&CIP=true&toolkit=cdk'; + return getDepictUrl($record->parent_canonical_smiles, 300, 300, 'cdk', true); }) ->width(200) ->height(200) ->ring(5) ->defaultImageUrl(url('/images/placeholder.png')), ImageEntry::make('canonical_smiles')->state(function ($record) { - return env('CM_PUBLIC_API', 'https://api.cheminf.studio/latest/').'depict/2D?smiles='.urlencode($record->canonical_smiles).'&height=300&width=300&CIP=true&toolkit=cdk'; + return getDepictUrl($record->canonical_smiles, 300, 300, 'cdk', true); }) ->width(200) ->height(200) ->ring(5) ->defaultImageUrl(url('/images/placeholder.png')), ImageEntry::make('standardized_canonical_smiles')->state(function ($record) { - return env('CM_PUBLIC_API', 'https://api.cheminf.studio/latest/').'depict/2D?smiles='.urlencode($record->standardized_canonical_smiles).'&height=300&width=300&CIP=true&toolkit=cdk'; + return getDepictUrl($record->standardized_canonical_smiles, 300, 300, 'cdk', true); }) ->width(200) ->height(200) @@ -133,7 +133,7 @@ public function table(Table $table): Table ImageColumn::make('structure')->square() ->label('Structure') ->state(function ($record) { - return env('CM_PUBLIC_API', 'https://api.cheminf.studio/latest/').'depict/2D?smiles='.urlencode($record->canonical_smiles).'&height=300&width=300&CIP=true&toolkit=cdk'; + return getDepictUrl($record->canonical_smiles, 300, 300, 'cdk', true); }) ->width(200) ->height(200) diff --git a/app/Filament/Dashboard/Resources/MoleculeResource.php b/app/Filament/Dashboard/Resources/MoleculeResource.php index 3c7041a6..46b31332 100644 --- a/app/Filament/Dashboard/Resources/MoleculeResource.php +++ b/app/Filament/Dashboard/Resources/MoleculeResource.php @@ -90,7 +90,7 @@ public static function table(Table $table): Table ImageColumn::make('structure')->square() ->label('Structure') ->state(function ($record) { - return env('CM_PUBLIC_API', 'https://api.cheminf.studio/latest/').'depict/2D?smiles='.urlencode($record->canonical_smiles).'&height=300&width=300&CIP=true&toolkit=cdk'; + return getDepictUrl($record->canonical_smiles, 300, 300, 'cdk', true); }) ->width(200) ->height(200) diff --git a/app/Filament/Dashboard/Resources/MoleculeResource/RelationManagers/MoleculesRelationManager.php b/app/Filament/Dashboard/Resources/MoleculeResource/RelationManagers/MoleculesRelationManager.php index b297563e..88516d97 100644 --- a/app/Filament/Dashboard/Resources/MoleculeResource/RelationManagers/MoleculesRelationManager.php +++ b/app/Filament/Dashboard/Resources/MoleculeResource/RelationManagers/MoleculesRelationManager.php @@ -26,7 +26,7 @@ public function table(Table $table): Table ImageColumn::make('structure')->square() ->label('Structure') ->state(function ($record) { - return env('CM_PUBLIC_API', 'https://api.cheminf.studio/latest/').'depict/2D?smiles='.urlencode($record->canonical_smiles).'&height=300&width=300&CIP=true&toolkit=cdk'; + return getDepictUrl($record->canonical_smiles, 300, 300, 'cdk', true); }) ->width(200) ->height(200) diff --git a/app/Filament/Dashboard/Resources/MoleculeResource/RelationManagers/RelatedRelationManager.php b/app/Filament/Dashboard/Resources/MoleculeResource/RelationManagers/RelatedRelationManager.php index c890aa7d..2599165b 100644 --- a/app/Filament/Dashboard/Resources/MoleculeResource/RelationManagers/RelatedRelationManager.php +++ b/app/Filament/Dashboard/Resources/MoleculeResource/RelationManagers/RelatedRelationManager.php @@ -26,7 +26,7 @@ public function table(Table $table): Table ImageColumn::make('structure')->square() ->label('Structure') ->state(function ($record) { - return env('CM_PUBLIC_API', 'https://api.cheminf.studio/latest/').'depict/2D?smiles='.urlencode($record->canonical_smiles).'&height=300&width=300&CIP=true&toolkit=cdk'; + return getDepictUrl($record->canonical_smiles, 300, 300, 'cdk', true); }) ->width(200) ->height(200) diff --git a/app/Filament/Dashboard/Resources/OrganismResource/RelationManagers/MoleculesRelationManager.php b/app/Filament/Dashboard/Resources/OrganismResource/RelationManagers/MoleculesRelationManager.php index 2052b6c5..5b8c03fc 100644 --- a/app/Filament/Dashboard/Resources/OrganismResource/RelationManagers/MoleculesRelationManager.php +++ b/app/Filament/Dashboard/Resources/OrganismResource/RelationManagers/MoleculesRelationManager.php @@ -40,7 +40,7 @@ public function table(Table $table): Table ImageColumn::make('structure')->square() ->label('Structure') ->state(function ($record) { - return env('CM_PUBLIC_API', 'https://api.cheminf.studio/latest/').'depict/2D?smiles='.urlencode($record->canonical_smiles).'&height=300&width=300&CIP=true&toolkit=cdk'; + return getDepictUrl($record->canonical_smiles, 300, 300, 'cdk', true); }) ->width(200) ->height(200) diff --git a/app/Filament/Dashboard/Resources/OrganismResource/RelationManagers/SampleLocationsRelationManager.php b/app/Filament/Dashboard/Resources/OrganismResource/RelationManagers/SampleLocationsRelationManager.php index 159a9fd9..454c0438 100644 --- a/app/Filament/Dashboard/Resources/OrganismResource/RelationManagers/SampleLocationsRelationManager.php +++ b/app/Filament/Dashboard/Resources/OrganismResource/RelationManagers/SampleLocationsRelationManager.php @@ -38,7 +38,7 @@ public function table(Table $table): Table ]) ->actions([ Tables\Actions\Action::make('edit') - ->url(fn (SampleLocation $record) => env('APP_URL').'/dashboard/sample-locations/'.$record->id.'/edit') + ->url(fn (SampleLocation $record) => config('app.url').'/dashboard/sample-locations/'.$record->id.'/edit') // ->color('info') ->icon('heroicon-m-pencil-square'), // Tables\Actions\EditAction::make(), diff --git a/app/Filament/Dashboard/Resources/ReportResource.php b/app/Filament/Dashboard/Resources/ReportResource.php index 12b8be91..daeddcf3 100644 --- a/app/Filament/Dashboard/Resources/ReportResource.php +++ b/app/Filament/Dashboard/Resources/ReportResource.php @@ -228,7 +228,7 @@ public static function form(Form $form): Form }), Action::make('viewCompoundPage') ->color('info') - ->url(fn (string $operation, $record): string => $operation === 'create' ? env('APP_URL').'/compounds/'.request()->compound_id : env('APP_URL').'/compounds/'.$record->mol_ids) + ->url(fn (string $operation, $record): string => $operation === 'create' ? config('app.url').'/compounds/'.request()->compound_id : config('app.url').'/compounds/'.$record->mol_ids) ->openUrlInNewTab() ->hidden(function (Get $get, string $operation) { return ! $get('type'); diff --git a/app/Filament/Dashboard/Resources/ReportResource/RelationManagers/EntriesRelationManager.php b/app/Filament/Dashboard/Resources/ReportResource/RelationManagers/EntriesRelationManager.php index ce7075ec..6fb4b5ae 100644 --- a/app/Filament/Dashboard/Resources/ReportResource/RelationManagers/EntriesRelationManager.php +++ b/app/Filament/Dashboard/Resources/ReportResource/RelationManagers/EntriesRelationManager.php @@ -94,21 +94,21 @@ public function infolist(Infolist $infolist): Infolist ]) ->schema([ ImageEntry::make('parent_canonical_smiles')->state(function ($record) { - return env('CM_PUBLIC_API', 'https://api.cheminf.studio/latest/').'depict/2D?smiles='.urlencode($record->parent_canonical_smiles).'&height=300&width=300&CIP=true&toolkit=cdk'; + return getDepictUrl($record->parent_canonical_smiles, 300, 300, 'cdk', true); }) ->width(200) ->height(200) ->ring(5) ->defaultImageUrl(url('/images/placeholder.png')), ImageEntry::make('canonical_smiles')->state(function ($record) { - return env('CM_PUBLIC_API', 'https://api.cheminf.studio/latest/').'depict/2D?smiles='.urlencode($record->canonical_smiles).'&height=300&width=300&CIP=true&toolkit=cdk'; + return getDepictUrl($record->canonical_smiles, 300, 300, 'cdk', true); }) ->width(200) ->height(200) ->ring(5) ->defaultImageUrl(url('/images/placeholder.png')), ImageEntry::make('standardized_canonical_smiles')->state(function ($record) { - return env('CM_PUBLIC_API', 'https://api.cheminf.studio/latest/').'depict/2D?smiles='.urlencode($record->standardized_canonical_smiles).'&height=300&width=300&CIP=true&toolkit=cdk'; + return getDepictUrl($record->standardized_canonical_smiles, 300, 300, 'cdk', true); }) ->width(200) ->height(200) @@ -128,7 +128,7 @@ public function table(Table $table): Table ImageColumn::make('structure')->square() ->label('Structure') ->state(function ($record) { - return env('CM_PUBLIC_API', 'https://api.cheminf.studio/latest/').'depict/2D?smiles='.urlencode($record->canonical_smiles).'&height=300&width=300&CIP=true&toolkit=cdk'; + return getDepictUrl($record->canonical_smiles, 300, 300, 'cdk', true); }) ->width(200) ->height(200) diff --git a/app/Filament/Dashboard/Resources/ReportResource/RelationManagers/MoleculesRelationManager.php b/app/Filament/Dashboard/Resources/ReportResource/RelationManagers/MoleculesRelationManager.php index 27247b19..d4834703 100644 --- a/app/Filament/Dashboard/Resources/ReportResource/RelationManagers/MoleculesRelationManager.php +++ b/app/Filament/Dashboard/Resources/ReportResource/RelationManagers/MoleculesRelationManager.php @@ -35,7 +35,7 @@ public function table(Table $table): Table ImageColumn::make('structure')->square() ->label('Structure') ->state(function ($record) { - return env('CM_PUBLIC_API', 'https://api.cheminf.studio/latest/').'depict/2D?smiles='.urlencode($record->canonical_smiles).'&height=300&width=300&CIP=true&toolkit=cdk'; + return getDepictUrl($record->canonical_smiles, 300, 300, 'cdk', true); }) ->width(200) ->height(200) diff --git a/app/Helper.php b/app/Helper.php index 4f36c30b..03c37da7 100644 --- a/app/Helper.php +++ b/app/Helper.php @@ -75,7 +75,7 @@ function doiRegxMatch($doi) function fetchDOICitation($doi) { $citationResponse = null; - $europemcUrl = env('EUROPEPMC_WS_API'); + $europemcUrl = config('services.citation.europepmc_url'); $europemcParams = [ 'query' => 'DOI:'.$doi, 'format' => 'json', @@ -89,13 +89,13 @@ function fetchDOICitation($doi) $citationResponse = formatCitationResponse($europemcResponse['resultList']['result'][0], 'europemc'); } else { // fetch citation from CrossRef - $crossrefUrl = env('CROSSREF_WS_API').$doi; + $crossrefUrl = config('services.citation.crossref_url').$doi; $crossrefResponse = makeRequest($crossrefUrl); if ($crossrefResponse && isset($crossrefResponse['message'])) { $citationResponse = formatCitationResponse($crossrefResponse['message'], 'crossref'); } else { // fetch citation from DataCite - $dataciteUrl = env('DATACITE_WS_API').$doi; + $dataciteUrl = config('services.citation.datacite_url').$doi; $dataciteResponse = makeRequest($dataciteUrl); if ($dataciteResponse && isset($dataciteResponse['data'])) { $citationResponse = formatCitationResponse($dataciteResponse['data'], 'datacite'); @@ -638,3 +638,24 @@ function handleJobFailure( $batchId ); } + +/** + * Generate a URL for 2D molecule depiction through the CMS proxy + * + * @param string $smiles The SMILES string + * @param int $height Image height (default: 300) + * @param int $width Image width (default: 300) + * @param string $toolkit Toolkit to use: cdk, rdkit, openbabel (default: cdk) + * @param bool $CIP Whether to include CIP labels (default: true) + * @return string The proxy URL + */ +function getDepictUrl(string $smiles, int $height = 300, int $width = 300, string $toolkit = 'cdk', bool $CIP = true): string +{ + return route('cms.depict2d', [ + 'smiles' => $smiles, + 'height' => $height, + 'width' => $width, + 'toolkit' => $toolkit, + 'CIP' => $CIP ? 'true' : 'false', + ]); +} diff --git a/app/Http/Controllers/CmsProxyController.php b/app/Http/Controllers/CmsProxyController.php new file mode 100644 index 00000000..fa49fe87 --- /dev/null +++ b/app/Http/Controllers/CmsProxyController.php @@ -0,0 +1,58 @@ +cmsClient = $cmsClient; + } + + /** + * Generic proxy for CMS API requests + * CSRF protection is automatically applied by Laravel's web middleware + * + * @return \Illuminate\Http\Response + */ + public function proxy(Request $request) + { + $endpoint = $request->input('endpoint'); + $params = $request->except(['endpoint', '_token']); + $method = $request->method(); + + $response = match ($method) { + 'GET' => $this->cmsClient->get($endpoint, $params), + 'POST' => $this->cmsClient->post($endpoint, $params), + default => abort(405, 'Method not allowed'), + }; + + return response($response->body()) + ->header('Content-Type', $response->header('Content-Type')) + ->header('Cache-Control', 'public, max-age=86400') + ->setStatusCode($response->status()); + } + + /** + * Proxy for 2D depiction - used by img tags + * No CSRF protection (called directly by browsers via img src) + * Rate limited via route middleware to prevent abuse + * + * @return \Illuminate\Http\Response + */ + public function depict2D(Request $request) + { + $params = $request->only(['smiles', 'height', 'width', 'toolkit', 'CIP']); + + $response = $this->cmsClient->get('depict/2D', $params); + + return response($response->body()) + ->header('Content-Type', $response->header('Content-Type') ?? 'image/svg+xml') + ->header('Cache-Control', 'public, max-age=86400'); + } +} diff --git a/app/Jobs/ImportEntry.php b/app/Jobs/ImportEntry.php index d411a162..3eb2605a 100644 --- a/app/Jobs/ImportEntry.php +++ b/app/Jobs/ImportEntry.php @@ -282,7 +282,7 @@ public function fetchDOICitation($doi, $molecule) $citationResponse = null; if ($citation->wasRecentlyCreated || $citation->title == '') { // fetch citation from EuropePMC - $europemcUrl = env('EUROPEPMC_WS_API'); + $europemcUrl = config('services.citation.europepmc_url'); $europemcParams = [ 'query' => 'DOI:'.$doi, 'format' => 'json', @@ -296,14 +296,14 @@ public function fetchDOICitation($doi, $molecule) $citationResponse = $this->formatCitationResponse($europemcResponse['resultList']['result'][0], 'europemc'); } else { // fetch citation from CrossRef - $crossrefUrl = env('CROSSREF_WS_API').$doi; + $crossrefUrl = config('services.citation.crossref_url').$doi; $response = $this->makeRequest($crossrefUrl); $crossrefResponse = $response ? $response->json() : null; if ($crossrefResponse && isset($crossrefResponse['message'])) { $citationResponse = $this->formatCitationResponse($crossrefResponse['message'], 'crossref'); } else { // fetch citation from DataCite - $dataciteUrl = env('DATACITE_WS_API').$doi; + $dataciteUrl = config('services.citation.datacite_url').$doi; $response = $this->makeRequest($dataciteUrl); $dataciteResponse = $response ? $response->json() : null; if ($dataciteResponse && isset($dataciteResponse['data'])) { diff --git a/app/Jobs/ImportEntryAuto.php b/app/Jobs/ImportEntryAuto.php index 59845b99..93d22916 100644 --- a/app/Jobs/ImportEntryAuto.php +++ b/app/Jobs/ImportEntryAuto.php @@ -363,7 +363,7 @@ public function fetchDOICitation($doi, $molecule) $citationResponse = null; if ($citation->wasRecentlyCreated || $citation->title == '') { // fetch citation from EuropePMC - $europemcUrl = env('EUROPEPMC_WS_API'); + $europemcUrl = config('services.citation.europepmc_url'); $europemcParams = [ 'query' => 'DOI:'.$doi, 'format' => 'json', @@ -377,14 +377,14 @@ public function fetchDOICitation($doi, $molecule) $citationResponse = $this->formatCitationResponse($europemcResponse['resultList']['result'][0], 'europemc'); } else { // fetch citation from CrossRef - $crossrefUrl = env('CROSSREF_WS_API').$doi; + $crossrefUrl = config('services.citation.crossref_url').$doi; $response = $this->makeRequest($crossrefUrl); $crossrefResponse = $response ? $response->json() : null; if ($crossrefResponse && isset($crossrefResponse['message'])) { $citationResponse = $this->formatCitationResponse($crossrefResponse['message'], 'crossref'); } else { // fetch citation from DataCite - $dataciteUrl = env('DATACITE_WS_API').$doi; + $dataciteUrl = config('services.citation.datacite_url').$doi; $response = $this->makeRequest($dataciteUrl); $dataciteResponse = $response ? $response->json() : null; if ($dataciteResponse && isset($dataciteResponse['data'])) { diff --git a/app/Jobs/ProcessEntry.php b/app/Jobs/ProcessEntry.php index 5c17ea83..6b6ef5a2 100644 --- a/app/Jobs/ProcessEntry.php +++ b/app/Jobs/ProcessEntry.php @@ -4,6 +4,7 @@ use App\Enums\ReportStatus; use App\Events\PrePublishJobFailed; +use App\Services\CmsClient; use Illuminate\Bus\Batchable; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; @@ -11,7 +12,6 @@ use Illuminate\Http\Client\RequestException; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; class ProcessEntry implements ShouldQueue @@ -55,11 +55,16 @@ public function handle(): void $has_stereocenters = false; $is_invalid = false; $error_code = -1; - $API_URL = env('API_URL', 'https://api.cheminf.studio/latest/'); - $ENDPOINT = $API_URL.'chem/coconut/pre-processing?smiles='.urlencode($canonical_smiles).'&_3d_mol=false&descriptors=false'; + + $cmsClient = app(CmsClient::class); try { - $response = Http::timeout(600)->get($ENDPOINT); + $response = $cmsClient->get('chem/coconut/pre-processing', [ + 'smiles' => $canonical_smiles, + '_3d_mol' => 'false', + 'descriptors' => 'false', + ], false); + if ($response->successful()) { $data = $response->json(); if (array_key_exists('original', $data)) { diff --git a/app/Jobs/ProcessEntryBatch.php b/app/Jobs/ProcessEntryBatch.php index 12d79a89..a2dac9b5 100644 --- a/app/Jobs/ProcessEntryBatch.php +++ b/app/Jobs/ProcessEntryBatch.php @@ -5,6 +5,7 @@ use App\Enums\ReportStatus; use App\Events\PrePublishJobFailed; use App\Models\Entry; +use App\Services\CmsClient; use Exception; use Illuminate\Bus\Batchable; use Illuminate\Bus\Queueable; @@ -13,7 +14,6 @@ use Illuminate\Http\Client\RequestException; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; class ProcessEntryBatch implements ShouldQueue @@ -121,11 +121,16 @@ public function processEntry(Entry $entry): void $is_cis_trans = false; $is_invalid = false; $error_code = -1; - $API_URL = env('API_URL', 'https://api.cheminf.studio/latest/'); - $ENDPOINT = $API_URL.'chem/coconut/pre-processing?smiles='.urlencode($canonical_smiles).'&_3d_mol=false&descriptors=false'; + + $cmsClient = app(CmsClient::class); try { - $response = Http::timeout(600)->get($ENDPOINT); + $response = $cmsClient->get('chem/coconut/pre-processing', [ + 'smiles' => $canonical_smiles, + '_3d_mol' => 'false', + 'descriptors' => 'false', + ], false); + if ($response->successful()) { $data = $response->json(); if (array_key_exists('original', $data)) { diff --git a/app/Livewire/MoleculeDepict2d.php b/app/Livewire/MoleculeDepict2d.php index 76b5666a..49a96a74 100644 --- a/app/Livewire/MoleculeDepict2d.php +++ b/app/Livewire/MoleculeDepict2d.php @@ -28,13 +28,13 @@ class MoleculeDepict2d extends Component #[Computed] public function source() { - return env('CM_PUBLIC_API').'depict/2D?smiles='.urlencode($this->smiles).'&height='.$this->height.'&width='.$this->width.'&toolkit='.$this->toolkit.'&CIP='.$this->CIP; + return getDepictUrl($this->smiles, $this->height, $this->width, $this->toolkit, $this->CIP); } #[Computed] public function preview() { - return env('CM_PUBLIC_API').'depict/2D?smiles='.urlencode($this->smiles); + return getDepictUrl($this->smiles); } public function downloadMolFile($toolkit) diff --git a/app/Models/Molecule.php b/app/Models/Molecule.php index db9b3b91..d03f01b4 100644 --- a/app/Models/Molecule.php +++ b/app/Models/Molecule.php @@ -303,7 +303,7 @@ public function getSchema($type = 'bioschemas') $moleculeSchema->identifier($this->identifier) ->name($this->name) - ->url(env('APP_URL').'/compound/'.$this->identifier) + ->url(config('app.url').'/compound/'.$this->identifier) ->inChI($this->standard_inchi) ->inChIKey($this->standard_inchi_key) ->iupacName($this->iupac_name) diff --git a/app/Notifications/PostPublishJobFailedNotification.php b/app/Notifications/PostPublishJobFailedNotification.php index 58c822e6..c5683b27 100644 --- a/app/Notifications/PostPublishJobFailedNotification.php +++ b/app/Notifications/PostPublishJobFailedNotification.php @@ -43,7 +43,7 @@ public function toMail(object $notifiable): MailMessage $timestamp = $this->event->errorDetails['timestamp']; // Create a dashboard URL for admins to check logs - $dashboardUrl = url(env('APP_URL').'/dashboard'); + $dashboardUrl = url(config('app.url').'/dashboard'); $mailMessage = (new MailMessage) ->subject('Coconut: Post Publish Job Failed - '.$jobName) diff --git a/app/Notifications/PrePublishJobFailedNotification.php b/app/Notifications/PrePublishJobFailedNotification.php index daa8850d..69671a05 100644 --- a/app/Notifications/PrePublishJobFailedNotification.php +++ b/app/Notifications/PrePublishJobFailedNotification.php @@ -43,7 +43,7 @@ public function toMail(object $notifiable): MailMessage $timestamp = $this->event->errorDetails['timestamp']; // Create a dashboard URL for admins to check logs - $dashboardUrl = url(env('APP_URL').'/dashboard'); + $dashboardUrl = url(config('app.url').'/dashboard'); $mailMessage = (new MailMessage) ->subject('Coconut: Pre Publish Job Failed - '.$jobName) diff --git a/app/Notifications/ReportAssignedNotification.php b/app/Notifications/ReportAssignedNotification.php index 1278b549..f29cf456 100644 --- a/app/Notifications/ReportAssignedNotification.php +++ b/app/Notifications/ReportAssignedNotification.php @@ -37,7 +37,7 @@ public function via(object $notifiable): array */ public function toMail(object $notifiable) { - $url = url(env('APP_URL').'/dashboard/reports/'.$this->event->report->id.'/edit'); + $url = url(config('app.url').'/dashboard/reports/'.$this->event->report->id.'/edit'); return (new ReportAssignedMail($this->event, $notifiable, 'curator', $url)) ->to($notifiable->email); diff --git a/app/Notifications/ReportStatusChangedNotification.php b/app/Notifications/ReportStatusChangedNotification.php index e93f69ba..610e9f22 100644 --- a/app/Notifications/ReportStatusChangedNotification.php +++ b/app/Notifications/ReportStatusChangedNotification.php @@ -41,9 +41,9 @@ public function via(object $notifiable): array public function toMail(object $notifiable) { if ($notifiable->can('update', $this->event->report)) { - $url = url(env('APP_URL').'/dashboard/reports/'.$this->event->report->id.'/edit'); + $url = url(config('app.url').'/dashboard/reports/'.$this->event->report->id.'/edit'); } else { - $url = url(env('APP_URL').'/dashboard/reports/'.$this->event->report->id); + $url = url(config('app.url').'/dashboard/reports/'.$this->event->report->id); } return (new ReportStatusChangedMail($this->event, $notifiable, $this->mail_to, $url)) diff --git a/app/Notifications/ReportSubmittedNotification.php b/app/Notifications/ReportSubmittedNotification.php index 0d99e770..246ef6c9 100644 --- a/app/Notifications/ReportSubmittedNotification.php +++ b/app/Notifications/ReportSubmittedNotification.php @@ -41,9 +41,9 @@ public function via(object $notifiable): array public function toMail(object $notifiable) { if ($notifiable->can('update', $this->event->report)) { - $url = url(env('APP_URL').'/dashboard/reports/'.$this->event->report->id.'/edit'); + $url = url(config('app.url').'/dashboard/reports/'.$this->event->report->id.'/edit'); } else { - $url = url(env('APP_URL').'/dashboard/reports/'.$this->event->report->id); + $url = url(config('app.url').'/dashboard/reports/'.$this->event->report->id); } return (new ReportSubmittedMail($this->event, $notifiable, $this->mail_to, $url)) diff --git a/app/Services/CmsClient.php b/app/Services/CmsClient.php new file mode 100644 index 00000000..dc51aee6 --- /dev/null +++ b/app/Services/CmsClient.php @@ -0,0 +1,74 @@ +internalUrl = config('services.cheminf.internal_api_url'); + $this->publicUrl = config('services.cheminf.api_url'); + $this->authToken = config('services.cheminf.internal_token'); + } + + /** + * Make a GET request to the CMS API + * + * @return \Illuminate\Http\Client\Response + */ + public function get(string $endpoint, array $params = []) + { + return $this->makeRequest('GET', $endpoint, $params); + } + + /** + * Make a POST request to the CMS API + * + * @return \Illuminate\Http\Client\Response + */ + public function post(string $endpoint, array $data = []) + { + return $this->makeRequest('POST', $endpoint, $data); + } + + /** + * Make the actual HTTP request + * + * @return \Illuminate\Http\Client\Response + */ + private function makeRequest(string $method, string $endpoint, array $data = []) + { + $request = Http::timeout(120); + + // Add authentication token if available + if ($this->authToken) { + $request = $request->withHeaders([ + 'Authorization' => 'Bearer '.$this->authToken, + ]); + } + + $url = $this->internalUrl.ltrim($endpoint, '/'); + + return match (strtoupper($method)) { + 'GET' => $request->get($url, $data), + 'POST' => $request->post($url, $data), + default => throw new \InvalidArgumentException("Unsupported HTTP method: {$method}"), + }; + } + + /** + * Get the public URL for the CMS API (for frontend use) + */ + public function getPublicUrl(): string + { + return $this->publicUrl; + } +} diff --git a/composer.lock b/composer.lock index 340106f4..3b49658b 100644 --- a/composer.lock +++ b/composer.lock @@ -11797,16 +11797,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", "shasum": "" }, "require": { @@ -11819,7 +11819,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -11844,7 +11844,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" }, "funding": [ { @@ -11860,7 +11860,7 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/error-handler", @@ -12228,16 +12228,16 @@ }, { "name": "symfony/http-foundation", - "version": "v7.2.6", + "version": "v7.3.7", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "6023ec7607254c87c5e69fb3558255aca440d72b" + "reference": "db488a62f98f7a81d5746f05eea63a74e55bb7c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/6023ec7607254c87c5e69fb3558255aca440d72b", - "reference": "6023ec7607254c87c5e69fb3558255aca440d72b", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/db488a62f98f7a81d5746f05eea63a74e55bb7c4", + "reference": "db488a62f98f7a81d5746f05eea63a74e55bb7c4", "shasum": "" }, "require": { @@ -12254,6 +12254,7 @@ "doctrine/dbal": "^3.6|^4", "predis/predis": "^1.1|^2.0", "symfony/cache": "^6.4.12|^7.1.5", + "symfony/clock": "^6.4|^7.0", "symfony/dependency-injection": "^6.4|^7.0", "symfony/expression-language": "^6.4|^7.0", "symfony/http-kernel": "^6.4|^7.0", @@ -12286,7 +12287,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.2.6" + "source": "https://github.com/symfony/http-foundation/tree/v7.3.7" }, "funding": [ { @@ -12297,12 +12298,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-09T08:14:01+00:00" + "time": "2025-11-08T16:41:12+00:00" }, { "name": "symfony/http-kernel", @@ -12500,16 +12505,16 @@ }, { "name": "symfony/mime", - "version": "v7.2.6", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "706e65c72d402539a072d0d6ad105fff6c161ef1" + "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/706e65c72d402539a072d0d6ad105fff6c161ef1", - "reference": "706e65c72d402539a072d0d6ad105fff6c161ef1", + "url": "https://api.github.com/repos/symfony/mime/zipball/b1b828f69cbaf887fa835a091869e55df91d0e35", + "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35", "shasum": "" }, "require": { @@ -12564,7 +12569,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.2.6" + "source": "https://github.com/symfony/mime/tree/v7.3.4" }, "funding": [ { @@ -12575,12 +12580,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-27T13:34:41+00:00" + "time": "2025-09-16T08:38:17+00:00" }, { "name": "symfony/polyfill-ctype", @@ -12741,7 +12750,7 @@ }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", @@ -12804,7 +12813,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" }, "funding": [ { @@ -12815,6 +12824,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -12824,7 +12837,7 @@ }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -12885,7 +12898,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" }, "funding": [ { @@ -12896,6 +12909,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -12905,7 +12922,7 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", @@ -12966,7 +12983,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" }, "funding": [ { @@ -12977,6 +12994,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -12986,7 +13007,7 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", @@ -13046,7 +13067,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" }, "funding": [ { @@ -13057,6 +13078,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -13066,16 +13091,16 @@ }, { "name": "symfony/polyfill-php83", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", - "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", "shasum": "" }, "require": { @@ -13122,7 +13147,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" }, "funding": [ { @@ -13133,12 +13158,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-07-08T02:45:35+00:00" }, { "name": "symfony/polyfill-uuid", diff --git a/config/filesystems.php b/config/filesystems.php index 6a645054..54016361 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -39,7 +39,7 @@ 'public' => [ 'driver' => 'local', 'root' => storage_path('app/public'), - 'url' => env('APP_URL').'/storage', + 'url' => config('app.url').'/storage', 'visibility' => 'public', 'throw' => false, ], diff --git a/config/services.php b/config/services.php index ad1a400b..9073b4d1 100644 --- a/config/services.php +++ b/config/services.php @@ -49,4 +49,21 @@ 'redirect' => env('NFDI_REDIRECT_URL'), ], + 'cheminf' => [ + 'api_url' => env('CM_PUBLIC_API', 'https://api.cheminf.studio/latest/'), + 'internal_api_url' => env('API_URL', 'https://api.cheminf.studio/latest/'), + 'internal_token' => env('CMS_INTERNAL_AUTH_TOKEN'), + 'rate_limit' => env('CMS_RATE_LIMIT', 200), + ], + + 'citation' => [ + 'europepmc_url' => env('EUROPEPMC_WS_API'), + 'crossref_url' => env('CROSSREF_WS_API'), + 'datacite_url' => env('DATACITE_WS_API'), + ], + + 'tawk' => [ + 'url' => env('TAWK_URL'), + ], + ]; diff --git a/resources/views/components/tawk-chat.blade.php b/resources/views/components/tawk-chat.blade.php index fa7f24b3..b4416c64 100644 --- a/resources/views/components/tawk-chat.blade.php +++ b/resources/views/components/tawk-chat.blade.php @@ -5,7 +5,7 @@ var s1 = document.createElement("script"), s0 = document.getElementsByTagName("script")[0]; s1.async = true; - s1.src = '{{ env('TAWK_URL') }}'; + s1.src = '{{ config('services.tawk.url') }}'; s1.charset = 'UTF-8'; s1.setAttribute('crossorigin', '*'); s0.parentNode.insertBefore(s1, s0); diff --git a/resources/views/forms/components/organisms-table.blade.php b/resources/views/forms/components/organisms-table.blade.php index 7403be0f..f3758ea6 100644 --- a/resources/views/forms/components/organisms-table.blade.php +++ b/resources/views/forms/components/organisms-table.blade.php @@ -14,7 +14,7 @@
-