feat: accept form submissions from the websites over an API endpoint

This commit is contained in:
fromme committed 2026-09-24 16:35:06 +02:00
1 parent f48e53fdd0
commit 30ad5e8488
17 files changed
+1350 -4

No files matched your search

+19 -4
View File
@@ -3,6 +3,7 @@ monolog:
- deprecation
- bpn
- audit
- api
handlers:
main:
@@ -11,7 +12,7 @@ monolog:
handler: exceptions
excluded_http_codes: [404, 405]
buffer_size: 50
channels: ["!bpn", "!audit"]
channels: ["!bpn", "!audit", "!api"]
exceptions:
type: rotating_file
path: "%kernel.logs_dir%/framework.%kernel.environment%.log"
@@ -29,6 +30,14 @@ monolog:
path: '%kernel.logs_dir%/audit.%kernel.environment%.log'
max_files: 5
level: info
# Form submissions posted by the sibling websites. Own file because it is machine
# traffic nobody watches between incidents, and it must stay out of the audit table.
api:
channels: [ "api" ]
type: rotating_file
path: '%kernel.logs_dir%/api.%kernel.environment%.log'
max_files: 30
level: info
database:
channels: ["audit"]
type: service
@@ -36,7 +45,7 @@ monolog:
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!bpn", "!doctrine", "!console"]
channels: ["!event", "!bpn", "!api", "!doctrine", "!console"]
when@dev:
monolog:
@@ -73,7 +82,7 @@ when@prod:
handler: exceptions
excluded_http_codes: [404, 405]
buffer_size: 50
channels: ["!bpn", "!audit"]
channels: ["!bpn", "!audit", "!api"]
exceptions:
type: rotating_file
path: "%kernel.logs_dir%/framework.%kernel.environment%.log"
@@ -91,6 +100,12 @@ when@prod:
path: '%kernel.logs_dir%/audit.%kernel.environment%.log'
max_files: 5
level: info
api:
channels: [ "api" ]
type: rotating_file
path: '%kernel.logs_dir%/api.%kernel.environment%.log'
max_files: 30
level: info
database:
channels: ["audit"]
type: service
@@ -98,4 +113,4 @@ when@prod:
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine", "!bpn", "!audit"]
channels: ["!event", "!doctrine", "!bpn", "!audit", "!api"]
+19
View File
@@ -7,6 +7,12 @@ security:
entity:
class: App\Entity\User
property: email
# The sibling websites post as this one; it exists in configuration only. An API caller
# must never be a row in the user table, where it would be a login to the UI as well.
api_user_provider:
memory:
users:
api: { roles: [ ROLE_API ] }
role_hierarchy:
ROLE_TEAM_ADMIN: [ ROLE_ADMINISTRATIVE ]
@@ -16,6 +22,17 @@ security:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
# Stateless and key-authenticated: a machine posts here inside a visitor's form
# submission on another site, so there is no session, no login form and no redirect.
# Must stay above "main", whose pattern is ^/.
api:
pattern: ^/api
stateless: true
provider: api_user_provider
access_token:
token_handler: App\Security\AccessToken\ApiKeyTokenHandler
token_extractors:
- App\Security\AccessToken\ApiKeyExtractor
main:
lazy: true
provider: bpn_user_provider
@@ -29,6 +46,8 @@ security:
path: app_security_logout
target: app_security_login
access_control:
# The firewall authenticates, the controller requires ROLE_API
- { path: '^/api', roles: PUBLIC_ACCESS, requires_channel: https }
- { path: '^/_uploader', roles: ROLE_USER, requires_channel: https }
- { path: '^/', roles: PUBLIC_ACCESS, requires_channel: https }
+7
View File
@@ -0,0 +1,7 @@
# Enabled for #[MapRequestPayload] on the api controllers - nothing else in this application
# serializes. ALLOW_EXTRA_ATTRIBUTES stays at its default (true) on purpose: the TYPO3 form
# payload grows keys without a release here, and rejecting an unknown one would turn every
# later mapping change into a 400 (see .ddev/plans/application-form-endpoint.md).
framework:
serializer:
name_converter: ~
+17
View File
@@ -35,6 +35,11 @@ parameters:
1461: 'SHM'
1462: 'AGR'
# Shared key the sibling websites authenticate their form submissions with. Empty here on
# purpose - the real value belongs in the environment of each stage, and an empty key
# rejects every request instead of letting them through.
application_form_api_key: '%env(string:APPLICATION_FORM_API_KEY)%'
default_email_from: '%env(APP_DEFAULT_EMAIL_FROM)%'
default_email_to: '%env(APP_DEFAULT_EMAIL_TO)%'
@@ -318,6 +323,18 @@ services:
from: '%default_email_from%'
to: '%default_email_to%'
# Its own log channel, not the audit one bound by default: the audit channel writes a row
# to the log table through App\Logging\DatabaseHandler, which records what staff did here.
App\Security\AccessToken\ApiKeyTokenHandler:
arguments:
$apiKey: '%application_form_api_key%'
$logger: '@monolog.logger.api'
App\Controller\Api\ApplicationFormController:
arguments:
$logger: '@monolog.logger.api'
tags: [ 'controller.service_arguments' ]
App\Security\OAuth2\MyEpClient:
arguments:
$options: