first commit

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-28 09:16:54 +01:00
commit 32a7ba8e8f
168 files changed
+123856

No files matched your search

+222
View File
@@ -0,0 +1,222 @@
# Port coverage matrix — 0.0.1
## Scope and source identity
The supplied source code, rather than the interface concept alone, defines functional
coverage. The archive contains no `.git` metadata, so the supplied archive SHA-256
values identify the reviewed inputs:
| Archive | SHA-256 |
| --- | --- |
| `mikrotik_backup_system-master.zip` | `3be24e72b1816070a78ec7ee7336ae0a7f884db070dc019a8afdd9c229c05f47` |
| `mikrotik_backup_system_rust_migration.zip` | `65e4ee36cd60e81e4ff25a6d12d5fd7913b9360b61c96b7301b3de550c00a104` |
The source has **36 active API method/path combinations**, all represented in the
Axum router. Paths below use the default `/api` prefix; `API_PREFIX` remains
configurable.
The test column identifies source tests and their intended coverage. A test's
presence is not proof of a successful run. The revision-specific
[validation report](validation.md) records actual test, interface, build, and process
checks. Controlled fixture servers exercise protocols without contacting production
devices. No physical MikroTik or live Pushover account was supplied.
## Contract test key
These test names are in `tests/api_contract.rs`.
| Key | Full test name |
| --- | --- |
| A | `auth_json_form_preferences_password_and_expiry` |
| R | `registration_and_login_rate_limit` |
| D | `devices_defaults_validation_secrets_and_disabled_operations` |
| O | `ownership_applies_to_every_device_and_backup_operation` |
| F | `files_filters_preview_diff_download_zip_and_bulk_deletion` |
| S | `settings_secrets_validation_reveal_scheduler_and_notification_errors` |
| L | `logs_retention_and_embedded_web_do_not_expose_storage` |
## API operations
| Method and path | Rust file / handler | Test and coverage |
| --- | --- | --- |
| `POST /api/auth/register` | `src/api/auth.rs` / `register` | R: registration disabled/enabled, conflicts, validation |
| `POST /api/auth/login` | `src/api/auth.rs` / `login` | A: JSON and form; R: failed-login throttling |
| `GET /api/auth/me` | `src/api/auth.rs` / `me` | A: account, missing/invalid token, no password hash in JSON |
| `PUT /api/auth/preferences` | `src/api/auth.rs` / `preferences` | A: four languages, font, unsupported values |
| `POST /api/auth/change-password` | `src/api/auth.rs` / `change_password` | A: current password, new login, rejection of old password |
| `GET /api/dashboard` | `src/api/system.rs` / `dashboard` | O: owner-scoped counts; F: counts updated after deletion |
| `GET /api/routers` | `src/api/devices.rs` / `list` | O: user list isolation |
| `GET /api/routers/ping-statuses` | `src/api/devices.rs` / `ping` | D: disabled flag; ICMP tests in `routeros.rs` |
| `POST /api/routers` | `src/api/devices.rs` / `create` | D: RouterOS/SwOS, default ports, names, secrets |
| `GET /api/routers/{id}` | `src/api/devices.rs` / `get_one` | D: metadata and flags; O: another user's record |
| `PUT /api/routers/{id}` | `src/api/devices.rs` / `update` | D: validation, flags, secret preservation/clearing; O |
| `DELETE /api/routers/{id}` | `src/api/devices.rs` / `remove` | D: deletion and 404; O: ownership protection |
| `GET /api/routers/{id}/test-connection` | `src/api/devices.rs` / `test_connection` | O: authorization; local protocol fixtures in `routeros.rs` |
| `GET /api/backups` | `src/api/backups.rs` / `list` | F: type, device, day, search, sorting; O |
| `GET /api/backups/router/{id}` | `src/api/backups.rs` / `for_router` | O: device authorization |
| `POST /api/backups/routers/export-all` | `src/api/backups.rs` / `export_all` | D: disabled devices skipped; `backup::run_all` returns per-device results |
| `POST /api/backups/routers/binary-all` | `src/api/backups.rs` / `binary_all` | D: disabled devices skipped; `backup::run_all` returns per-device results |
| `POST /api/backups/router/{id}/export` | `src/api/backups.rs` / `export` | D: policies and unsupported SwOS export; O: ownership; SSH adapter results in validation report |
| `POST /api/backups/router/{id}/binary` | `src/api/backups.rs` / `binary` | D: policies; O: ownership; SwOS fixture and SSH adapter checks |
| `POST /api/backups/router/{id}/upload/{backup_id}` | `src/api/backups.rs` / `upload` | O: both resources owned; SSH/SFTP adapter check; upload does not restore |
| `DELETE /api/backups/{id}` | `src/api/backups.rs` / `remove` | F: file and metadata deletion; O: ownership |
| `GET /api/backups/{id}/download` | `src/api/backups.rs` / `download` | F: exact binary bytes and Content-Type; O |
| `GET /api/backups/{id}/view` | `src/api/backups.rs` / `view` | F: export text and rejection of binary content; O |
| `POST /api/backups/{id}/email` | `src/api/backups.rs` / `email` | O: ownership; `smtp_delivery_with_attachment_against_local_fixture` for SMTP |
| `GET /api/backups/{left}/diff/{right}` | `src/api/backups.rs` / `diff_json` | F: statistics/text and rejection of binary content; O: both resources |
| `GET /api/backups/{left}/diff/{right}/html` | `src/api/backups.rs` / `diff_html` | F: HTML; O; `escapes_configuration_and_file_names` |
| `POST /api/backups/bulk` | `src/api/backups.rs` / `bulk` | F: ZIP, no partial mutation for an invalid ID, deduplication; O |
| `GET /api/settings` | `src/api/system.rs` / `settings` | `settings_secrets_round_trip_without_public_exposure`; S: saved response |
| `GET /api/settings/scheduler-status` | `src/api/system.rs` / `scheduler_status` | S: timezone and five jobs; cron/DST tests in `scheduler.rs` |
| `PUT /api/settings` | `src/api/system.rs` / `update_settings` | S: cron, preserve/clear secrets, invalid input rejected before save |
| `POST /api/settings/reveal-ssh-key` | `src/api/system.rs` / `reveal_ssh_key` | S: incorrect and correct account password |
| `POST /api/settings/test-email` | `src/api/system.rs` / `test_email` | S: configuration errors; local SMTP provider fixture |
| `POST /api/settings/test-pushover` | `src/api/system.rs` / `test_pushover` | S and `missing_pushover_settings_fail_before_network`; no live account delivery |
| `GET /api/logs` | `src/api/system.rs` / `logs` | L: limit; original id/message/timestamp model |
| `DELETE /api/logs/older-than/{days}` | `src/api/system.rs` / `delete_logs` | L: validation and actual removal of old records |
| `GET /api/health` | `src/api/system.rs` / `health` | L: public health check, version, configurable prefix |
Backup list parameters are `search`, `backup_type=export|binary`, `router_id`,
`created_on=YYYY-MM-DD`, `sort_by=created_at|file_name|backup_type|router_name`, and
`order=asc|desc`. Logs retain `limit` in the range 1–500. Bulk operations accept
`action=delete|download` and `backup_ids`. Login accepts `username`/`password`;
password change uses `current_password`/`new_password`; SSH key reveal uses
`password`; preferences use `preferred_language`/`preferred_font`. A login response
contains `access_token`, `token_type=bearer`, and `user`.
## Models and source fields
Models are explicitly typed in `src/models.rs`. `src/db.rs` implements persistence,
current-schema validation, and explicit import. `src/schema.sql` is the complete
fresh-install schema. Revision 4 does not run incremental schema migrations at startup.
| Model / group | Preserved or added fields | Notes |
| --- | --- | --- |
| User | `id`, `username`, `password_hash`, `created_at`, `preferred_language`, `preferred_font` | Password hashes are never serialized. The user DTO has the same four public fields as the source. |
| Router identity | `id`, `owner_id`, `name`, `device_type`, `host`, `port`, `ssh_user`, `created_at` | Every device/backup access checks ownership. |
| Router credentials | `ssh_key`, `ssh_password` | Public responses contain null and presence flags; values remain stored in the database. |
| Router SwitchOS source | `use_global_switchos_credentials` | Nullable addition: true selects the global pair, false selects the local pair, null preserves legacy fallback. Changing mode preserves local secrets. |
| Router connection result | `last_connection_status`, `last_connection_tested_at`, `last_connection_error`, `last_connection_hostname`, `last_connection_model`, `last_connection_version`, `last_connection_uptime`, `last_connection_transport`, `last_connection_server`, `last_connection_auth_mode`, `last_connection_http_status`, `last_connection_backup_available` | Unknown values remain null; errors are not presented as success. |
| Router policies | `disable_all_backups`, `disable_export_backups`, `disable_binary_backups`, `disable_ping` | Enforced server-side, including scheduled and batch operations. |
| Router DTO | `effective_username`, `uses_global_ssh_key`, `has_effective_ssh_key`, `uses_global_switchos_credentials`, `has_effective_password`, `has_own_password`, `supports_export`, `supports_restore_upload` | Derived from the selected credential source and device type without disclosing secrets. |
| RouterTestConnection | `success`, `tested_at`, `model`, `uptime`, `hostname`, `version`, `error`, `transport`, `server`, `auth_mode`, `http_status`, `backup_available` | The same result is persisted in the device's last_connection fields. |
| RouterPingStatus | `router_id`, `reachable`, `latency_ms`, `disabled`; optional `error` | Batch response contains `items`. Diagnostic errors distinguish missing ICMP permissions from an ordinary timeout. |
| Backup | `id`, `router_id`, `file_path`, `file_name`, `backup_type`, `checksum`, `created_at` | Additional DTO fields: `router_name`, `device_type`, `file_size`. Import copies existing bytes without converting their format. |
| OperationLog | `id`, `message`, `timestamp` | No invented level/component/router_id fields. |
| Dashboard | `routers_count`, `export_count`, `binary_count`, `total_backups`, `storage`, `recent_logs` | Device/backup counts are owner-scoped; logs remain global as in the source. |
| StorageStats | `total`, `used`, `free`, `folder_used`, `usage_percent` | `usage_percent` measures the data directory against total disk capacity, matching the source. |
| Settings retention | `backup_retention_days`, `log_retention_days` | 0 means an age threshold of zero days, matching the original retention function; it does not disable cleanup. |
| Settings scheduling | `export_cron`, `binary_cron`, `retention_cron`, `enable_auto_export`, `connection_test_interval_minutes` | Five-field cron, configured timezone, Monday=0 as in APScheduler. |
| Settings device defaults | `global_ssh_key`, `default_switchos_username`, `default_switchos_password` | Revealing the global key requires password verification. |
| Settings notifications | `pushover_token`, `pushover_userkey`, `notify_failures_only`, `smtp_host`, `smtp_port`, `smtp_login`, `smtp_password`, `smtp_notifications_enabled`, `recipient_email` | SMTP, attachments, Pushover, provider tests, and failures-only mode. |
| Settings DTO | `id`, `has_global_ssh_key`, `has_default_switchos_credentials` | Additional `has_smtp_password`, `has_pushover_token`, `has_pushover_userkey`, and `has_default_switchos_password` flags support editing masked secrets. |
| Diff JSON | `left_backup_id`, `right_backup_id`, `left_file_name`, `right_file_name`, `diff_text`, `stats`, `lines` | Statistics: added/removed/modified/context. Lines: type, left_number/right_number, left_text/right_text. Standalone `diff_html` is generated only by the dedicated HTML endpoint to avoid duplicating large payloads in the browser response. |
| SchedulerStatus | `timezone`, `running`, `jobs` | Job fields: key, label, enabled, cron, description, description_params, valid, next_runs, error. |
## Client views and behavior
| Source view / behavior | Implementation |
| --- | --- |
| `/login`, `/register`, session restoration, auth/guest guards, Bearer token | `web/js/core.js`, `web/js/views.js`; actual auth API |
| `/` dashboard | API counts, storage, recent backups/logs, and batch actions |
| `/devices`, alias `/routers` | List, search, device state, create, and edit |
| `/devices/:id`, alias `/routers/:id` | Connection test, metadata, and backups; no RSC exports or diff for SwitchOS; comparison only for RouterOS text exports |
| `/files` | Filters, sorting, selection, ZIP, deletion, preview, email, and diff |
| `/diff-configs` | Two exports, counts, line numbers, side-by-side/unified views, and search |
| `/logs` | Original messages and timestamps, cleanup by age in days |
| `/settings` | Retention, cron, global credentials, SMTP/Pushover, tests, and next runs |
| `/settings?tab=automation`, alias `/schedule` | Flat schedule controls; no separate Schedule navigation item |
| `/settings?tab=account`, alias `/change-password` | Password change in account settings; old route redirects to that tab |
| `/docs`, `/openapi.json`, alias `/redoc` | Local Swagger UI, full contract, authentication, and real API requests |
| Language and font | `pl`, `en`, `es`, `no`; `default`, `adwaita_mono`, `hack`; persisted user preferences |
| Theme, sidebar, responsive layout, toasts, confirmations | `web/css/app.css`, `web/js/core.js`, `web/js/views.js`; no sample devices inserted into the database |
| CSS/JavaScript delivery | Modular `web/` sources bundled into one `app.css` and one `app.js`, embedded in the executable |
Default font families preserve the source stacks: Inter/system UI for content and
Roboto Mono/IBM Plex Mono/SFMono-Regular/Consolas for headings. Adwaita Mono and Hack
preferences select their respective stacks. Specific fonts depend on what is
installed on the client device.
## Services, configuration, and data
| Area | Implementation | Tests / evidence |
| --- | --- | --- |
| Argon2id and Python password compatibility | `security.rs` | `new_passwords_use_argon2id`, `verifies_python_passlib_fixture_without_reset`, `verifies_legacy_werkzeug_fixture`, `malformed_hashes_fail_closed` |
| JWT HS256, username subject, expiration, and nbf | `security.rs` | `jwt_has_original_username_subject`, `expired_and_wrong_algorithm_tokens_are_rejected`, `signed_malformed_or_missing_expiration_is_rejected`, `not_before_is_optional_but_must_be_valid_and_reached`, `verifies_independent_python_hmac_jwt_fixture` |
| Environment, SQLite URL, timezone, initial secret | `config.rs` | `generated_secret_survives_restart`, `accepts_legacy_sqlite_url_and_custom_prefix`, `rejects_unsafe_configuration` |
| Fresh schema and existing-schema validation | `db.rs`, `src/schema.sql` | Fresh initialization and incompatible-schema rejection regressions in `db.rs`; `generated_bootstrap_password_is_saved_and_valid` |
| Explicit database/file import | `db.rs` | `import_copies_files_and_preserves_source`, `failed_import_does_not_publish_partial_database`, `orphaned_legacy_backups_are_not_silently_skipped`, `import_rejects_corrupted_backup_before_publication`, `import_rejects_backup_symlink_outside_selected_root` |
| Import exclusivity and atomic publication | `db.rs` | `import_obeys_storage_lock_and_keeps_source_untouched`, `atomic_copy_publication_refuses_existing_destination` |
| Import wrapper | `import-python.sh`, `tests/import_launcher.rs` | `help_and_invalid_sources_do_not_build_or_import`, `resolves_source_and_preserves_arguments_and_destination_settings`, `forwards_import_failure_without_falling_back_to_serve` |
| Secret redaction and preservation | `models.rs`, `db.rs` | `settings_keep_secrets_and_clear_explicitly`, `invalid_patch_is_atomic`, `generic_serialization_does_not_leak_secrets`, `settings_secrets_round_trip_without_public_exposure` |
| RouterOS SSH export/binary, SFTP/SCP, host keys, timeout | `routeros.rs`, `backup.rs` | Controlled SSH/SFTP server checks in the validation report; no physical MikroTik supplied |
| Native SSH library fixes | `vendor/libssh2-sys` | Patch reproduction and `tests/run-security-regressions.sh`; sources, hashes, and limits in [dependency-security.md](dependency-security.md) |
| SwOS through the existing device model | `routeros.rs` | `switchos_digest_probe_and_binary_transfer_against_local_fixture`, `switchos_basic_fallback_and_html_rejection` |
| ICMP ping | `routeros.rs` | `native_icmp_loopback_and_disabled_flag`, `icmp_checksum_verifies_packet`; non-root Linux requires configured `net.ipv4.ping_group_range` |
| Atomic files, checksums, directory boundaries, device locks | `backup.rs` | `checksum_matches_sha256`, `file_names_cannot_escape`, `deletion_allows_missing_file_but_rejects_escape`, `symlink_cannot_escape_data_directory`, `device_lock_releases_on_drop` |
| Python/Rust timestamp retention | `backup.rs` | `supports_python_and_rust_timestamps`, `retention_uses_age_and_zero_is_not_disabled` |
| Diff and escaped HTML | `diff.rs` | `reports_all_line_types`, `escapes_configuration_and_file_names`, `final_newline_and_crlf_are_normalized`, `tracks_blank_lines_in_replacements`, `unified_output_matches_python_difflib_golden_fixtures` against 32 Python difflib fixtures |
| Cron and DST | `scheduler.rs` | `validates_cron_and_ranges`, `monday_zero_matches_apscheduler`, `timezone_and_dst_preview`, `named_weekdays_and_last_day`, `rare_calendar_schedule_is_not_lost` |
| SMTP/Pushover/error redaction | `notifications.rs` | `smtp_delivery_with_attachment_against_local_fixture`, `missing_pushover_settings_fail_before_network`, `error_redaction_covers_global_secret_fields` |
All source environment variables are mapped: `APP_NAME`, `APP_ENV`, `SECRET_KEY`,
`JWT_ALGORITHM` (HS256 only), `ACCESS_TOKEN_EXPIRE_MINUTES`, `DATABASE_URL`, `DATA_DIR`,
`ALLOW_REGISTRATION`, `API_PREFIX`, `TIMEZONE`, `DEFAULT_ADMIN_USERNAME`,
`DEFAULT_ADMIN_PASSWORD`, `SMTP_STARTTLS`, `SMTP_TIMEOUT_SECONDS`, and `CORS_ORIGINS`.
`APP_PORT` controls the default port. `LISTEN_ADDR`, `DATABASE_PATH`,
`SSH_TIMEOUT_SECONDS`, `SSH_HOST_KEY_POLICY`, and `MAX_PARALLEL_BACKUPS` are explicit
deployment extensions.
## Intentional security and reliability changes
- No shared `admin/admin` password. A fresh installation generates an initial
password in a private file unless one is configured. Registration is disabled
by default and can be explicitly enabled with `ALLOW_REGISTRATION=true`.
- Public responses redact secrets. Blank/null secret input preserves the value;
explicit `clear_*` removes it. Upload and diff check ownership of both resources.
- SSH host keys are verified. Default `accept-new` permits the first connection,
records the key, and rejects changes. Explicit `strict` requires a trusted
known_hosts entry. A host key is distinct from a user's authentication private key.
- Directory and symlink checks protect backups. ZIP responses stream a temporary
file rather than buffering the complete archive in memory. Bulk downloads are
limited to 2 GiB, and email attachments to 25 MiB; exceeding a limit is an error.
- New backup filenames include a finer-grained timestamp and random suffix so
concurrent operations cannot overwrite each other. Imported names and contents
remain supported.
- External service failures return controlled HTTP 502 responses with secret values
redacted. A notification failure does not remove or invalidate a successful backup.
- Naive SQLite timestamps are presented as UTC ISO 8601, preserving the source's
actual time without relying on the browser timezone to interpret it.
- Structured diff corrects old blank-line numbering in replacement blocks: an empty
existing line is not treated as a missing line. Unified output is checked against
32 file-based fixtures generated by Python difflib.
- Empty cron expressions disable their schedules. Invalid expressions, invalid
ports, negative retention, and excessive request sizes are rejected. Jobs are
prevented from overlapping themselves.
- Revision 4 initializes only a fresh schema and validates an existing database
before use. There are no startup migrations or backup-layout conversions. Python
import is explicit, reads the source without modifying it, and requires a new
destination. See [data-import.md](data-import.md).
- Runtime is one Rust process with SQLite, scheduling, and an embedded interface.
Python, Node, Angular, and nginx are not required application processes.
## Concept image and inactive source code
| Item | Decision |
| --- | --- |
| Git integration / commits / synchronization | No source model, endpoint, or service exists; the concept screen is not represented as a working feature. |
| Arbitrary job CRUD | The source has system jobs controlled through settings; that behavior is preserved. |
| `/swos-beta/probe`, `/swos-beta/download` | The router was not included in `backend/app/api/__init__.py`; no new public beta API is activated. |
| Existing `switchos` type | Preserved: ordinary connection tests and binary backups invoke SwOS code through the original RouterService despite the inactive separate beta router. |
| Concept's restore action | The source only transfers a binary file to RouterOS. Upload is preserved without automatic restore or restart. |
| Invented network traffic or log levels | Views use actual API data rather than demo measurements or fields absent from the model. |
## Evidence limits
No production database, physical routers, device host keys, or provider accounts were
supplied. Local fixture checks do not establish compatibility with every firmware
version. For a deployment transition, follow the explicit
[import procedure](data-import.md), verify a connection to one real device, and
confirm that its backup can be downloaded. Starting the new server activates
schedules from the imported settings, so stop the old processes first. Release tests
do not automatically restore any device configuration.