feat/Update_Controllers_to_Allow_for_Premium_Membership

This commit is contained in:
cesnimda
2026-08-03 09:17:28 +02:00
parent de937d25dc
commit c3f4a57195
187 changed files with 26062 additions and 991 deletions
+19
View File
@@ -0,0 +1,19 @@
# AI privacy and external-processing policy
Updated: 2026-08-03
The default execution mode is local-only. External processing of `/cv/*` payloads requires all of:
1. `Ai:ExternalProcessingEnabled=true` on the backend;
2. `EXTERNAL_AI_ENABLED=true` on the AI sidecar;
3. a configured external `Ai:ExternalProvider` / `AI_PROVIDER` (`gemini` or `groq`);
4. a current Pro/Admin entitlement resolved from the database;
5. AI enabled in the user's server-side settings; and
6. the user's explicit `ExternalAiProcessingAllowed` opt-in.
The backend adds `X-Ai-External-Allowed: true` only after that live policy check. The sidecar otherwise routes `/cv/*` to Ollama even when an external provider is configured. `/summarize` always uses the local summarization model. Provider keys remain server-side and are never returned by the settings API.
`GET/PUT /api/ai/settings` owns the user settings. Disabling AI takes effect on the next protected request and is also rechecked by the current enrichment and queued-CV workers. Existing users migrate with AI enabled to preserve current behaviour; external consent always defaults to false.
This is the privacy admission foundation, not the final routing system. AI-001/AI-002 must carry an immutable policy snapshot into durable operations, recheck it at execution, record the actual provider/reason, add bounded local-first fallback triggers and minimize each external payload. Background CV work currently fails safe to local because it has no HTTP user context.
@@ -174,6 +174,8 @@ I missing", "what happened previously". All deterministic, all owned by nothing:
| Endpoint | Reads | Owns |
|---|---|---|
| `GET /{id}/timeline` | `JobEvent` | nothing |
| `GET /{id}/interview-prep` | editable `InterviewPrepItem` board | user edits only |
| `GET /{id}/interview-prep/brief` | cached generated `InterviewPrepNote` | explicit refresh or attachment-context change |
| `GET /{id}/analysis` | `JobApplication.Description` | nothing |
| `GET /{id}/match` | `CareerProfile` + the advert | nothing |
+36
View File
@@ -0,0 +1,36 @@
# Attachment storage invariants
Updated: 2026-08-02
Attachments remain local files under `Data:AttachmentsRoot/<jobId>/` with metadata in `Attachments`. No object-store abstraction or schema migration is used.
## Invariants
- Every managed path resolves beneath `Data:AttachmentsRoot` without crossing a symlink/junction.
- Stored filenames are generated and stable. Renaming changes only the user-visible `FileName` metadata.
- A committed row normally has its final file. A temporary suffix is the only recognized recoverable exception.
- Unknown plain files are reported and preserved; reconciliation never guesses that a legacy orphan is safe to delete.
- All row lookups remain parent/job tenant-scoped.
## Durable filesystem states
| State | Meaning | Startup action |
|---|---|---|
| `<final>.uploading` and matching DB row | metadata committed; promotion was interrupted | atomically promote to `<final>` |
| `<final>.uploading` without DB row | copy/request failed before commit | purge the staging file |
| `<final>.deleting` and matching DB row | delete stopped before DB commit | restore to `<final>` |
| `<final>.deleting` without DB row | DB deletion committed; purge was interrupted | purge the quarantined file |
| plain file without DB row | unknown/legacy orphan | report only |
| DB row without final or recognized state | missing bytes | report only |
Upload validates the complete batch before copying, stages every file, commits all metadata and derived flags in one database transaction, then promotes files. A post-commit promotion failure returns 202 and leaves `.uploading` for startup recovery.
Delete first atomically renames bytes to `.deleting`, removes metadata and updates flags in one transaction, then purges. A database failure restores the file. A post-commit purge failure returns 202 and leaves a retryable marker.
## Operations
Reconciliation runs once after database initialization and before the API begins serving. It logs counts only—never file contents or paths. Nonzero missing, unsafe, unknown-orphan or failure counts require operator review. Persistent suffix-state failures are retried on the next safe service restart.
Rollback requires draining/reconciling `.uploading` and `.deleting` markers before reverting the application. Do not delete unknown plain files. The same managed-root and quarantine conventions must be reused by account deletion (SEC-009).
Object storage, content deduplication, periodic multi-replica reconciliation and destructive legacy-orphan cleanup are explicitly deferred until deployment topology or measured volume requires them.
+27
View File
@@ -0,0 +1,27 @@
# Background worker ownership and activation
Updated: 2026-08-02
## Tenant execution contract
HTTP requests derive `JobTrackerContext.CurrentUserId` from the authenticated request. Hosted services have no HTTP context, so deny-on-null query filters intentionally return no tenant rows.
`BackgroundTenantRunner` is the only worker bypass for the four job-owner schedulers. It uses `IgnoreQueryFilters` only to enumerate distinct non-empty job owners, then creates a fresh scope per owner and sets `CurrentUserService` before resolving the scoped `JobTrackerContext`. All work queries run through the normal tenant filters. An owner failure is counted and isolated; logs contain worker name, exception type and aggregate counts, not owner IDs or private content. An HTTP context cannot be replaced by a background owner.
This is a sequential, single-instance foundation. Generic operation leasing now exists in OPS-001A, but notifications, bounded AI handlers and multi-replica scheduling belong to OPS-001B/C and AI-001 and must precede activation that needs them.
## Hosted-service inventory
| Service | Tenant behavior | Side effect | Activation |
|---|---|---|---|
| `RulesHostedService` | owner runner + normal filters/per-user rules | changes job status | `Workers:RulesEnabled=false` by default; keep off until user-visible notification/audit behavior is ready |
| `FollowUpReminderHostedService` | owner runner + normal filters | sends email, then marks date | both `Workers:FollowUpRemindersEnabled` and `Email:FollowUpReminders:Enabled`; keep off until persistent notification/idempotency work |
| `DailyExportHostedService` | owner runner + normal filters; one hashed-owner atomic file | writes local JSON | both `Workers:DailyExportEnabled` and `Exports:DailyEnabled`; keep off pending retention/operator rollout |
| `JobEnrichmentHostedService` | owner runner + normal filters | deterministic tags and AI summary | `Workers:JobEnrichmentEnabled=false`; do not enable before Pro/privacy/provider/queue gates |
| `CvProcessingHostedService` | existing explicit run owner on every unfiltered query | user-requested CV parsing/AI | unchanged; persistent run rows recover at startup, process-local wake-up remains single-instance |
| `SummarizerProbeHostedService` | tenant-neutral; no user payload | AI-sidecar health probe | existing probe settings; unchanged |
| `DatabaseBackupHostedService` | tenant-neutral complete database snapshot | local backup | existing backup settings; unchanged |
## Rollout and rollback
Changing old settings alone cannot activate the four repaired workers; the new worker-specific switch must also be true. Enable one worker at a time only after its listed dependencies, fake/synthetic tests, operator monitoring and rollback are ready. Rollback is setting its worker switch to false; do not delete outputs or undo already-applied user-visible changes without a separate reviewed procedure.
+4 -4
View File
@@ -256,13 +256,13 @@ erDiagram
| Controller | Lines | Highlights |
|---|---|---|
| `JobApplicationsController` | **2313** | **38 endpoints.** CRUD, paging/filter/sort, board, reminders, stats, analytics, history, timeline, status/follow-up PATCH, soft delete/restore, duplicate-check, **plus** the whole AI surface: match-score, candidate-fit, focus-plan, interview-prep, readiness, tailored-CV draft/preview/export/generate, application-drafts, application-package, follow-up drafts + send, ai-metrics. |
| `JobApplicationsController` | **2394** | **37 endpoints.** CRUD, paging/filter/sort, board, reminders, stats, analytics, history, status/follow-up PATCH, soft delete/restore, duplicate-check, **plus** the AI surface: match-score, candidate-fit, focus-plan, generated interview-prep brief, readiness, tailored-CV draft/preview/export/generate, application-drafts, application-package, follow-up drafts + send, ai-metrics. The canonical timeline and editable interview board live in their focused controllers. |
| `ProfileCvController` | **2249** | CV upload artifacts, extraction runs, structure parsing, reprocess/rebuild/improve, rewrite-section, rewrite-preview, templates, Playwright PDF export, benchmark harness. |
| `GmailController` | **1023** | OAuth connect/callback, sync, review queue, import decisions, job matching. |
| `AuthController` | **879** | login/register/me/config, Google + Microsoft exchange and link/unlink, avatar, password change/reset, email verification, session cookie + CSRF. |
| `AdminSystemController` | 342 | System readiness (DB/Gmail/AI). |
| `TwoFactorController` | 341 | TOTP enrol/verify/disable, recovery codes. |
| `AttachmentsController` | 245 | Multipart upload to disk, download, rename, delete, purpose/AI-inclusion metadata. |
| `AttachmentsController` | 342 | Validated staged upload, managed-root download, metadata-only rename, quarantined delete, purpose/AI-inclusion metadata and restart reconciliation. See `attachment-storage.md`. |
| `UsersController` | 229 | Admin user/role management. |
| `AdminAuditController` | 219 | Audit trail. |
| `CorrespondenceController` | 185 | Per-job messages CRUD. |
@@ -294,7 +294,7 @@ erDiagram
| `CvProcessingHostedService` + `CvProcessingQueue` | Process-local wake-up queue for CV extraction; queued/running database work is recovered at startup |
| `DatabaseBackupHostedService``DatabaseBackupRunner` | Automated DB backup (`VACUUM INTO`, server-derived path) |
Caches and worker coordination are process-local. CV work itself is durable and recovered after restart, but the worker remains a deliberate single-instance design without database leasing.
Rules, reminders, daily export and enrichment now enumerate owners explicitly, then re-enter normal tenant-filtered scopes. All four have deny-by-default worker switches and remain inactive until their notification/privacy/entitlement/operations prerequisites are ready; see `docs/architecture/background-workers.md`. Caches and worker coordination are process-local. CV work itself is durable and recovered after restart, but the worker remains a deliberate single-instance design without database leasing.
---
@@ -321,7 +321,7 @@ Caches and worker coordination are process-local. CV work itself is durable and
## 10. Email
`SmtpEmailSender` + `EmailSettingsResolver`: config from env/appsettings **or** DB-stored `SystemEmailSettings` (admin-editable). Gmail SMTP + app password in prod. Flows: password reset, email verification, follow-up reminders. `App:PublicBaseUrl` builds links.
`SmtpEmailSender` + `EmailSettingsResolver`: config from env/appsettings **or** DB-stored `SystemEmailSettings` (admin-editable). Gmail SMTP + app password in prod. Flows: password reset, email verification, follow-up reminders. `App:PublicBaseUrl` is the canonical external origin for generated links, OAuth callbacks, billing redirects, secure cookies and production Host validation.
Inbound: `GmailOAuthService` (655), `MicrosoftGraphOAuthService` (507), `ImapService` (345, SSRF-guarded).
+8 -3
View File
@@ -5,9 +5,14 @@ Compose. The backend is not published directly; nginx proxies `/api`. `deploy/de
configuration, takes and verifies a provider-appropriate backup before replacement, builds/restarts the
stack, and performs health checks.
Production compose enables `Proxy:TrustForwardedHeaders` because nginx is the sole ingress, allowing
HTTPS scheme and client-IP rate limits to use one trusted forwarded hop. The development override
publishes the API directly and disables forwarded-header trust.
Production commands explicitly select `docker-compose.yml`; it publishes no application ports.
Traefik reaches frontend/nginx over `jobtracker_shared`, must match the canonical Host exactly, and
must replace `X-Forwarded-For` and `X-Forwarded-Proto`. Nginx passes those sanitized values to the
backend over the dedicated `WEB_PROXY_SUBNET`; nginx also derives its only application server name
from `APP_PUBLIC_BASE_URL` and rejects unknown Hosts except its liveness endpoint. The backend trusts
only the dedicated CIDR and one forwarded hop. Local development explicitly adds
`docker-compose.dev.yml`, which publishes ports 3000/5202, uses the localhost origin, and disables
forwarded-header trust.
Each service rotates local Docker logs at 10 MB and retains three files. Add a central sink only if
cross-host search or longer retention becomes necessary.
+25
View File
@@ -0,0 +1,25 @@
# Durable operation state
Updated: 2026-08-02
`UserOperations` is the shared persistence foundation for long-running CV, Strategy Snapshot and later AI work. It is not a workflow engine and carries no raw CV, email, prompt, job description or private note. Producers store only bounded task/policy fields plus an opaque subject reference.
## State and ownership
Allowed application states are `queued`, `running`, `waiting_for_retry`, `waiting_for_external_fallback`, `succeeded`, `failed` and `cancelled`. Every row has an owner query filter and a unique `(OwnerUserId, TaskType, IdempotencyKey)` index, so duplicate clicks for one user return the same operation while another user may use the same key safely.
Workers claim from a neutral scope with one conditional database update, receive the owner ID and lease token, then must re-enter that owner scope before heartbeat/completion/failure. Owner mutations use normal query filters plus the unguessable lease token. Claiming from an owner/HTTP scope and completing from a neutral scope are refused.
Expired leases become retryable until `MaxAttempts`; the final expiry fails. Cancellation is immediate before execution and cooperative while running; an expired cancelled lease converges to `cancelled`. Queued deadlines fail closed. Retry delay, attempts, leases, field lengths and progress percentages are bounded.
## Schema ownership
`20260802224646_AddUserOperations` is EF-owned and intentionally absent from `StartupInitializationExtensions`. Its `Up` branches by provider: native SQLite DDL from the model and explicit bounded MariaDB `varchar`/`char`, `datetime(6)` and `int` DDL. Common indexes are generated by the active provider. This incrementally reduces the dual-ownership risk from JT-019 while preserving clean MariaDB types.
Rollback requires stopping operation producers/workers, draining or explicitly cancelling active rows, retaining any referenced results, then applying the migration `Down`. Rolling an old application version against a database that still contains this additive table is safe; dropping it loses operation history and must not be done casually.
Terminal notifications are described in `notifications.md`. Authenticated owner APIs expose bounded list/detail/cancel/retry state under `/api/operations`; DTOs omit idempotency keys, leases, provider/model fields, failure text and result references.
`AiOperationAdmission` now provides the shared AI producer boundary: it rechecks live Pro/AI settings, snapshots `local_only` or `external_allowed`, applies per-user/global capacity, assigns a deadline and returns the stable `/api/operations/{id}` status URL. It stores only subject type/ID, never raw CV/email/prompt text. The current process-local admission semaphore is correct for the documented single-backend deployment; multi-replica rollout requires a database capacity reservation.
`AiOperationWorker` claims only registered task types by priority, enters the explicit owner scope, rechecks entitlement/privacy/cancellation, runs one inference by default, heartbeats the lease, enforces a timeout, classifies bounded retry/permanent failure and commits the existing terminal notification. `Workers:AiOperationsEnabled` defaults false and no production feature handler is registered yet. AI-003/004 add the Strategy/CV handlers and 202 producer endpoints; AI-002 adds provider/model concurrency, circuit and actual-provider provenance.
+15
View File
@@ -0,0 +1,15 @@
# Persistent operation notifications
Updated: 2026-08-02
`UserNotifications` provides durable, owner-scoped visibility for terminal `UserOperations`. It is not an email outbox: no SMTP or provider delivery is triggered by this record.
Each operation may have one current notification, enforced by a unique nullable `OperationId`. Success, permanent failure and cancellation update the operation and insert a generic notification in one relational database transaction. Retryable failures do not notify. Manually retrying a failed or cancelled operation removes its previous terminal notification so its next terminal outcome can create one. Duplicate terminal calls make no change.
Notification text never includes operation inputs, provider diagnostics, failure messages, CV/email/job content or result data. Read and dismiss mutations use the normal owner query filter; dismissed notifications are excluded from lists and unread counts. Authenticated APIs expose list/unread/read/dismiss under `/api/notifications`.
The frontend `/operations` page polls only while mounted, shows loading/empty/error/progress/cancellation/retry states and dispatches a local refresh event after notification mutations. The application shell polls only the unread count every 60 seconds; the reminders badge remains separate. The bell links to `/operations` and has an accessible name.
Migration `20260802225941_AddUserNotifications` is EF-owned and absent from startup reconciliation. SQLite uses native scaffolded types; MariaDB uses bounded `char`/`varchar` and `datetime(6)` columns with a foreign key that sets `OperationId` null if operation history is deleted.
Rollback requires stopping producers/workers, retaining any required notification evidence elsewhere, then applying the migration `Down`. An older application can run with this additive table still present, which is the safer application rollback. Dropping the table loses notification state.