* Frontend: Add settings for albums, favorites, folders, and media features * Frontend: Enhance download settings for files and albums #848 * Frontend: Enhance download settings for files and albums #848 * Frontend: Update album download settings to restrict access to super admins * Frontend: Add album sorting options to settings for super admins * Frontend: Update download settings visibility * Frontend: Refine download settings access control for user roles * Tests: Add album, folder, moment, state, and month order selection options to page-model * Frontend: Update access control for super admins in settings to include scope check * Tests: Add new tests for download name and album order settings persistence * Tests: Implement new tests for default album sort order persistence in settings * Frontend: Move default album order settings * Frontend: Add zoom accessibility option in settings #799 * Frontend: Update download name from "Share Identifier" to "Share Friendly" and refine hints for download options in settings * Tests: Add new acceptance test for disabling album downloads while allowing file downloads in settings * Frontend: Improve download options layout and hints * Frontend: Refactor download options condition in settings page * Frontend: Implement native zoom control in lightbox component * Settings: Add Collections tab and move Services to its own page #848 #5429 - Add a super-admin Collections tab after Content holding the album Download and Sort Order settings; remove those cards from Content. - Move Services from a settings tab to a dedicated page (page/services.vue) with a search toolbar and a Reload / Learn More action menu; link it from the navigation settings menu. - Implement server-side services search (acc_name filter) with tests. - De-duplicate the picture sort-order options into a shared SortOrderOptions builder in options.js. - Update acceptance/unit tests and page models to match. * Frontend: Regenerate translation catalogs #848 #5429 Ran `make gettext-extract` to pick up the new settings strings (Collections, Features, Disable Downloads, and related messages) so Weblate can translate them. Existing translations are preserved. * Frontend: Move lightbox viewport-zoom handling into the view helper - Move the viewport pinch-zoom lock/restore (disableNativeZoom / restoreNativeZoom + savedViewportContent) from lightbox.vue onto the View singleton in common/view.js. - Drive it passively from View.apply() via a disableViewportZoom flag in the PLightbox case, alongside hideScrollbar / disableScrolling / disableNavigationGestures; lightbox.vue no longer references zoom. - Make save/restore idempotent and extract the viewport constants. - Cover the helpers and the apply() wiring in view.test.js. * Settings: Add Accessibility section to General with Reduce Motion #848 #5429 - Move the page-zoom checkbox out of the feature-flags grid into a dedicated Accessibility card (super-admin only) with four toggles: Open on Hover, Reduce Motion, Hide Scrollbar, and Allow Page Zoom. - Add a UI.ReduceMotion flag (defaults false) that toggles an html.reduce-motion class via config.setReduceMotion and suppresses map fly-to animations non-destructively (util.mapAnimateDuration). - Make Open on Hover apply live by reading shouldOpenOnHover() through a reactive computed in the action/auth/lightbox menus; Zoom and Hide Scrollbar reload since they are baked into the server-rendered HTML. - Reword two feature-grid hints to the "pictures" convention and cover the new helpers with Go and Vitest tests. * Frontend: Regenerate translation catalogs #848 #5429 Ran `make gettext-extract` to pick up the new Accessibility settings strings (Accessibility, Allow Page Zoom, Open on Hover, Hide Scrollbar, Reduce Motion, and their hints) and the reworded feature-grid hints. Existing translations are preserved. --------- Co-authored-by: Michael Mayer <michael@photoprism.app> |
||
|---|---|---|
| .. | ||
| download | ||
| embed | ||
| testdata | ||
| abort.go | ||
| AGENTS.md | ||
| albums.go | ||
| albums_search.go | ||
| albums_search_test.go | ||
| albums_test.go | ||
| api.go | ||
| api_auth.go | ||
| api_auth_jwt.go | ||
| api_auth_jwt_test.go | ||
| api_auth_test.go | ||
| api_client_config.go | ||
| api_client_config_test.go | ||
| api_event.go | ||
| api_event_test.go | ||
| api_log.go | ||
| api_methods.go | ||
| api_request.go | ||
| api_response.go | ||
| api_response_headers.go | ||
| api_response_test.go | ||
| api_test.go | ||
| auth_tokens.go | ||
| batch_albums.go | ||
| batch_labels.go | ||
| batch_photos.go | ||
| batch_photos_edit.go | ||
| batch_photos_edit_test.go | ||
| batch_photos_test.go | ||
| cache.go | ||
| cache_test.go | ||
| cameras.go | ||
| cameras_search.go | ||
| cameras_search_test.go | ||
| cameras_test.go | ||
| cluster_health_test.go | ||
| cluster_metrics.go | ||
| cluster_metrics_test.go | ||
| cluster_nodes.go | ||
| cluster_nodes_redaction_test.go | ||
| cluster_nodes_register.go | ||
| cluster_nodes_register_test.go | ||
| cluster_nodes_test.go | ||
| cluster_nodes_update_siteurl_test.go | ||
| cluster_permissions_test.go | ||
| cluster_summary.go | ||
| cluster_test_helpers.go | ||
| cluster_theme.go | ||
| cluster_theme_test.go | ||
| config_options.go | ||
| config_options_test.go | ||
| config_settings.go | ||
| config_settings_test.go | ||
| connect.go | ||
| connect_test.go | ||
| covers.go | ||
| covers_test.go | ||
| doc_overrides.go | ||
| docs.go | ||
| download.go | ||
| download_album.go | ||
| download_album_test.go | ||
| download_test.go | ||
| echo.go | ||
| echo_test.go | ||
| errors.go | ||
| errors_test.go | ||
| faces.go | ||
| faces_search.go | ||
| faces_search_test.go | ||
| faces_test.go | ||
| file_delete.go | ||
| file_delete_test.go | ||
| file_orientation.go | ||
| files.go | ||
| files_test.go | ||
| folders_cover.go | ||
| folders_cover_test.go | ||
| folders_search.go | ||
| folders_search_test.go | ||
| health.go | ||
| import.go | ||
| import_test.go | ||
| index.go | ||
| index_test.go | ||
| labels.go | ||
| labels_search.go | ||
| labels_search_test.go | ||
| labels_test.go | ||
| lenses.go | ||
| lenses_search.go | ||
| lenses_search_test.go | ||
| lenses_test.go | ||
| links.go | ||
| links_test.go | ||
| markers.go | ||
| markers_test.go | ||
| mcp.go | ||
| mcp_test.go | ||
| metrics.go | ||
| metrics_test.go | ||
| moments_time.go | ||
| moments_time_test.go | ||
| oauth_authorize.go | ||
| oauth_authorize_test.go | ||
| oauth_error.go | ||
| oauth_error_test.go | ||
| oauth_handlers.go | ||
| oauth_logout.go | ||
| oauth_logout_test.go | ||
| oauth_revoke.go | ||
| oauth_revoke_test.go | ||
| oauth_token.go | ||
| oauth_token_ratelimit_test.go | ||
| oauth_token_test.go | ||
| oauth_userinfo.go | ||
| oauth_userinfo_test.go | ||
| oidc_login.go | ||
| oidc_login_test.go | ||
| oidc_redirect.go | ||
| oidc_redirect_test.go | ||
| oidc_session_cookie.go | ||
| oidc_session_cookie_test.go | ||
| options.go | ||
| photo_label.go | ||
| photo_label_test.go | ||
| photo_unstack.go | ||
| photo_unstack_test.go | ||
| photos.go | ||
| photos_search.go | ||
| photos_search_geo.go | ||
| photos_search_geo_test.go | ||
| photos_search_test.go | ||
| photos_test.go | ||
| places_reverse.go | ||
| places_search.go | ||
| places_test.go | ||
| reactions.go | ||
| README.md | ||
| request_limits.go | ||
| server.go | ||
| services.go | ||
| services_search.go | ||
| services_search_test.go | ||
| services_test.go | ||
| services_upload.go | ||
| services_upload_test.go | ||
| session.go | ||
| session_create.go | ||
| session_delete.go | ||
| session_delete_test.go | ||
| session_get.go | ||
| session_ratelimit_test.go | ||
| session_response.go | ||
| session_test.go | ||
| share.go | ||
| share_preview.go | ||
| share_preview_test.go | ||
| share_test.go | ||
| status.go | ||
| status_test.go | ||
| subjects.go | ||
| subjects_search.go | ||
| subjects_search_test.go | ||
| subjects_test.go | ||
| svg.go | ||
| svg_test.go | ||
| swagger.json | ||
| thumbnails.go | ||
| thumbnails_test.go | ||
| users_avatar.go | ||
| users_avatar_test.go | ||
| users_passcode.go | ||
| users_passcode_test.go | ||
| users_password.go | ||
| users_password_test.go | ||
| users_sessions.go | ||
| users_sessions_test.go | ||
| users_update.go | ||
| users_update_test.go | ||
| users_upload.go | ||
| users_upload_multipart_test.go | ||
| users_upload_test.go | ||
| video.go | ||
| video_test.go | ||
| vision_caption.go | ||
| vision_caption_test.go | ||
| vision_face.go | ||
| vision_face_test.go | ||
| vision_labels.go | ||
| vision_labels_test.go | ||
| vision_nsfw.go | ||
| vision_nsfw_test.go | ||
| websocket.go | ||
| websocket_create.go | ||
| websocket_reader.go | ||
| websocket_test.go | ||
| websocket_topics_test.go | ||
| websocket_writer.go | ||
| zip.go | ||
| zip_test.go | ||
API Package Guide
Overview
The API package exposes PhotoPrism’s HTTP endpoints via Gin handlers. Each file under internal/api contains the handlers, request/response DTOs, and Swagger annotations for a specific feature area. Handlers remain thin: they validate input, enforce security or ACL checks, and delegate domain work to services in internal/photoprism, internal/service, or other internal packages. Keep exported types aligned with the REST schema and avoid embedding business logic directly in handlers.
Routing & Wiring
- Register handlers in
internal/server/routes.goby attaching them to the proper router group (for example,APIv1 := router.Group(conf.BaseUri("/api/v1"), Api(conf))). - Group endpoints by resource to match existing patterns: sessions, cluster, photos, labels, files, downloads, metadata, and technical routes.
- Apply middleware stacks (
Api,AuthRequired,limiter.Auth, etc.) at the router level to keep handlers focused on request handling. - Use
conf.BaseUri()when constructing route prefixes so configuration overrides propagate consistently. - When new endpoints require feature toggles, gate them in the router rather than inside the handler so disabled routes remain undiscoverable.
Handler Implementation Patterns
- Accept and return JSON using the shared response helpers. Set
header.ContentTypeJSONand ensure responses include proper cache headers (no-storefor sensitive payloads). - Parse parameters with Gin binding and validate inputs before delegating work. For complex payloads, define dedicated request structs with validation tags.
- Use the shared download helpers (
safe.Download,avatar.SafeDownload) when calling outward HTTP APIs to inherit timeout, size, and SSRF protections. - Query and persist data through the corresponding services or repositories; avoid ad-hoc SQL or GORM usage in handlers when dedicated functions exist elsewhere.
- Surface pagination consistently with
count,offset, andlimitfollowing the defaults (100 max 1000). Validateoffset >= 0and clampcountto the allowed range. - When responses need role-specific fields, build DTOs that redact sensitive data for non-admin roles so the handler stays deterministic.
- JSON field casing: use TitleCase field names (
UUID,Name,SiteUrl,CreatedAt) for request/response bodies that correspond to a database entity (mirroring the entity/model, e.g. the clusterNodeandClusterInstanceDTOs), and camelCase (storageNamespace,redirectUri) for generated or artificial payloads that do not map to a specific entity — client config, session responses, and action/RPC bodies. A filtered or computed projection of an entity stays TitleCase; an action payload that operates on an entity stays camelCase but MAY TitleCase the single identity field that mirrors the entity (e.g. aUUID).
Security & Middleware
- Authenticate requests using the standard middleware (
AuthRequired) and check roles via helpers ininternal/auth/acl(acl.ParseRole,acl.ScopePermits,acl.ScopeAttrPermits). - Bound request bodies before parsing JSON or multipart payloads. Use
LimitRequestBodyBytes(...)with a route-appropriate cap beforeBindJSON(...)/ShouldBindJSON(...), detectIsRequestBodyTooLarge(err), and return413 Request Entity Too LargeviaAbortRequestTooLarge(...). - Keep new JSON binding sites on the shared request-limit path by running
make check-api-request-limits(also included inmake lint) after adding or refactoring API handlers in the root repo or private overlays. - Never log secrets or tokens. Prefer structured logging through
event.Logand redact sensitive values before logging. - Enforce rate limiting with the shared limiters (
limiter.Auth,limiter.Login) and respond withlimiter.AbortJSONto maintain consistent 429 JSON payloads. - Derive client IPs through
api.ClientIPand extract bearer tokens withheader.BearerTokenor the helper setters. Use constant-time comparison for tokens and secrets. - For downloads or proxy endpoints, validate URLs against allowed schemes (
http,https) and reject private or loopback addresses unless explicitly required. - Upload-time NSFW screening (
users_upload.go) — whenPHOTOPRISM_UPLOAD_NSFW=false, the upload handler runsvision.DetectNSFWagainst every accepted file and deletes any file flagged above the NSFW threshold before it reachesoriginals/. The check is skipped entirely whenUPLOAD_NSFW=true(default). Seeinternal/ai/nsfw/README.mdfor the full NSFW call-graph and flag matrix.
Audit Logging
- Emit security events via
event.Audit*(AuditInfo,AuditWarn,AuditErr,AuditDebug) and always build the slice as Who → What → Outcome.- Who:
ClientIP(c)followed by the most specific actor context ("session %s","client %s","user %s"). - What: Resource constant plus action segments (for example,
string(acl.ResourceCluster),"node", "%s"). Place extra context such as counts or error placeholders in separate segments before the outcome. - Outcome: End with a single token such as
status.Succeeded,status.Failed,status.Denied, orstatus.Error(err)when the sanitized error message should be the outcome; nothing comes after it.
- Who:
- Prefer existing helpers (
ClientIP,clean.Log,clean.LogQuote,clean.Error) instead of formatting values manually, and avoid inline=expressions. - Example patterns:
event.AuditInfo([]string{ ClientIP(c), "session %s", string(acl.ResourceCluster), "node", "%s", status.Deleted, }, s.RefID, uuid) event.AuditErr([]string{ clientIp, "session %s", string(acl.ResourceCluster), "download theme", status.Error(err), }, refID)
User-Visible Notifications vs Audit Log
event.AuditInfo / AuditWarn / AuditErr write to the audit log and broadcast on audit.log.<level> — the toast component on the frontend does NOT subscribe to that channel, so an audit entry alone produces no UI feedback. To raise a red or green toast in the browser, publish on the notify.* channel via event.Error(msg) / event.ErrorMsg(id, …) (red) or event.Success(msg) (green).
The two helpers have distinct subscribers; choose based on who the message is for:
- Short endpoints whose response the frontend reads (single-shot CRUD, login, settings updates). The calling component renders the response, so
AuditErrplus an HTTP error is enough — the UI gets the error string from the response body. - Long-running endpoints that the UI drives via the event hub (
POST /api/v1/index,POST /api/v1/import/*path, and similar). The frontend cancels the in-flight HTTP request on the firstindex.*/import.*wire event, so the response body is invisible in normal operation. In-flight failures that need a specific toast MUST be published viaevent.ErrorMsg(...)onnotify.error; an HTTP error alone produces only the frontend's generic fallback toast (or nothing, if the cancel has already fired). - Forensic events that don't need UI surfacing (rate limiting, ACL denials, internal aborts whose user-visible signal comes from a sibling channel).
AuditErralone is the right call.
When in doubt, ask: "after this handler returns, what does the user see?" If the answer is "the frontend will read the response", AuditErr covers it. If the answer is "the page is already subscribed to wire events and the response is discarded", publish on notify.* as well.
// Forensic audit only — frontend will read the response body and render the error.
event.AuditErr([]string{ClientIP(c), "session %s", "delete album", status.Failed}, s.RefID)
AbortBadRequest(c, err)
// Forensic audit + specific red toast — needed when the request was already canceled by the wire.
event.AuditErr([]string{ClientIP(c), "session %s", "index files", status.Failed}, s.RefID)
event.ErrorMsg(i18n.ErrIndexingFailed)
Swagger Documentation
- Annotate handlers with Swagger comments that include full
/api/v1/...paths, request/response schemas, and security definitions. Only annotate routes that are externally accessible. - Regenerate docs after adding or updating handlers:
make fmt-go swag-fmt swag. This formats Go files, normalizes annotations, and updatesinternal/api/swagger.json. Do not edit the generated JSON manually. - When adding new DTOs, keep field names aligned with the JSON schema and update client documentation if serialized names change.
- Use enum annotations sparingly and ensure they reflect actual runtime constraints to avoid misleading generated clients.
Testing Strategy
- Build tests around the API harness (
NewApiTest) to obtain a configured Gin router, config, and dependencies. This isolates filesystem paths and avoids polluting global state. - Wrap requests with helper functions (for example,
PerformRequestJSON,PerformAuthenticatedRequest) to capture status codes, headers, and payloads. Assert headers using constants frompkg/http/header. - When handlers interact with the database, initialize fixtures through config helpers such as
config.NewTestConfig("api")orconfig.NewMinimalTestConfigWithDb("api", t.TempDir())depending on fixture needs. - Stub external dependencies (
httptest.Server) for remote calls and setAllowPrivate=trueexplicitly when the test server binds to loopback addresses. - Structure tests with table-driven subtests (
t.Run("CaseName", ...)) and use PascalCase names. Provide cleanup functions (t.Cleanup) to remove temporary files or databases created during tests. - Do not run
internal/apitests in parallel. These suites share fixture files, temporary assets, and database state, so parallelgo testinvocations can cause false failures and readonly/fixture-conflict errors.
Focused Test Runs
- Fast iteration:
go test ./internal/api -run '<Package|HandlerName>' -count=1 - Cluster endpoints:
go test ./internal/api -run 'Cluster' -count=1 - Downloads and zip streaming:
go test ./internal/api -run 'Download|Archive' -count=1 - Combined CLI and API validation: pair
go test ./internal/commands -run 'Cluster' -count=1with the matching API suite to ensure DTOs remain compatible. - Keep focused
internal/apiruns sequential. Do not launch multiplego test ./internal/api ...commands at the same time.
Preflight Checklist
- Format and regenerate documentation:
make fmt-go swag-fmt swag - Compile backend:
go build ./... - Execute targeted API suites:
go test ./internal/api -run '<Name>' -count=1 - Run integration-heavy checks before release:
go test ./internal/service/cluster/registry -count=1alongside relevant API routes to confirm cluster DTOs stay aligned. - Verify that
photoprism show commands --jsonreflects any new API-driven flags or outputs when CLI exposure changes.