07 — API Reference
Every HTTP endpoint, grouped by controller. Generated by reading every controller file in src/AssetTracking.API/Controllers/. For request/response shape, follow the linked handler in 02-backend-modules; for the underlying schema, 03-database.
Conventions
- Base path: every controller mounts at
api/v1/[controller] unless overridden by an explicit [Route("api/v1/...")] attribute. Where the kebab-case path differs from the C# class name, the override is noted in the controller's section.
- Auth: every action carries
[RequirePermission(...)] or [Authorize] or [AllowAnonymous] — the RequirePermission coverage test fails the build otherwise. The "Permission" column lists the resolved requirement (auth-only when [Authorize] with no permission key).
- Pagination:
?page=1&pageSize=20 (defaults vary). Response shape: PagedResult<T> = { items: T[], totalCount, page, pageSize }.
- Errors: RFC 7807
application/problem+json. Mappings detailed in 01 §Exceptions.
- Bilingual fields: handlers accept and return
*Primary / *Secondary pairs. The active UI language passes through the Accept-Language header.
- Idempotency: only
POST /audit-results is idempotent (via clientSubmissionId body field).
- Export endpoints: list resources support
GET /<resource>/export?format=xlsx|pdf returning a binary file. The format=pdf path on per-report endpoints requires the matching report.{name}.export-pdf granular permission, evaluated programmatically via EnsurePermissionAsync in ApiControllerBase.cs:25.
Controller index
| Controller |
Base path |
Module |
AuthController |
api/v1/auth |
Identity |
UsersController |
api/v1/users |
Identity |
RolesController |
api/v1/roles |
Identity |
PermissionsController |
api/v1/permissions |
Identity |
LoginAuditController |
api/v1/login-audit |
Identity |
OrganizationsController |
api/v1/organizations |
Master Data |
LocationsController |
api/v1/locations |
Master Data |
ClassificationsController |
api/v1/classifications |
Master Data |
VendorsController |
api/v1/vendors |
Master Data |
ManufacturersController |
api/v1/manufacturers |
Master Data |
AssetsController |
api/v1/assets |
Assets |
AssetStatusesController |
api/v1/asset-statuses |
Assets |
AssetTransfersController |
api/v1/asset-transfers |
Transfers |
CheckOutsController |
api/v1/checkouts |
Custody |
AuditPlansController |
api/v1/audit-plans |
Audits |
AuditAssignmentsController |
api/v1/audit-assignments |
Audits |
AuditResultsController |
api/v1/audit-results |
Audits |
MaintenancePlansController |
api/v1/maintenanceplans |
Maintenance |
MaintenanceRequestsController |
api/v1/maintenancerequests |
Maintenance |
WorkOrdersController |
api/v1/workorders |
Maintenance |
NotificationsController |
api/v1/notifications |
Notifications |
NotificationPreferencesController |
api/v1/notification-preferences |
Notifications |
NotificationTemplatesController |
api/v1/notification-templates |
Notifications |
DocumentsController |
api/v1/documents |
Documents |
ReportsController |
api/v1/reports |
Reporting |
HierarchyConfigController |
api/v1/hierarchy-config |
Settings |
AppSettingsController |
api/v1/app-settings |
Settings |
TranslationsController |
api/v1/translations |
Settings |
EmailProviderSettingsController |
api/v1/email-settings |
Settings |
AuditLogController |
api/v1/audit-log |
System |
Some controller class names do not match their kebab path exactly because the default [controller] token strips the Controller suffix and PascalCase-folds: MaintenancePlansController → maintenanceplans (no dash unless an explicit [Route("...")] is set). The base-path column above is authoritative.
AuthController (api/v1/auth)
| Method |
Path |
Permission |
Body |
Returns |
| POST |
/login |
[AllowAnonymous] |
LoginCommand { email, password, deviceId? } (IP enriched server-side) |
LoginResultDto { accessToken, refreshToken, expiresAt, user } |
| POST |
/refresh |
[AllowAnonymous] |
RefreshTokenCommand { refreshToken, deviceId? } (IP enriched) |
TokenResponse |
| POST |
/logout |
[Authorize] |
LogoutCommand { refreshToken } |
204 |
| POST |
/change-password |
[Authorize] |
ChangePasswordCommand { currentPassword, newPassword } |
204 |
| GET |
/me |
[Authorize] |
— |
CurrentUserDto (user record) |
| GET |
/me/permissions |
[Authorize] |
— |
string[] (effective permission keys) |
UsersController (api/v1/users)
| Method |
Path |
Permission |
Notes |
| GET |
/ |
user.read |
Query: page, pageSize, search, isActive, code, email, phoneNumber, userType |
| GET |
/{id:guid} |
user.read |
|
| POST |
/ |
user.create |
CreateUserCommand |
| PUT |
/{id:guid} |
user.update |
UpdateUserCommand (id from URL) |
| DELETE |
/{id:guid} |
user.delete |
Soft-delete |
| GET |
/{id:guid}/permissions |
user.read |
Effective permissions for that user |
| GET |
/{id:guid}/roles |
user.read |
List of role assignments |
| POST |
/{id:guid}/roles |
role.assign |
AssignRoleToUserCommand |
| DELETE |
/{id:guid}/roles/{roleId:guid} |
role.assign |
|
| GET |
/{id:guid}/direct-permissions |
user.read |
Paged. Query: page, pageSize, search, module, isDangerous |
| POST |
/{id:guid}/direct-permissions |
permission.assign-direct |
GrantPermissionCommand |
| DELETE |
/{id:guid}/direct-permissions/{permissionId:guid} |
permission.assign-direct |
|
| GET |
/{id:guid}/sessions |
user.read |
Query: activeOnly (default true) |
| DELETE |
/{id:guid}/sessions/{familyId:guid} |
user.update |
Revoke a refresh-token family |
| GET |
/export |
user.export |
`format=xlsx |
RolesController (api/v1/roles)
| Method |
Path |
Permission |
Notes |
| GET |
/ |
role.read |
Query: page, pageSize, search, code, isBuiltIn, isSystem |
| GET |
/{id:guid} |
role.read |
|
| POST |
/ |
role.create |
CreateRoleCommand |
| PUT |
/{id:guid} |
role.update |
UpdateRoleCommand |
| DELETE |
/{id:guid} |
role.delete |
Refuses if IsSystem == true |
| GET |
/{id:guid}/permissions |
role.read |
Permissions linked to the role |
| PUT |
/{id:guid}/permissions |
role.update |
AssignPermissionsToRoleCommand — diff-based; busts caches of every user holding the role |
| GET |
/export |
role.export |
|
PermissionsController (api/v1/permissions)
| Method |
Path |
Permission |
Notes |
| GET |
/ |
permission.read |
Returns PermissionGroup[] grouped by Module |
| GET |
/export |
permission.export |
Flattened to one row per permission |
There is no create/update/delete here — permissions are catalog-only, owned by PermissionSeeder.
LoginAuditController (api/v1/login-audit)
| Method |
Path |
Permission |
Notes |
| GET |
/ |
login-audit.read |
Query: page, pageSize, email, userId, result, from, to |
OrganizationsController (api/v1/organizations)
| Method |
Path |
Permission |
Notes |
| GET |
/ |
organization.read |
Query: page, pageSize, search, parentId, rootsOnly, level, ancestorId, code |
| GET |
/children |
organization.read |
Query: parentId? (returns roots when omitted). Lazy-tree feed |
| GET |
/{id:guid} |
organization.read |
|
| POST |
/ |
organization.create |
CreateOrganizationCommand |
| PUT |
/{id:guid} |
organization.update |
UpdateOrganizationCommand |
| DELETE |
/{id:guid} |
organization.delete |
|
| GET |
/template |
organization.read |
Downloads an Excel template for bulk import |
| GET |
/export |
organization.read |
Returns .xlsx |
| POST |
/import |
organization.import |
multipart/form-data file (≤ 10 MB) |
LocationsController (api/v1/locations) — same shape as Organizations
| Method |
Path |
Permission |
| GET / |
location.read |
|
| GET /children |
location.read |
|
| GET /{id:guid} |
location.read |
|
| POST / |
location.create |
|
| PUT /{id:guid} |
location.update |
|
| DELETE /{id:guid} |
location.delete |
|
| GET /template |
location.read |
|
| GET /export |
location.read |
|
| POST /import |
location.import |
|
ClassificationsController (api/v1/classifications) — same shape as Organizations
| Method |
Path |
Permission |
| GET / |
classification.read |
|
| GET /children |
classification.read |
|
| GET /{id:guid} |
classification.read |
|
| POST / |
classification.create |
|
| PUT /{id:guid} |
classification.update |
|
| DELETE /{id:guid} |
classification.delete |
|
| GET /template |
classification.read |
|
| GET /export |
classification.read |
|
| POST /import |
classification.import |
|
VendorsController (api/v1/vendors)
| Method |
Path |
Permission |
Notes |
| GET |
/ |
vendor.read |
Query: page, pageSize, search, code, contactName, contactEmail, contactPhone, taxId |
| GET |
/{id:guid} |
vendor.read |
|
| POST |
/ |
vendor.create |
CreateVendorCommand |
| PUT |
/{id:guid} |
vendor.update |
UpdateVendorCommand |
| DELETE |
/{id:guid} |
vendor.delete |
|
| GET |
/template |
vendor.read |
Excel template |
| GET |
/export |
vendor.read |
.xlsx |
| POST |
/import |
vendor.import |
multipart, ≤10 MB |
ManufacturersController (api/v1/manufacturers) — same shape as Vendors
| Method |
Path |
Permission |
| GET / |
manufacturer.read |
|
| GET /{id:guid} |
manufacturer.read |
|
| POST / |
manufacturer.create |
|
| PUT /{id:guid} |
manufacturer.update |
|
| DELETE /{id:guid} |
manufacturer.delete |
|
| GET /template |
manufacturer.read |
|
| GET /export |
manufacturer.read |
|
| POST /import |
manufacturer.import |
|
AssetsController (api/v1/assets)
| Method |
Path |
Permission |
Notes |
| GET |
/ |
asset.read |
Query: page, pageSize, search, organizationId, locationId, classificationId, statusId, isCritical, includeDescendants. Returns chain arrays for tooltips. |
| GET |
/{id:guid} |
asset.read |
Includes AssetDetails |
| POST |
/ |
asset.create |
CreateAssetCommand (returns FirstAsset for batch responses) |
| PUT |
/{id:guid} |
asset.update |
UpdateAssetCommand — writes history rows on dimensional changes |
| DELETE |
/{id:guid} |
asset.delete |
Soft-delete |
| POST |
/{id:guid}/status |
asset.update |
ChangeAssetStatusCommand — dedicated transition path |
| GET |
/{id:guid}/history |
asset.read |
Unified timeline (status + location + org + custody) |
| GET |
/template |
asset.read |
Excel template |
| GET |
/export |
asset.read |
.xlsx |
| POST |
/import |
asset.import |
multipart, ≤20 MB |
AssetStatusesController (api/v1/asset-statuses)
| Method |
Path |
Permission |
Notes |
| GET |
/ |
asset.read |
All status rows for dropdowns |
AssetTransfersController (api/v1/asset-transfers)
| Method |
Path |
Permission |
Notes |
| GET |
/ |
asset-transfer.read |
Query: page, pageSize, status, mineOnly, search, fromOrganizationId, toOrganizationId, fromLocationId, toLocationId, requestedFrom, requestedTo, includeDescendants |
| GET |
/{id:guid} |
asset-transfer.read |
|
| POST |
/ |
asset-transfer.create |
Creates as Draft |
| POST |
/{id:guid}/submit |
asset-transfer.submit |
Draft → Submitted; emits transfer.pending |
| POST |
/{id:guid}/approve |
asset-transfer.approve |
Submitted → Approved → InTransit; emits transfer.approved |
| POST |
/{id:guid}/reject |
asset-transfer.reject |
Body RejectTransferCommand { rejectionReason }; emits transfer.rejected |
| POST |
/{id:guid}/lines/{lineId:guid}/receive |
asset-transfer.receive |
ReceiveTransferLineCommand { receivedStatus, notes? } |
| POST |
/{id:guid}/receive-all |
asset-transfer.receive |
ReceiveAllTransferLinesCommand |
| POST |
/{id:guid}/complete |
asset-transfer.complete |
All lines must have ReceivedAt; emits transfer.completed; writes asset histories |
| POST |
/{id:guid}/cancel |
asset-transfer.cancel |
`Draft |
| DELETE |
/{id:guid} |
asset-transfer.delete |
Soft-delete |
| GET |
/export |
asset-transfer.export |
.xlsx / .pdf |
CheckOutsController (api/v1/checkouts)
| Method |
Path |
Permission |
Notes |
| GET |
/ |
check-out.read |
Query: page, pageSize, status, custodianId, assetId, mineOnly, search, checkedOutFrom, checkedOutTo |
| GET |
/{id:guid} |
check-out.read |
|
| POST |
/ |
check-out.create |
CreateCheckOutCommand. Sets asset Status=OnLoan; one active check-out per asset enforced by DB index |
| POST |
/{id:guid}/check-in |
check-out.return |
CheckInCommand { returnConditionRating?, returnNotes? }; restores PreviousStatusId |
| POST |
/{id:guid}/cancel |
check-out.cancel |
Reverts asset state if active |
| GET |
/export |
check-out.export |
|
AuditPlansController (api/v1/audit-plans)
| Method |
Path |
Permission |
Notes |
| GET |
/ |
audit-plan.read |
Query: page, pageSize, search, code, name, statuses[], priorities[], assignedUserIds[] |
| GET |
/{id:guid} |
audit-plan.read |
Includes Scopes |
| GET |
/{id:guid}/assignments |
audit-plan.read |
Paged list of assignments under the plan |
| POST |
/ |
audit-plan.create |
CreateAuditPlanCommand |
| POST |
/preview-scope |
audit-plan.create |
Body: PreviewScopeQuery — returns the resolved asset count + sample without persisting |
| PUT |
/{id:guid} |
audit-plan.update |
UpdateAuditPlanCommand |
| DELETE |
/{id:guid} |
audit-plan.delete |
Refuses if any non-cancelled assignment exists |
| GET |
/export |
audit-plan.export |
|
AuditAssignmentsController (api/v1/audit-assignments)
| Method |
Path |
Permission |
Notes |
| GET |
/ |
audit-assignment.read |
Filtered to AssignedUserId == currentUser; ordered CreatedAt DESC. Query: page, pageSize, status, search, code |
| GET |
/{id:guid} |
audit-assignment.read |
Single assignment |
| GET |
/{id:guid}/assets |
audit-assignment.read |
Snapshot rows + each asset's location chain |
| GET |
/{id:guid}/result |
audit-result.read |
The submitted result if any |
| POST |
/ |
audit-plan.assign |
CreateAssignmentCommand |
| GET |
/lookup-asset/{assetId:guid} |
audit-assignment.submit |
Mobile scanner: code/name/location lookup |
| GET |
/lookup-asset-by-code |
audit-assignment.submit |
Query: code — manual entry path |
| GET |
/location-hierarchy |
audit-assignment.submit |
Levels + labels for the mobile filter |
| GET |
/{id:guid}/draft |
audit-assignment.submit |
Returns DraftPayload JSON |
| PUT |
/{id:guid}/draft |
audit-assignment.submit |
Body { payload: string } — debounced auto-save |
| POST |
/{id:guid}/photos |
audit-assignment.submit |
multipart, ≤20 MB; returns the Document id |
| GET |
/export |
audit-assignment.read |
|
AuditResultsController (api/v1/audit-results)
| Method |
Path |
Permission |
Notes |
| GET |
/pending |
audit-result.review |
Reviewer's queue. Query: page, pageSize, search, code, reviewStatus, submittedFrom, submittedTo |
| GET |
/pending/export |
audit-result.review |
.xlsx / .pdf |
| GET |
/{id:guid} |
audit-result.read |
Result + lines + review actions |
| GET |
/{id:guid}/lines |
audit-result.read |
Paged. Query: page, pageSize, search, outcome, identificationMethod, decided |
| POST |
/ |
audit-assignment.submit |
SubmitResultCommand — idempotent via clientSubmissionId; emits audit.result.review-required |
| POST |
/{id:guid}/review |
audit-result.review |
ReviewLinesCommand (single-line). On full-coverage approve: assignment → Completed; emits audit.result.approved |
| POST |
/{id:guid}/bulk-review |
audit-result.review |
BulkReviewLinesCommand (multi-line) |
| GET |
/photo/{documentId:guid}/content |
audit-result.read OR audit-assignment.submit |
Streams a photo. Either reviewer (post-submit) or auditor (pre-submit) — handler enforces auditor-only path for unlinked photos |
| GET |
/excel-template/{assignmentId:guid} |
audit-assignment.submit |
Builds a pre-filled .xlsx for the offline path |
| POST |
/from-excel |
audit-assignment.submit |
multipart, ≤20 MB. Form: auditAssignmentId, clientSubmissionId, deviceInfo? + the file |
MaintenancePlansController (api/v1/maintenanceplans)
| Method |
Path |
Permission |
Notes |
| GET |
/ |
maintenance-plan.read |
Query: page, pageSize, assetId, isActive, search, code, frequency, nextDueFrom, nextDueTo |
| GET |
/{id:guid} |
maintenance-plan.read |
Includes Assets join collection |
| GET |
/{id:guid}/workorders |
maintenance-plan.read |
Paged WO history per plan |
| POST |
/ |
maintenance-plan.create |
CreatePlanCommand — requires assetIds[] (≥1) |
| PUT |
/{id:guid} |
maintenance-plan.update |
Diffs join rows |
| DELETE |
/{id:guid} |
maintenance-plan.delete |
Soft-delete (cascades the join) |
| POST |
/{id:guid}/generate-work-orders |
maintenance-plan.generate |
One WO per plan-asset; refuses if NextDueDate > now. Rolls plan: LastPerformedDate=now, NextDueDate += frequency |
| GET |
/export |
maintenance-plan.export |
|
MaintenanceRequestsController (api/v1/maintenancerequests)
| Method |
Path |
Permission |
Notes |
| GET |
/ |
maintenance-request.read |
Query: page, pageSize, status, severity, assetId, mineOnly, search, code, reportedFrom, reportedTo |
| GET |
/{id:guid} |
maintenance-request.read |
|
| POST |
/ |
maintenance-request.create |
CreateRequestCommand |
| POST |
/{id:guid}/promote |
maintenance-request.review |
PromoteToWorkOrderCommand |
| POST |
/{id:guid}/reject |
maintenance-request.review |
RejectRequestCommand { rejectionReason } |
| GET |
/export |
maintenance-request.export |
|
WorkOrdersController (api/v1/workorders)
| Method |
Path |
Permission |
Notes |
| GET |
/ |
work-order.read |
Query: page, pageSize, status, type, assetId, assignedToUserId, mineOnly, search, code, priority, scheduledFrom, scheduledTo |
| GET |
/{id:guid} |
work-order.read |
|
| POST |
/ |
work-order.create |
CreateWorkOrderCommand |
| POST |
/{id:guid}/assign |
work-order.assign |
AssignWorkOrderCommand (user XOR vendor); emits work-order.assigned |
| POST |
/{id:guid}/start |
work-order.update |
Assigned → InProgress; sets Asset.Status=InMaintenance, captures previous |
| POST |
/{id:guid}/complete |
work-order.close |
CompleteWorkOrderCommand { actualEnd?, downtimeHours?, laborCost?, partsCost?, currencyCode?, resolutionNotes? }; restores asset previous status |
| POST |
/{id:guid}/cancel |
work-order.update |
|
| GET |
/export |
work-order.export |
|
NotificationsController (api/v1/notifications)
| Method |
Path |
Permission |
Notes |
| GET |
/ |
[Authorize] |
Query: page, pageSize, unreadOnly |
| GET |
/unread-count |
[Authorize] |
Returns { unreadCount } |
| POST |
/{id:guid}/read |
[Authorize] |
|
| POST |
/read-all |
[Authorize] |
|
| DELETE |
/{id:guid} |
[Authorize] |
|
| DELETE |
/ |
[Authorize] |
Query: onlyRead (default false); returns { removed: int } |
Notifications are scoped to the caller — RecipientUserId == currentUser — so [Authorize] alone is sufficient.
NotificationPreferencesController (api/v1/notification-preferences)
| Method |
Path |
Permission |
Notes |
| GET |
/ |
[Authorize] |
The caller's preferences |
| PUT |
/ |
[Authorize] |
UpdateMyPreferencesCommand |
NotificationTemplatesController (api/v1/notification-templates)
| Method |
Path |
Permission |
Notes |
| GET |
/ |
notification-template.read |
All templates |
| GET |
/{id:guid} |
notification-template.read |
|
| POST |
/ |
notification-template.update |
CreateTemplateCommand |
| PUT |
/{id:guid} |
notification-template.update |
UpdateTemplateCommand |
| DELETE |
/{id:guid} |
notification-template.delete |
|
| GET |
/events |
notification-template.read |
The NotificationEventCatalog |
| POST |
/preview |
notification-template.read |
PreviewTemplateCommand — render a draft with sample merge fields |
| GET |
/available |
[Authorize] |
Identifiers + subjects only — for the user's preferences page |
DocumentsController (api/v1/documents)
| Method |
Path |
Permission |
Notes |
| GET |
/{id:guid} |
document.read |
Metadata |
| GET |
/by-owner |
document.read |
Query: ownerType, ownerId |
| GET |
/{id:guid}/content |
document.read |
Streams the bytes |
| POST |
/ |
document.upload |
multipart, ≤100 MB. Form: file, category?, nameEn?, nameAr?, ownerType?, ownerId? (auto-link if provided) |
| DELETE |
/{id:guid} |
document.delete |
|
| POST |
/{id:guid}/links |
document.upload |
LinkDocumentCommand |
| DELETE |
/links/{linkId:guid} |
document.delete |
Unlink (document remains) |
ReportsController (api/v1/reports)
Six business reports, each with a paged-list and an export endpoint. Export endpoints check report.{key}.read first, then programmatically gate by format on report.{key}.export-excel vs report.{key}.export-pdf.
Asset Inventory (/asset-inventory)
| Method |
Path |
Permission |
| GET |
/asset-inventory |
report.asset-inventory.read |
| GET |
/asset-inventory/export |
report.asset-inventory.read AND (export-excel or export-pdf) |
Query: page, pageSize, search, organizationId, locationId, classificationId, statusId, isCritical, includeDescendants.
Audit Results (/audit-results)
| Method |
Path |
Permission |
| GET |
/audit-results |
report.audit-results.read |
| GET |
/audit-results/export |
report.audit-results.read + format gate |
Query: search, planId, assetId, outcome, reviewStatus, submittedFrom, submittedTo.
Audit History (/audit-history)
| Method |
Path |
Permission |
| GET |
/audit-history |
report.audit-history.read |
| GET |
/audit-history/export |
report.audit-history.read + format gate |
Query: search, assetId, planId, outcome, reviewStatus. (dueFrom/dueTo filters were removed when DueDate left the system.)
Maintenance History (/maintenance-history)
| Method |
Path |
Permission |
| GET |
/maintenance-history |
report.maintenance-history.read |
| GET |
/maintenance-history/export |
report.maintenance-history.read + format gate |
Query: search, assetId, vendorId, type, priority, status, completedFrom, completedTo.
Transfer History (/transfer-history)
| Method |
Path |
Permission |
| GET |
/transfer-history |
report.transfer-history.read |
| GET |
/transfer-history/export |
report.transfer-history.read + format gate |
Query: search, assetId, fromOrganizationId, toOrganizationId, fromLocationId, toLocationId, status, requestedFrom, requestedTo.
Checkout Activity (/checkout-activity)
| Method |
Path |
Permission |
| GET |
/checkout-activity |
report.checkout-activity.read |
| GET |
/checkout-activity/export |
report.checkout-activity.read + format gate |
Query: search, assetId, custodianId, status, overdueOnly, checkedOutFrom, checkedOutTo.
HierarchyConfigController (api/v1/hierarchy-config)
| Method |
Path |
Permission |
Notes |
| GET |
/{entityType} |
hierarchy-config.read |
entityType = Location / Organization / Classification |
| PUT |
/{entityType} |
hierarchy-config.update |
SetHierarchyConfigCommand |
AppSettingsController (api/v1/app-settings)
| Method |
Path |
Permission |
Notes |
| GET |
/ |
[Authorize] |
Singleton; primary/secondary language codes |
| PUT |
/ |
app-settings.update |
UpdateAppSettingsCommand |
TranslationsController (api/v1/translations)
| Method |
Path |
Permission |
Notes |
| GET |
/ |
translation.read |
All override rows |
| GET |
/active/{language} |
[Authorize] |
Resolved key→value map for one language |
| PUT |
/{language}/{key} |
translation.update |
Body { value } |
| DELETE |
/{language}/{key} |
translation.update |
Removes the override (resets to bundled default) |
| GET |
/export |
translation.export |
|
EmailProviderSettingsController (api/v1/email-settings)
| Method |
Path |
Permission |
Notes |
| GET |
/ |
email-settings.manage |
Singleton |
| PUT |
/ |
email-settings.manage |
UpdateEmailProviderSettingsCommand (encrypts password) |
| POST |
/test |
email-settings.manage |
TestEmailProviderSettingsCommand { toAddress } — sends a one-off; always returns 200 with { success, message } |
AuditLogController (api/v1/audit-log)
| Method |
Path |
Permission |
Notes |
| GET |
/ |
audit-log.read |
Query: page, pageSize, entityName, entityId, userId, action, from, to |
| GET |
/entity-names |
audit-log.read |
Distinct EntityNames for filter dropdown |
| GET |
/export |
audit-log.read |
`format=xlsx |
Response shapes — quick reference
// Common envelopes
PagedResult<T> = { items: T[], totalCount: number, page: number, pageSize: number }
LoginResultDto = { accessToken: string, refreshToken: string, expiresAt: string,
tokenType: 'Bearer', user: CurrentUserDto }
TokenResponse = { accessToken: string, refreshToken: string, expiresAt: string }
CurrentUserDto = { id, code, email, userName, fullNamePrimary, fullNameSecondary,
userType, preferredLanguage, preferredTheme, avatarUrl, … }
ProblemDetails = { status, title, detail?, errors?, errorCode?, required?, ... }
DocumentLinkResultDto = { id, documentId, ownerType, ownerId, linkedAt }
For the full set of DTO record signatures, search src/AssetTracking.Application/DTOs/ — the records mirror the JSON fields one-to-one (PascalCase in C#, camelCase on the wire via JsonNamingPolicy.CamelCase).
Where to go next