handler

package
v0.0.0-...-5dab014 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 13, 2026 License: MIT Imports: 46 Imported by: 0

Documentation

Overview

Package handler — audit_log.go.

See docs/api/handlers/audit-log.md for the filter shape, cursor pagination semantics, and ACL boundary discussion.

Package handler — billing.go

BillingHandler serves the read-only usage-transparency endpoint GET /businesses/{id}/billing/summary. It exposes plan + credits + usage; no payment surface (that is Track-B). Gated by authz.PermBillingRead.

Package handler — business_deletion.go

DELETE /api/v1/businesses/{id} and POST /api/v1/businesses/{id}/restore. Both routes sit inside the /businesses/{id} group (RequireBusinessAccess); the OWNER-role gate lives in the service layer and surfaces as 403. The delete endpoint is idempotent — a second call returns 423 from the service.

Package handler — channel_request.go.

ChannelRequestHandler implements the not-yet-supported-channel fake-door: POST /businesses/{id}/channel-requests records demand for a channel OneVoice does not support yet, and GET returns the per-channel demand aggregate for the business. Both are business-scoped (RequireBusinessAccess + content perm) so a caller only ever writes or reads its own tenant's signals.

Package handler — consents.go

Three endpoints:

- POST /auth/consents — submit the re-consent modal (Surface E). 409 version_mismatch when the build's currentVersion has bumped since the modal was rendered; 400 consent_required on missing slugs; 204 on success. - GET /users/me/consents — Surface F (Withdraw / status list). - POST /users/me/consents/pdn/withdraw — Surface F danger CTA. Triggers the deletion flow; 423 when the account is already mid-deletion.

All three routes JWT-required + always-reachable (precedent): they are NOT wrapped by RequireVerifiedEmail* or BlockWritesDuringGrace because the right-to-erasure / right-to-withdraw cannot be gated by other middleware (152-ФЗ Art. 21).

Package handler — feedback.go.

FeedbackHandler implements POST /feedback — in-app user feedback capture. JWT-required, business-agnostic (the widget is reachable everywhere, including before an organization exists).

Package handler — HITL resolve + resume + GET /tools endpoints. See docs/api/handlers/hitl.md.

Package handler — invitations.go.

See docs/api/handlers/invitations.md for the invitation lifecycle, ACL boundaries, and the race-safe single-use guarantee on accept.

Package handler — landing.go.

LandingHandler serves public waitlist signups, channel votes, and CTA events. Requests are rate-limited per IP and successful captures return 204.

Package handler — user_deletion.go

DELETE /api/v1/users/me and POST /api/v1/users/me/restore. Both routes are JWT-required but NOT wrapped by RequireVerifiedEmail* (right to erasure cannot be gated by verification) and NOT wrapped by BlockWritesDuringGrace (the restore endpoint is the explicit escape hatch from the grace state; the delete endpoint is idempotent — second call returns 423 from the service layer, not from middleware).

Index

Constants

View Source
const (
	DefaultTaskLimit = 20
	MaxTaskLimit     = 100
)

Constants for task pagination

View Source
const (
	DefaultConversationLimit = 20
	MaxConversationLimit     = 100
)

Pagination + ObjectID-length constants. See docs/api/handlers/conversation.md.

View Source
const (
	// Lockout / SmartCaptcha codes for /auth/login.
	ErrCodeAccountLocked   = "account_locked"
	ErrCodeCaptchaRequired = "captcha_required"
	ErrCodeCaptchaInvalid  = "captcha_invalid"

	// Internal-error codes for the auth handler family.
	ErrCodeRegistrationInviteRequired  = "registration_invite_required"
	ErrCodeRegistrationGateUnavailable = "registration_gate_unavailable"
	ErrCodeRegisterInternal            = "register_internal"        // Register, post-create
	ErrCodeAutoLoginFailed             = "auto_login_failed"        // Register, auto-login
	ErrCodeRefreshInternal             = "refresh_internal"         // RefreshToken
	ErrCodeLogoutInternal              = "logout_internal"          // Logout
	ErrCodeGetUserInternal             = "get_user_internal"        // Me
	ErrCodeChangePasswordInternal      = "change_password_internal" // ChangePassword
	ErrCodeUpdateLocaleInternal        = "update_locale_internal"   // UpdatePreferredLocale
	ErrCodeUpdateProfileInternal       = "update_profile_internal"  // UpdateProfile
)

Machine-readable error code constants. The frontend resolves each code to a Russian string via the i18n catalog (messages/ru.json under common.errors.*).

View Source
const (
	DefaultPostLimit = 20
	MaxPostLimit     = 100
)

Constants for post pagination

View Source
const (
	DefaultReviewLimit = 20
	MaxReviewLimit     = 100
)

Constants for review pagination

View Source
const ResumeBatchHeader = "X-Onevoice-Resume-Batch-Id"

ResumeBatchHeader is the explicit HITL resume header. Pinned to the handler package so router + tests can keep using handler.ResumeBatchHeader.

Variables

View Source
var SoleOwnerExtractor func(err error) (rows []SoleOwnerEntry, ok bool)

SoleOwnerExtractor is a package-level hook the wire layer fills with a closure that runs errors.As against *service.ErrSoleOwnerBusinesses. Kept as a hook so this handler package doesn't import the service package's concrete error type (which would force the service package to re-import a handler-side definition — back-pointer that breaks the layering).

Functions

func EmailVerificationErrorStatus

func EmailVerificationErrorStatus(code string) int

EmailVerificationErrorStatus maps verification public codes to canonical HTTP status codes. Mirrors the frontend services/frontend/lib/error_mapping.ts COPY map (21-CROSS-PLAN-CONTRACTS §4) so the backend and frontend agree on the wire shape.

Exposed as a func (not a map literal) so the constants reference http.Status* directly — easier to grep + harder to type-mismatch.

Types

type AccountDeletionInfo

type AccountDeletionInfo = openapi.AccountDeletionInfo

AccountDeletionInfo carries the 30-day grace window timestamps for /auth/me.

type AccountDeletionServiceAPI

type AccountDeletionServiceAPI interface {
	RequestDeletion(ctx context.Context, userID uuid.UUID, password, clientIP, userAgent, reason string) error
	CancelDeletion(ctx context.Context, userID uuid.UUID, clientIP, userAgent string) error
	GetScheduledDeletionAt(ctx context.Context, userID uuid.UUID) (time.Time, error)
}

AccountDeletionServiceAPI is the slice of *service.AccountDeletionService the handler consumes. Declared as an interface here so handler tests can pass an in-memory double (mirrors PasswordResetServiceAPI and EmailVerificationServiceAPI).

RequestDeletion grew a trailing `reason` parameter so ConsentService.WithdrawPDN can pass "consent_withdrawn" to bypass the bcrypt check. The user_deletion.go Delete handler always passes "".

type AgentTaskHandler

type AgentTaskHandler struct {
	// contains filtered or unexported fields
}

AgentTaskHandler handles agent task-related HTTP requests

func NewAgentTaskHandler

func NewAgentTaskHandler(agentTaskService AgentTaskService, hub *taskhub.Hub) (*AgentTaskHandler, error)

NewAgentTaskHandler creates a new agent task handler instance

func (*AgentTaskHandler) DismissTask

func (h *AgentTaskHandler) DismissTask(w http.ResponseWriter, r *http.Request)

DismissTask removes a failed task from the active list without deleting its audit record. It does not repeat or reverse the external action.

func (*AgentTaskHandler) ListTasks

func (h *AgentTaskHandler) ListTasks(w http.ResponseWriter, r *http.Request)

ListTasks handles GET /api/v1/tasks

func (*AgentTaskHandler) RerunVerification

func (h *AgentTaskHandler) RerunVerification(w http.ResponseWriter, r *http.Request)

RerunVerification repeats only the readonly external readback. writeLimit bounds abuse even though content.read is sufficient authorization.

func (*AgentTaskHandler) RetryTask

func (h *AgentTaskHandler) RetryTask(w http.ResponseWriter, r *http.Request)

RetryTask handles POST /api/v1/tasks/{taskId}/retry — re-dispatches a RETRYABLE failed task to its platform agent without the LLM. The original tool call was already HITL-approved before it failed at the platform, so the explicit retry is the consent and the shared HITL dedupe key guards against double-executing a call that actually landed. A non-retryable failure is rejected with a machine-readable reason: a rejected token routes to a reconnect signal, a permanent failure to "not retryable". Requires PermContentUpdate — this drives external platform work, so a read-only viewer must not trigger it.

func (*AgentTaskHandler) StreamTasks

func (h *AgentTaskHandler) StreamTasks(w http.ResponseWriter, r *http.Request)

StreamTasks handles GET /api/v1/tasks/stream — an SSE endpoint that pushes task lifecycle events (task.created, task.updated) for the authenticated user's business.

type AgentTaskService

type AgentTaskService interface {
	List(ctx context.Context, businessID uuid.UUID, filter domain.TaskFilter) ([]domain.AgentTask, int, error)
	Retry(ctx context.Context, businessID uuid.UUID, taskID string) (*domain.AgentTask, error)
	Dismiss(ctx context.Context, businessID uuid.UUID, taskID string) error
	RerunVerification(ctx context.Context, businessID uuid.UUID, taskID string) (*domain.AgentTask, error)
}

AgentTaskService defines the interface for agent task operations.

type AuditLogDTO

type AuditLogDTO = openapi.AuditEvent

AuditLogDTO is the historic local-name alias for the spec-side wire shape. Pointer fields are emitted as null when missing (no omitempty), matching the legacy struct semantics.

Details is `map[string]interface{}` in the spec (the pkg/audit RawMessage is decoded into a generic map for the wire). Re-encoding into the response envelope is semantically equivalent but NOT byte-identical with the upstream Details bytes: key order is alphabetic instead of struct- field order, and integer floats may be re-formatted by encoding/json. The UI consumes details as a generic JSON object so this is acceptable; no downstream service (orchestrator / billing / monitoring) reads the audit-list output bytes.

type AuditLogHandler

type AuditLogHandler struct {
	// contains filtered or unexported fields
}

AuditLogHandler implements GET /businesses/{id}/audit-logs.

func NewAuditLogHandler

func NewAuditLogHandler(repo AuditLogLister) *AuditLogHandler

NewAuditLogHandler constructs an AuditLogHandler.

func (*AuditLogHandler) List

List handles GET /api/v1/businesses/{id}/audit-logs. See docs/api/handlers/audit-log.md.

type AuditLogListResponse

type AuditLogListResponse = openapi.AuditLogListResponse

AuditLogListResponse is the historic local-name alias for the spec-side list envelope. NextCursor is *string (omitempty in spec); the handler emits `"next_cursor":null` when the page is short of the limit by explicitly setting the field to nil (oapi-codegen drops the omitempty in the spec via the absence of x-go-type-extra-tags, so the field stays present in the JSON envelope).

type AuditLogLister

type AuditLogLister interface {
	ListByBusinessWithActors(ctx context.Context, businessID uuid.UUID, f domain.AuditLogFilter) ([]repository.AuditLogRow, error)
}

AuditLogLister is the narrow read surface the handler depends on. Exported so wire/ can type-assert domain.AuditLogRepository into this shape.

type AuthHandler

type AuthHandler struct {
	// contains filtered or unexported fields
}

AuthHandler handles authentication endpoints.

func NewAuthHandler

func NewAuthHandler(userService UserService, secureCookies bool, auditLogger audit.Logger, jwtSecret []byte) (*AuthHandler, error)

NewAuthHandler creates a new auth handler instance.

func (*AuthHandler) ChangePassword

func (h *AuthHandler) ChangePassword(w http.ResponseWriter, r *http.Request)

ChangePassword handles PUT /api/v1/auth/password.

func (*AuthHandler) ConfirmPasswordReset

func (h *AuthHandler) ConfirmPasswordReset(w http.ResponseWriter, r *http.Request)

ConfirmPasswordReset handles POST /api/v1/auth/password-reset/confirm. On success the service has consumed the token, rotated the password hash, and wiped all refresh tokens for the user.

func (*AuthHandler) EmailBeforeVerify

func (h *AuthHandler) EmailBeforeVerify(w http.ResponseWriter, r *http.Request)

EmailBeforeVerify handles PATCH /api/v1/auth/email-before-verify. Only allowed when email_verified=false; otherwise 403 email_already_verified.

func (*AuthHandler) Login

func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request)

Login layered defense: lockout middleware short-circuits TierLocked requests, captcha gate verifies X-Captcha-Token when middleware demands it, ErrInvalidCredentials increments the lockout counter, success clears it.

func (*AuthHandler) Logout

func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request)

Logout extracts user_id from refresh-token claims BEFORE the service invalidates the token in Redis — otherwise we lose the audit attribution.

func (*AuthHandler) Me

Me uses the deletion-aware getter when wired so users inside the 30-day grace window can still exercise restore.

func (*AuthHandler) RefreshToken

func (h *AuthHandler) RefreshToken(w http.ResponseWriter, r *http.Request)

RefreshToken rotates the refresh-token cookie and mints a new access token.

func (*AuthHandler) Register

func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request)

Register handles user registration and auto-login. Validates the consents block against legalconfig.CurrentVersion(slug); missing/stale → 400 consent_required. On success, writes consents + user + verify token + outbox enqueue in the same tx.

func (*AuthHandler) RequestPasswordReset

func (h *AuthHandler) RequestPasswordReset(w http.ResponseWriter, r *http.Request)

RequestPasswordReset handles POST /api/v1/auth/password-reset/request. Returns 204 ALWAYS regardless of whether the email is registered — the service handles per-email rate-limit + dummy-audit-on-unknown-email + outbox enqueue with symmetric timing across branches. The router also wraps this route in an IP-scoped rate limit (keyed on client IP + path, NOT on the email), which caps abusive mail-trigger volume from one source. Because that limiter throttles uniformly — 429 before the handler whether or not the email is registered — it does not weaken the no-enumeration parity: it leaks nothing about which addresses exist.

func (*AuthHandler) SetConsentDiffer

func (h *AuthHandler) SetConsentDiffer(d ConsentDiffer)

SetConsentDiffer injects the ConsentService for /auth/me requiresReconsent.

func (*AuthHandler) SetEmailVerificationService

func (h *AuthHandler) SetEmailVerificationService(s EmailVerificationServiceAPI)

SetEmailVerificationService injects the email-verification dependency post-construction.

func (*AuthHandler) SetMeUserExtraGetter

func (h *AuthHandler) SetMeUserExtraGetter(fn func(ctx context.Context, userID uuid.UUID) (*domain.User, error))

SetMeUserExtraGetter injects the deletion-aware getter used by /auth/me.

func (*AuthHandler) SetPasswordResetService

func (h *AuthHandler) SetPasswordResetService(s PasswordResetServiceAPI)

SetPasswordResetService injects the password-reset dependency post-construction.

func (*AuthHandler) UpdatePreferredLocale

func (h *AuthHandler) UpdatePreferredLocale(w http.ResponseWriter, r *http.Request)

UpdatePreferredLocale handles PATCH /api/v1/auth/locale. Deliberately does NOT read Accept-Language — locale persistence is an explicit user action.

func (*AuthHandler) UpdateProfile

func (h *AuthHandler) UpdateProfile(w http.ResponseWriter, r *http.Request)

UpdateProfile handles PATCH /api/v1/auth/profile. Currently persists the display name only; the request schema bounds it to 2..100 chars.

func (*AuthHandler) VerifyConfirm

func (h *AuthHandler) VerifyConfirm(w http.ResponseWriter, r *http.Request)

VerifyConfirm handles POST /api/v1/auth/verify-email/confirm.

CRITICAL: NO Set-Cookie is emitted — no session is granted, so an attacker who registered with a victim's email cannot become the victim's session.

On invalid-failure we run a single follow-up LookupExpired to surface the "expired" UX hint — both codes are equally safe (token is burned either way).

func (*AuthHandler) VerifyResend

func (h *AuthHandler) VerifyResend(w http.ResponseWriter, r *http.Request)

VerifyResend handles POST /api/v1/auth/verify-email/resend. Auth-required.

func (*AuthHandler) WithInviteOnlyRegistration

func (h *AuthHandler) WithInviteOnlyRegistration(gate RegistrationAccessGate) *AuthHandler

WithInviteOnlyRegistration enables the closed-beta gate. Leaving it unset keeps registration open, which is the local-development default. A nil gate after enabling this mode fails closed instead of silently reopening signup.

func (*AuthHandler) WithLockout

func (h *AuthHandler) WithLockout(lock *lockout.Lockout, captcha service.SmartCaptchaVerifier, failOpen bool) *AuthHandler

WithLockout installs the lockout + SmartCaptcha verifier. Optional — when not called the handler runs without lockout / captcha.

failOpen=true logs and proceeds on ErrCaptchaTransient (Yandex outage) so legit users aren't locked out during outages; false rejects as 403.

type BillingHandler

type BillingHandler struct {
	// contains filtered or unexported fields
}

BillingHandler implements GET /businesses/{id}/billing/summary.

func NewBillingHandler

func NewBillingHandler(svc BillingSummarizer) *BillingHandler

NewBillingHandler constructs a BillingHandler.

func (*BillingHandler) Summary

func (h *BillingHandler) Summary(w http.ResponseWriter, r *http.Request)

Summary handles GET /api/v1/businesses/{id}/billing/summary.

type BillingService

type BillingService interface {
	LogUsage(ctx context.Context, log *llm.UsageLog) error
	GetDailySpend(ctx context.Context, businessID uuid.UUID, day time.Time) (float64, error)
}

BillingService is the narrow write-only surface the internal billing handler depends on. LogUsage backs the POST usage_logs endpoint; GetDailySpend backs the GET daily_spend endpoint that the orchestrator's rate-limiter consults before each chat turn.

type BillingSummarizer

type BillingSummarizer interface {
	Summary(ctx context.Context, businessID uuid.UUID) (service.BillingSummary, error)
}

BillingSummarizer is the narrow service surface the handler depends on.

type BusinessDeletionHandler

type BusinessDeletionHandler struct {
	// contains filtered or unexported fields
}

BusinessDeletionHandler owns DELETE /businesses/{id} + POST /businesses/{id}/restore.

func NewBusinessDeletionHandler

func NewBusinessDeletionHandler(svc BusinessDeletionServiceAPI, allowedOrigins []string) *BusinessDeletionHandler

NewBusinessDeletionHandler constructs the handler. allowedOrigins holds the CORS_ALLOWED_ORIGINS values for Restore's Origin-header CSRF check.

func (*BusinessDeletionHandler) Delete

Delete handles DELETE /api/v1/businesses/{id}. Response codes: 204 / 403 not-owner|origin / 404 not-found / 423 already-pending.

CSRF defense: the Origin header MUST match an allowed CORS origin (mirrors Restore). The endpoint is side-effecting (schedules business deletion), so a CSRF-vulnerable path could trigger deletion from a hostile origin.

func (*BusinessDeletionHandler) Restore

Restore handles POST /api/v1/businesses/{id}/restore. Response codes: 204 / 403 not-owner|origin / 404 no-pending / 410 too-old.

CSRF defense: Origin header MUST match an allowed CORS origin (mirrors the account-restore endpoint).

type BusinessDeletionServiceAPI

type BusinessDeletionServiceAPI interface {
	RequestDeletion(ctx context.Context, actorUserID, businessID uuid.UUID, clientIP, userAgent string) error
	CancelDeletion(ctx context.Context, actorUserID, businessID uuid.UUID, clientIP, userAgent string) error
	GetScheduledDeletionAt(ctx context.Context, businessID uuid.UUID) (time.Time, error)
}

BusinessDeletionServiceAPI is the slice of *service.BusinessDeletionService the handler consumes. Declared as an interface so handler tests can pass an in-memory double.

type BusinessHandler

type BusinessHandler struct {
	// contains filtered or unexported fields
}

BusinessHandler handles business profile endpoints

func NewBusinessHandler

func NewBusinessHandler(businessService BusinessService, syncer BusinessSyncer, objectStorage storage.Uploader) (*BusinessHandler, error)

NewBusinessHandler creates a new business handler instance. syncer may be nil; if provided, it is called asynchronously after each successful update. objectStorage may be nil in tests that do not exercise UploadLogo.

func (*BusinessHandler) CreateBusiness

func (h *BusinessHandler) CreateBusiness(w http.ResponseWriter, r *http.Request)

CreateBusiness handles POST /api/v1/businesses (BIZ-03). Creates a new business and owner membership for the authenticated user.

func (*BusinessHandler) GetBusiness

func (h *BusinessHandler) GetBusiness(w http.ResponseWriter, r *http.Request)

GetBusiness returns the business profile for the request's BusinessContext. Requires PermBusinessRead.

func (*BusinessHandler) GetBusinessToolApprovals

func (h *BusinessHandler) GetBusinessToolApprovals(w http.ResponseWriter, r *http.Request)

GetBusinessToolApprovals handles GET /business/{id}/tool-approvals. Response shape: `{"toolApprovals": {"tool_name": "auto"|"manual", ...}}`. Absence from the map means the registry floor applies. Requires PermBusinessRead.

func (*BusinessHandler) GetDescriptionTemplate

func (h *BusinessHandler) GetDescriptionTemplate(w http.ResponseWriter, r *http.Request)

GetDescriptionTemplate handles GET /business/{id}/description-template. Returns the stored template override (empty string when unset) plus the allowed placeholder names for the editor. Requires PermBusinessRead.

func (*BusinessHandler) GetDriftAlerts

func (h *BusinessHandler) GetDriftAlerts(w http.ResponseWriter, r *http.Request)

GetDriftAlerts returns the explicit, default-off drift notification consent.

func (*BusinessHandler) GetOwnerBrief

func (h *BusinessHandler) GetOwnerBrief(w http.ResponseWriter, r *http.Request)

GetOwnerBrief handles GET /businesses/{id}/owner-brief. Returns the stored weekly-owner-brief preferences. Default-on: a business that never set a preference reads back enabled=true, Monday 09:00. Requires PermBusinessRead.

func (*BusinessHandler) GetReviewAutopilot

func (h *BusinessHandler) GetReviewAutopilot(w http.ResponseWriter, r *http.Request)

GetReviewAutopilot handles GET /business/{id}/review-autopilot. Returns the stored review-reply autopilot config. When unset the response is the default-off state {enabled:false, minRating:ReviewAutopilotMinRatingFloor}. Requires PermBusinessRead.

func (*BusinessHandler) GetVoiceProfile

func (h *BusinessHandler) GetVoiceProfile(w http.ResponseWriter, r *http.Request)

GetVoiceProfile handles GET /business/{id}/voice-profile. Returns the stored brand-voice profile (empty string when unset). Requires PermBusinessRead.

func (*BusinessHandler) ListUserBusinesses

func (h *BusinessHandler) ListUserBusinesses(w http.ResponseWriter, r *http.Request)

ListUserBusinesses handles GET /api/v1/businesses (BIZ-02). Returns the businesses the authenticated user is a member of, hydrated with business name + role. Auth-only (no BusinessContext needed — the user is not yet in a business scope).

func (*BusinessHandler) SetToolsCache

func (h *BusinessHandler) SetToolsCache(c ToolsCache)

SetToolsCache wires a tools-registry cache for tool-name validation. Called after construction so existing NewBusinessHandler call sites don't churn. Safe to call with nil to disable the endpoints.

func (*BusinessHandler) UpdateBusiness

func (h *BusinessHandler) UpdateBusiness(w http.ResponseWriter, r *http.Request)

UpdateBusiness updates the business profile for the request's BusinessContext. Requires PermBusinessUpdate.

func (*BusinessHandler) UpdateBusinessToolApprovals

func (h *BusinessHandler) UpdateBusinessToolApprovals(w http.ResponseWriter, r *http.Request)

UpdateBusinessToolApprovals handles PUT /business/{id}/tool-approvals. Requires PermBusinessUpdate.

func (*BusinessHandler) UpdateDescriptionTemplate

func (h *BusinessHandler) UpdateDescriptionTemplate(w http.ResponseWriter, r *http.Request)

UpdateDescriptionTemplate handles PUT /business/{id}/description-template. A non-empty template is a full replacement of the platform description; an empty string clears the override. Unknown placeholder tokens are rejected. On success the change is fanned out to connected platforms. Requires PermBusinessUpdate.

func (*BusinessHandler) UpdateDriftAlerts

func (h *BusinessHandler) UpdateDriftAlerts(w http.ResponseWriter, r *http.Request)

UpdateDriftAlerts stores only driftAlerts, preserving every sibling setting.

func (*BusinessHandler) UpdateOwnerBrief

func (h *BusinessHandler) UpdateOwnerBrief(w http.ResponseWriter, r *http.Request)

UpdateOwnerBrief handles PUT /businesses/{id}/owner-brief. Enables/disables the weekly owner-brief DM (enabled=false is the one-tap opt-out) and optionally sets the weekday/hour window. Absent fields keep their current stored value so a caller can toggle enabled without resupplying the schedule. Written via the same jsonb_set sub-key path as voiceProfile (no migration). Requires PermBusinessUpdate.

func (*BusinessHandler) UpdateReviewAutopilot

func (h *BusinessHandler) UpdateReviewAutopilot(w http.ResponseWriter, r *http.Request)

UpdateReviewAutopilot handles PUT /business/{id}/review-autopilot. Stores the opt-in autopilot config. minRating is bounded to the positive range [ReviewAutopilotMinRatingFloor..5] so the setting can only RAISE the positive floor and can never be configured to auto-publish a negative or neutral review. Like the voice profile it is prompt/automation-only and is NOT fanned out to any connected platform. Requires PermBusinessUpdate.

func (*BusinessHandler) UpdateSchedule

func (h *BusinessHandler) UpdateSchedule(w http.ResponseWriter, r *http.Request)

UpdateSchedule updates the business schedule (stored in settings). Requires PermBusinessUpdate.

func (*BusinessHandler) UpdateVoiceProfile

func (h *BusinessHandler) UpdateVoiceProfile(w http.ResponseWriter, r *http.Request)

UpdateVoiceProfile handles PUT /business/{id}/voice-profile. A non-empty value is stored verbatim and governs both the chat loop and the review drafter; an empty string clears the override. Unlike the description template it is not fanned out to any platform — it changes prompt text only, not a platform-visible field. Requires PermBusinessUpdate.

func (*BusinessHandler) UpdateVoiceTone

func (h *BusinessHandler) UpdateVoiceTone(w http.ResponseWriter, r *http.Request)

UpdateVoiceTone updates the business voice/tone tags (stored in settings). Body: {"tones": ["Тёплый", "Дружеский"]}. Requires PermBusinessUpdate.

func (h *BusinessHandler) UploadLogo(w http.ResponseWriter, r *http.Request)

UploadLogo handles multipart logo upload, stores the file in object storage, and updates the business logo_url to the public URL. Requires PermBusinessUpdate.

type BusinessService

type BusinessService interface {
	Create(ctx context.Context, business *domain.Business, ownerUserID uuid.UUID) (*domain.Business, error)
	GetByID(ctx context.Context, id uuid.UUID) (*domain.Business, error)
	// Update applies a business profile edit; actorUserID is threaded through
	// for the service-layer audit emission. It does not touch logo_url.
	Update(ctx context.Context, business *domain.Business, actorUserID uuid.UUID) (*domain.Business, error)
	// UpdateLogoURL writes only the logo_url via a targeted column update and
	// returns the re-read row, so a concurrent profile edit cannot revert the
	// new logo (and vice versa).
	UpdateLogoURL(ctx context.Context, businessID uuid.UUID, url string, actorUserID uuid.UUID) (*domain.Business, error)
	// UpdateSettingsKeys writes only the named settings sub-keys (schedule,
	// voiceTone, specialDates) via a targeted jsonb_set and returns the re-read
	// row. Sibling settings sub-keys (incl. tool_approvals) are preserved so a
	// concurrent writer of a different sub-key is never reverted.
	UpdateSettingsKeys(ctx context.Context, businessID uuid.UUID, keys map[string]interface{}, actorUserID uuid.UUID) (*domain.Business, error)
	// ListMembershipsByUser powers GET /api/v1/businesses.
	ListMembershipsByUser(ctx context.Context, userID uuid.UUID) ([]service.MembershipSummary, error)
	// Tool-approval methods. Permission enforcement (PermBusinessRead /
	// PermBusinessUpdate) is at the handler layer via authz.Can — the
	// service is a thin data wrapper since (CLEAN-01).
	GetToolApprovals(ctx context.Context, businessID uuid.UUID) (map[string]domain.ToolFloor, error)
	UpdateToolApprovals(ctx context.Context, businessID uuid.UUID, approvals map[string]domain.ToolFloor) error
}

BusinessService defines the interface for business operations

type BusinessSyncer

type BusinessSyncer interface {
	SyncBusiness(business *domain.Business)
}

BusinessSyncer syncs updated business data to connected platforms.

type ChannelRequestHandler

type ChannelRequestHandler struct {
	// contains filtered or unexported fields
}

ChannelRequestHandler handles channel-demand capture.

func NewChannelRequestHandler

func NewChannelRequestHandler(svc channelRequestService) *ChannelRequestHandler

NewChannelRequestHandler constructs a ChannelRequestHandler.

func (*ChannelRequestHandler) Create

Create handles POST /businesses/{id}/channel-requests. The enum + note-length checks are enforced by decodeAndValidate from the spec-derived validate tags; the service re-validates the channel as defense in depth.

func (*ChannelRequestHandler) List

List handles GET /businesses/{id}/channel-requests, returning the per-channel demand aggregate for the caller's business.

type ChatProxyHandler

type ChatProxyHandler struct {
	// contains filtered or unexported fields
}

ChatProxyHandler is a thin HTTP-facade over chatturn.Turn — parses, gates, maps TurnOutcome to HTTP. See docs/api/handlers/chat-proxy.md.

messageRepo is retained for one legacy test (TestChatProxy_LoadHistory_SkipsEmptyAssistant) that constructs the handler via struct literal; loadHistory reads it directly so the test does not need a fully-wired *chatturn.Turn.

func NewChatProxyHandler

func NewChatProxyHandler(
	businessService BusinessService,
	integrationService IntegrationService,
	projectService ProjectService,
	conversationRepo domain.ConversationRepository,
	messageRepo domain.MessageRepository,
	pendingRepo domain.PendingToolCallRepository,
	postRepo domain.PostRepository,
	reviewRepo domain.ReviewRepository,
	agentTaskRepo domain.AgentTaskRepository,
	taskHub *taskhub.Hub,
	orchClient *orchestratorclient.Client,
	titler *service.Titler,
	auditLogger audit.Logger,
	historyLimit int,
) *ChatProxyHandler

NewChatProxyHandler builds the chatturn.Turn from its dependency set. See docs/api/handlers/chat-proxy.md §"Construction".

func (*ChatProxyHandler) Chat

Chat handles POST /chat/{conversationID}; delegates lifecycle to Turn.Run and maps TurnOutcome → HTTP. See docs/api/handlers/chat-proxy.md.

func (*ChatProxyHandler) SetSSECounter

func (h *ChatProxyHandler) SetSSECounter(c *ssecounter.Counter, defaultTier string)

SetSSECounter wires the per-user SSE concurrency cap (optional).

func (*ChatProxyHandler) Turn

func (h *ChatProxyHandler) Turn() *chatturn.Turn

Turn exposes the shared chat-turn lifecycle so sibling handlers (HITLHandler.Resume) can delegate to the same persisting resume path instead of building a second Turn.

type ConsentDiffer

type ConsentDiffer interface {
	DiffAgainstCurrent(ctx context.Context, userID uuid.UUID) (*service.RequiresReconsentInfo, error)
}

ConsentDiffer powers the /auth/me requiresReconsent field. nil-safe.

type ConsentsHandler

type ConsentsHandler struct {
	// contains filtered or unexported fields
}

ConsentsHandler owns the three consent endpoints.

func NewConsentsHandler

func NewConsentsHandler(svc ConsentsServiceAPI, deletion AccountDeletionServiceAPI, repo ConsentsListerAPI, allowedOrigins []string) *ConsentsHandler

NewConsentsHandler constructs the consent handler. The allowedOrigins slice is the CORS_ALLOWED_ORIGINS values — used for the Origin-header CSRF check on the two side-effecting endpoints. deletion supplies the scheduled deletion timestamp surfaced in the PDN-withdrawal response (shared with UserDeletionHandler).

func (*ConsentsHandler) ListMine

func (h *ConsentsHandler) ListMine(w http.ResponseWriter, r *http.Request)

ListMine handles GET /users/me/consents. Returns the user's current consent state for Surface F (Withdraw / status panel).

func (*ConsentsHandler) Reconsent

func (h *ConsentsHandler) Reconsent(w http.ResponseWriter, r *http.Request)

Reconsent handles POST /auth/consents.

204 on success; 400 consent_required when a slug is missing / validation fails before the service runs; 409 version_mismatch when the service detects the build's currentVersion has bumped since the submitted version; 403 origin_not_allowed when the Origin header is blank or unmatched (CSRF discipline).

func (*ConsentsHandler) WithdrawPDN

func (h *ConsentsHandler) WithdrawPDN(w http.ResponseWriter, r *http.Request)

WithdrawPDN handles POST /users/me/consents/pdn/withdraw.

200 on success and 423 account_pending_deletion (already in the grace window) both return a PendingDeletionResponse carrying the real scheduled deletion date; 403 when the Origin header is missing/unmatched; 404 when the user row is gone.

type ConsentsListerAPI

type ConsentsListerAPI interface {
	ListByUser(ctx context.Context, userID uuid.UUID) ([]repository.Consent, error)
}

ConsentsListerAPI is the slice of UserConsentsRepository the handler uses for GET /users/me/consents. Kept separate from ConsentsServiceAPI because the service layer doesn't need a List* method — the read path is repo-direct.

type ConsentsServiceAPI

type ConsentsServiceAPI interface {
	ReConsent(ctx context.Context, userID uuid.UUID, ip, userAgent string, policies []service.PolicyAccepted) error
	WithdrawPDN(ctx context.Context, userID uuid.UUID, ip, userAgent string) error
}

ConsentsServiceAPI is the slice of *service.ConsentService the handler consumes. Declared as an interface so handler tests can pass an in-memory double (mirrors the rest of the service-API boundaries).

type ContentTemplateHandler

type ContentTemplateHandler struct {
	// contains filtered or unexported fields
}

func (*ContentTemplateHandler) Create

func (*ContentTemplateHandler) CreateFromPost

func (h *ContentTemplateHandler) CreateFromPost(w http.ResponseWriter, r *http.Request)

func (*ContentTemplateHandler) CreateFromReview

func (h *ContentTemplateHandler) CreateFromReview(w http.ResponseWriter, r *http.Request)

func (*ContentTemplateHandler) Delete

func (*ContentTemplateHandler) Get

func (*ContentTemplateHandler) List

func (*ContentTemplateHandler) Render

func (*ContentTemplateHandler) Update

type ConversationHandler

type ConversationHandler struct {
	// contains filtered or unexported fields
}

ConversationHandler serves the conversation CRUD + move/pin/unpin/list-messages surface. See docs/api/handlers/conversation.md.

func NewConversationHandler

func NewConversationHandler(
	conversationRepo domain.ConversationRepository,
	messageRepo domain.MessageRepository,
	businessService BusinessService,
	projectService ProjectService,
	conversationService ConversationService,
) (*ConversationHandler, error)

NewConversationHandler constructs a ConversationHandler; all five deps are required.

func (*ConversationHandler) CreateConversation

func (h *ConversationHandler) CreateConversation(w http.ResponseWriter, r *http.Request)

CreateConversation handles POST /conversations. See docs/api/handlers/conversation.md.

func (*ConversationHandler) DeleteConversation

func (h *ConversationHandler) DeleteConversation(w http.ResponseWriter, r *http.Request)

DeleteConversation handles DELETE /conversations/{id}. See docs/api/handlers/conversation.md.

func (*ConversationHandler) GetConversation

func (h *ConversationHandler) GetConversation(w http.ResponseWriter, r *http.Request)

GetConversation handles GET /conversations/{id}. See docs/api/handlers/conversation.md.

func (*ConversationHandler) ListConversations

func (h *ConversationHandler) ListConversations(w http.ResponseWriter, r *http.Request)

ListConversations handles GET /conversations. See docs/api/handlers/conversation.md.

func (*ConversationHandler) ListMessages

func (h *ConversationHandler) ListMessages(w http.ResponseWriter, r *http.Request)

ListMessages handles GET /conversations/{id}/messages — adapter over ConversationService.OpenChat. See docs/api/handlers/conversation.md.

func (*ConversationHandler) MoveConversation

func (h *ConversationHandler) MoveConversation(w http.ResponseWriter, r *http.Request)

MoveConversation handles POST /conversations/{id}/move. See docs/api/handlers/conversation.md. Null/empty/absent ProjectId all map to the "Без проекта" bucket.

func (*ConversationHandler) Pin

Pin handles POST /conversations/{id}/pin — atomic pinned_at=now scoped by (id, business_id, user_id). Cross-tenant → uniform 404 (no leak). See docs/api/handlers/conversation.md §"Pin / unpin atomicity".

func (*ConversationHandler) Unpin

Unpin handles POST /conversations/{id}/unpin — symmetric to Pin. See docs/api/handlers/conversation.md §"Pin / unpin atomicity".

func (*ConversationHandler) UpdateConversation

func (h *ConversationHandler) UpdateConversation(w http.ResponseWriter, r *http.Request)

UpdateConversation handles PUT /conversations/{id} — manual rename. See docs/api/handlers/conversation.md §"UpdateConversation".

type ConversationService

type ConversationService interface {
	List(ctx context.Context, businessID, userID uuid.UUID, limit, offset int) ([]domain.Conversation, error)
	MoveToProject(
		ctx context.Context,
		conversationID string,
		businessID, requesterUserID uuid.UUID,
		projectID *string,
	) (*domain.Conversation, error)

	OpenChat(
		ctx context.Context,
		conversationID string,
		businessID, requesterUserID uuid.UUID,
	) (*service.ChatView, error)

	DeleteWithMessages(ctx context.Context, conversationID string) error
}

ConversationService is the handler-facing view of the *service.ConversationService concrete type. Owns conversation operations that cross multiple repository reads/writes: - MoveToProject — multi-repo write transition (move + system note). - OpenChat — multi-repo read composite (messages + pending batches) returning the fully-projected ChatView the handler JSON-encodes verbatim.

Declared as an interface so conversation_test.go can swap in a noop fake for handler tests that don't exercise these paths.

type EmailVerificationServiceAPI

type EmailVerificationServiceAPI interface {
	RequestResend(ctx context.Context, userID uuid.UUID) error
	ConfirmVerify(ctx context.Context, plaintextToken string) (uuid.UUID, error)
	IsTokenExpired(ctx context.Context, plaintextToken string) (bool, error)
	ChangeEmailBeforeVerify(ctx context.Context, userID uuid.UUID, newEmail string) (oldEmail string, err error)
}

EmailVerificationServiceAPI is the AuthHandler's view of EmailVerificationService.

type ErrorResponse

type ErrorResponse = openapi.ErrorResponse

Re-export spec-owned error envelopes under the historic handler.* names so the existing test suite continues to compile against named handler types. Wire shape (keys + nesting) is identical to the legacy local definitions — both `error` and `fields` are required, matching writeJSONError / writeValidationError output.

type FeedbackHandler

type FeedbackHandler struct {
	// contains filtered or unexported fields
}

FeedbackHandler handles in-app feedback submissions.

func NewFeedbackHandler

func NewFeedbackHandler(svc feedbackSubmitter) *FeedbackHandler

NewFeedbackHandler constructs a FeedbackHandler.

func (*FeedbackHandler) Submit

func (h *FeedbackHandler) Submit(w http.ResponseWriter, r *http.Request)

Submit handles POST /feedback.

type HITLHandler

type HITLHandler struct {
	// contains filtered or unexported fields
}

HITLHandler serves the three HITL HTTP endpoints. See docs/api/handlers/hitl.md.

func NewHITLHandler

func NewHITLHandler(
	hitlService *service.HITLService,
	businessService BusinessService,
	conversationRepo domain.ConversationRepository,
	integrationRepo integrationLister,
	resumer chatResumer,
) (*HITLHandler, error)

NewHITLHandler constructs a HITLHandler; all deps are required. resumer is the shared chat-turn lifecycle — Resume delegates to it so the approved-tool result is persisted (the bare SSE proxy this replaces never wrote it back). integrationRepo lets Resume derive the active-platform list the orchestrator needs to keep post-approval platform tools available.

func (*HITLHandler) GetTools

func (h *HITLHandler) GetTools(w http.ResponseWriter, r *http.Request)

GetTools handles GET /tools — registry projection via 5-min cache. See docs/api/handlers/hitl.md §"GetTools projection".

func (*HITLHandler) ResolvePendingToolCalls

func (h *HITLHandler) ResolvePendingToolCalls(w http.ResponseWriter, r *http.Request)

ResolvePendingToolCalls handles POST /conversations/{id}/pending-tool-calls/{batch_id}/resolve. See docs/api/handlers/hitl.md.

func (*HITLHandler) Resume

func (h *HITLHandler) Resume(w http.ResponseWriter, r *http.Request)

Resume handles POST /chat/{id}/resume?batch_id=X. See docs/api/handlers/hitl.md.

func (*HITLHandler) SetAuditLogger

func (h *HITLHandler) SetAuditLogger(auditLogger audit.Logger)

SetAuditLogger wires the audit logger used to record in-app approval resolutions. A nil logger degrades to audit.Nop() so the handler never panics when audit is unconfigured (tests). Mirrors the off-app TelegramApprovalConsumer, so an in-app resolve is attributable to the acting owner + channel exactly as an off-app tap is.

func (*HITLHandler) SetSSECounter

func (h *HITLHandler) SetSSECounter(c *ssecounter.Counter, defaultTier string)

SetSSECounter wires the per-user SSE concurrency cap (optional). Mirrors ChatProxyHandler.SetSSECounter so the chat and resume streams share one cap.

type IntegrationHandler

type IntegrationHandler struct {
	// contains filtered or unexported fields
}

IntegrationHandler handles integration endpoints

func NewIntegrationHandler

func NewIntegrationHandler(integrationService IntegrationService, businessService BusinessService, auditLogger audit.Logger) (*IntegrationHandler, error)

NewIntegrationHandler creates a new integration handler instance.

businessService lets DeleteIntegration reject disconnects against a soft-deleted (erasure-pending) organization, which the RequireBusinessAccess middleware does not filter.

Integration audit events are emitted by the service layer.

func (*IntegrationHandler) DeleteIntegration

func (h *IntegrationHandler) DeleteIntegration(w http.ResponseWriter, r *http.Request)

DeleteIntegration deletes an integration by ID

func (*IntegrationHandler) GetIntegrationsDrift

func (h *IntegrationHandler) GetIntegrationsDrift(w http.ResponseWriter, r *http.Request)

GetIntegrationsDrift returns the proactive-sync drift status for every connected channel of the business. Read-gated on PermIntegrationsRead like ListIntegrations. Returns an empty list when reconciliation is unwired.

func (*IntegrationHandler) ListIntegrations

func (h *IntegrationHandler) ListIntegrations(w http.ResponseWriter, r *http.Request)

ListIntegrations returns all integrations for the business from the request context.

func (*IntegrationHandler) SetHealthChecker

func (h *IntegrationHandler) SetHealthChecker(checker connHealthChecker)

SetHealthChecker injects the connection-health checker the verify endpoint uses to compute the synchronous per-integration health array. Nil-safe; mirrors SetReconciler so the constructor and its test call sites are unchanged. When unwired the verify endpoint behaves exactly as before.

func (*IntegrationHandler) SetReconciler

func (h *IntegrationHandler) SetReconciler(reconciler driftLister, syncTrigger businessSyncTrigger)

SetReconciler injects the proactive-sync collaborators that back the drift + verify endpoints. Both may be nil (reconciliation unwired); the endpoints degrade gracefully. Called from wire/handlers.go after construction so the existing constructor signature (and its many test call sites) is unchanged.

func (*IntegrationHandler) VerifyIntegrations

func (h *IntegrationHandler) VerifyIntegrations(w http.ResponseWriter, r *http.Request)

VerifyIntegrations performs manual repair: it re-pushes the stored business profile to every connected platform (the SAME re-push the reactive path uses) and reschedules the channels for an immediate re-check. Write-gated on PermBusinessUpdate because it mutates the remote platforms.

type IntegrationService

type IntegrationService interface {
	ListByBusinessID(ctx context.Context, businessID uuid.UUID) ([]domain.Integration, error)
	GetByBusinessAndPlatform(ctx context.Context, businessID uuid.UUID, platform string) (*domain.Integration, error)
	Delete(ctx context.Context, integrationID uuid.UUID, actorID uuid.UUID) error
	MarkTokenExpired(ctx context.Context, businessID uuid.UUID, platform, externalID string) error
}

IntegrationService defines the interface for integration operations

type InternalBillingHandler

type InternalBillingHandler struct {
	// contains filtered or unexported fields
}

InternalBillingHandler serves POST /internal/v1/billing/usage_logs on the mTLS-protected :8443 listener. It is reached only via the orchestrator's pkg/billingclient, gated by router-level middleware.RequireServiceIdentity.

func NewInternalBillingHandler

func NewInternalBillingHandler(b BillingService, log *slog.Logger) *InternalBillingHandler

NewInternalBillingHandler wires the handler with a backing BillingService (production: services/api/internal/repository.billingRepository). A nil logger falls back to slog.Default().

func (*InternalBillingHandler) GetDailySpend

func (h *InternalBillingHandler) GetDailySpend(w http.ResponseWriter, r *http.Request)

GetDailySpend handles GET /internal/v1/billing/daily_spend.

Query parameters (both required):

  • business_id — UUID of the business whose spend to look up.
  • date — UTC calendar day in YYYY-MM-DD form.

Status codes:

  • 200 OK — body is {"daily_spend_usd": <float>}.
  • 400 Bad Request — missing/invalid business_id or date; body is {"error":"invalid_payload","detail":"…"}.
  • 405 Method Not Allowed — non-GET verb.
  • 500 Internal Server Error — repository failure; body is {"error":"transient"} (no internal details leaked).

func (*InternalBillingHandler) LogUsage

LogUsage handles POST /internal/v1/billing/usage_logs.

Status codes:

  • 204 No Content — success.
  • 400 Bad Request — invalid JSON, missing required fields, or negative token counts. Body is {"error":"invalid_payload","detail":"…"}.
  • 405 Method Not Allowed — non-POST verb.
  • 500 Internal Server Error — repository failure. Body is {"error":"transient"} (no internal details leaked).

type InternalTokenHandler

type InternalTokenHandler struct {
	// contains filtered or unexported fields
}

InternalTokenHandler handles internal token endpoints.

func NewInternalTokenHandler

func NewInternalTokenHandler(tokenService TokenService) *InternalTokenHandler

NewInternalTokenHandler creates a new InternalTokenHandler instance

func (*InternalTokenHandler) GetToken

GetToken handles GET /internal/v1/tokens Query params: business_id (required), platform (required), external_id (optional)

type InternalYandexSharedHandler

type InternalYandexSharedHandler struct {
	// contains filtered or unexported fields
}

InternalYandexSharedHandler sets/rotates the single shared representative session used by delegated-representative access. It is served ONLY on the mTLS-protected internal listener behind a service-identity allowlist — the ops/admin bootstrap path. It never touches customer credentials.

func NewInternalYandexSharedHandler

func NewInternalYandexSharedHandler(setter sharedSessionSetter, sharedBusinessID string) *InternalYandexSharedHandler

NewInternalYandexSharedHandler constructs the bootstrap handler. sharedBusinessID is the config sentinel (YANDEX_SHARED_BUSINESS_ID); an empty value makes the endpoint fail closed ("delegated access not configured").

func (*InternalYandexSharedHandler) SetSharedSession

func (h *InternalYandexSharedHandler) SetSharedSession(w http.ResponseWriter, r *http.Request)

SetSharedSession handles POST /internal/v1/yandex/shared-session. It validates the pasted cookies, then stores/rotates them as the KMS-wrapped shared singleton under the sentinel business.

type InvitationsHandler

type InvitationsHandler struct {
	// contains filtered or unexported fields
}

InvitationsHandler serves the invitation CRUD + accept endpoints.

func NewInvitationsHandler

func NewInvitationsHandler(
	ir domain.InvitationRepository,
	mr domain.BusinessMembershipRepository,
	rr domain.RoleRepository,
	ur domain.UserRepository,
	br domain.BusinessRepository,
	bs businessGetter,
	pool poolBeginner,
	inv memberCacheInvalidator,
	auditLogger audit.Logger,
) (*InvitationsHandler, error)

NewInvitationsHandler constructs an InvitationsHandler; every dep is required. businessService lets the write endpoints reject mutations against a soft-deleted (erasure-pending) organization that RequireBusinessAccess (membership-only) does not gate.

func (*InvitationsHandler) Accept

Accept handles POST /api/v1/invitations/{token}/accept — auth-required, NOT business-scoped. See docs/api/handlers/invitations.md for the race-safe accept order.

func (*InvitationsHandler) Create

Create handles POST /api/v1/businesses/{id}/invitations. See docs/api/handlers/invitations.md.

func (*InvitationsHandler) ListPending

func (h *InvitationsHandler) ListPending(w http.ResponseWriter, r *http.Request)

ListPending handles GET /api/v1/businesses/{id}/invitations. See docs/api/handlers/invitations.md. ListPending includes inviters in their account deletion grace window.

func (*InvitationsHandler) Preview

Preview handles GET /api/v1/invitations/{token} — PUBLIC; the token IS the auth. See docs/api/handlers/invitations.md.

func (*InvitationsHandler) Revoke

Revoke handles DELETE /api/v1/businesses/{id}/invitations/{inviteId}. See docs/api/handlers/invitations.md.

type LandingHandler

type LandingHandler struct {
	// contains filtered or unexported fields
}

LandingHandler handles public marketing-landing capture.

func NewLandingHandler

func NewLandingHandler(svc landingService) *LandingHandler

NewLandingHandler constructs a LandingHandler.

func (*LandingHandler) JoinWaitlist

func (h *LandingHandler) JoinWaitlist(w http.ResponseWriter, r *http.Request)

JoinWaitlist handles POST /waitlist. The email/enum/consent checks are enforced by decodeAndValidate from the spec-derived validate tags; the service re-validates and normalizes as defense in depth.

func (*LandingHandler) RecordChannelVote

func (h *LandingHandler) RecordChannelVote(w http.ResponseWriter, r *http.Request)

RecordChannelVote handles POST /channel-votes.

func (*LandingHandler) RecordLandingEvent

func (h *LandingHandler) RecordLandingEvent(w http.ResponseWriter, r *http.Request)

RecordLandingEvent handles POST /landing-events.

type LoginRequest

type LoginRequest = openapi.LoginRequest

Re-export spec-owned shapes under the historic handler.* names.

type LoginResponse

type LoginResponse = openapi.LoginResponse

Re-export spec-owned shapes under the historic handler.* names.

type MeResponse

type MeResponse = openapi.MeResponse

MeResponse is the spec-owned wire shape for GET /auth/me. The historic handler-local struct that embedded *domain.User is gone; the spec (docs/api/spec/openapi.yaml#/components/schemas/MeResponse) is now the sole source of truth for the field set. Mappers below copy only the five wire-safe fields from domain.User — never the password hash, the email_verified_at timestamp, or the soft-delete-state timestamps that domain.User carries with json:"-".

type MembersHandler

type MembersHandler struct {
	// contains filtered or unexported fields
}

MembersHandler implements MEMBER-01..06:

GET    /businesses/{id}/members          → ListMembers   (MEMBER-01)
PATCH  /businesses/{id}/members/{userId} → UpdateMemberRole (MEMBER-02 + MEMBER-05 + MEMBER-06)
DELETE /businesses/{id}/members/{userId} → RemoveMember  (MEMBER-03 + MEMBER-04 + MEMBER-05)

func NewMembersHandler

func NewMembersHandler(
	mr domain.BusinessMembershipRepository,
	rr domain.RoleRepository,
	ur domain.UserRepository,
	ir domain.InvitationRepository,
	bs businessGetter,
	pool poolBeginner,
	inv memberCacheInvalidator,
	auditLogger audit.Logger,
) (*MembersHandler, error)

NewMembersHandler constructs a MembersHandler. All dependencies are required.

adds `auditLogger` so PATCH/DELETE member endpoints can emit rbac.role_granted / rbac.member_removed audit events AFTER the underlying transaction commits. businessService lets the write endpoints reject mutations against a soft-deleted (erasure-pending) organization that RequireBusinessAccess (membership-only) does not gate.

func (*MembersHandler) ListMembers

func (h *MembersHandler) ListMembers(w http.ResponseWriter, r *http.Request)

ListMembers handles GET /api/v1/businesses/{id}/members. Includes members in their account deletion grace window.

func (*MembersHandler) RemoveMember

func (h *MembersHandler) RemoveMember(w http.ResponseWriter, r *http.Request)

RemoveMember handles DELETE /api/v1/businesses/{id}/members/{userId}. Self-removal is permission-exempt but remains subject to the service last-owner check.

func (*MembersHandler) UpdateMemberRole

func (h *MembersHandler) UpdateMemberRole(w http.ResponseWriter, r *http.Request)

UpdateMemberRole handles PATCH /api/v1/businesses/{id}/members/{userId}. The membership service validates the role and commits the mutation with invitation revocation.

type PasswordResetServiceAPI

type PasswordResetServiceAPI interface {
	RequestReset(ctx context.Context, emailAddr, clientIP, userAgent string) error
	ConfirmReset(ctx context.Context, plaintextToken, newPassword, clientIP, userAgent string) error
}

PasswordResetServiceAPI is the AuthHandler's view of PasswordResetService.

type PermissionsHandler

type PermissionsHandler struct{}

PermissionsHandler exposes the static permission registry as a JSON endpoint. ships GET /api/v1/permissions; the endpoint sits behind the existing authMiddleware (any authenticated user) per AUTHZ-01 / CONTEXT decision.

The handler holds no state: AllPermissions returns a fresh slice on every call and the body is computed in the request goroutine.

func NewPermissionsHandler

func NewPermissionsHandler() *PermissionsHandler

NewPermissionsHandler constructs a zero-dependency handler. It exists for symmetry with the project's NewXxxHandler convention; future versions (descriptions) may take dependencies and benefit from the seam.

func (*PermissionsHandler) List

List returns the permission registry as JSON. Auth is enforced upstream by the existing authMiddleware (router wiring in Plan G).

Descriptions are localized per request locale via the i18n catalog (permissions.<resource>.<action>.desc). The registry's hardcoded RU Description is the fallback when a catalog key is absent.

GET /api/v1/permissions

200 -> openapi.PermissionRegistryResponse
401 -> handled by authMiddleware before we get here

type PlatformAvailability

type PlatformAvailability struct {
	Telegram       bool
	VK             bool
	YandexBusiness bool
	GoogleBusiness bool
}

PlatformAvailability tells the PlatformsHandler which integration platforms have their credentials configured in the current deployment. The handler uses this to downgrade the default Active status to OAuthNotConfigured for platforms whose credentials are missing, so the frontend can hide broken connect flows.

Each field is true iff the minimum credentials required to run the corresponding flow are present (non-empty). Telegram uses a bot token rather than OAuth; the field name keeps the OAuth-centric naming for frontend symmetry.

type PlatformsHandler

type PlatformsHandler struct {
	// contains filtered or unexported fields
}

PlatformsHandler exposes the canonical integration platform registry.

func NewPlatformsHandler

func NewPlatformsHandler(availability PlatformAvailability) *PlatformsHandler

NewPlatformsHandler creates a PlatformsHandler that reports the registry merged with the supplied availability flags.

func (*PlatformsHandler) List

List handles GET /api/v1/platforms.

The endpoint is intentionally unauthenticated so the marketing landing page can render the list of supported platforms without prompting for login. The returned data is non-sensitive — only platform names, descriptions, and a derived availability status, no credentials and no per-tenant data.

type PostHandler

type PostHandler struct {
	// contains filtered or unexported fields
}

PostHandler handles post-related HTTP requests

func NewPostHandler

func NewPostHandler(postService PostService) (*PostHandler, error)

NewPostHandler creates a new post handler instance

func (*PostHandler) GetPost

func (h *PostHandler) GetPost(w http.ResponseWriter, r *http.Request)

GetPost handles GET /api/v1/posts/{id}

func (*PostHandler) ListPosts

func (h *PostHandler) ListPosts(w http.ResponseWriter, r *http.Request)

ListPosts handles GET /api/v1/posts

type PostService

type PostService interface {
	List(ctx context.Context, businessID uuid.UUID, filter domain.PostFilter) ([]domain.Post, int, error)
	GetByID(ctx context.Context, businessID uuid.UUID, id string) (*domain.Post, error)
}

PostService defines the interface for post operations.

type PresenceHealthHandler

type PresenceHealthHandler struct {
	// contains filtered or unexported fields
}

PresenceHealthHandler serves the read-only composite presence-health score.

func NewPresenceHealthHandler

func NewPresenceHealthHandler(scorer PresenceHealthScorer, snapshots PresenceHealthSnapshotStore) (*PresenceHealthHandler, error)

NewPresenceHealthHandler constructs the handler over the scorer + snapshot store. A nil dependency is a wiring bug and returns an error.

func (*PresenceHealthHandler) GetPresenceHealth

func (h *PresenceHealthHandler) GetPresenceHealth(w http.ResponseWriter, r *http.Request)

GetPresenceHealth handles GET /businesses/{id}/presence-health. It returns the composite 0-100 presence-health score, the week-over-week trend delta, the four normalized sub-scores, the weight set used, and a single next-action recommendation. Aggregate-only: no author name, review text, or reply text is fetched or returned. Authz mirrors the other GET review endpoints: RequireBusinessAccess + content:read.

It reads the trend from the most-recent PRIOR-week snapshot and, when the current score has a composite, lazily upserts this week's snapshot so the trend is fresh without waiting for the weekly worker — the prior-week read excludes the current week, so this lazy write can never become its own baseline.

type PresenceHealthScorer

type PresenceHealthScorer interface {
	Score(ctx context.Context, businessID uuid.UUID, targetHours int) (service.PresenceHealthScore, error)
}

PresenceHealthScorer computes the composite presence-health score for one business. *service.PresenceHealthService satisfies it.

type PresenceHealthSnapshotStore

type PresenceHealthSnapshotStore interface {
	GetMostRecentPrior(ctx context.Context, businessID uuid.UUID, currentWeek string) (*domain.PresenceHealthSnapshot, error)
	Upsert(ctx context.Context, snap domain.PresenceHealthSnapshot) error
}

PresenceHealthSnapshotStore reads the prior-week snapshot (for the trend delta) and upserts the current week's snapshot (the optional lazy write on read). domain.PresenceHealthSnapshotRepository satisfies it.

type ProjectHandler

type ProjectHandler struct {
	// contains filtered or unexported fields
}

ProjectHandler serves the /api/v1/projects REST endpoints.

func NewProjectHandler

func NewProjectHandler(ps ProjectService, bs businessGetter) (*ProjectHandler, error)

NewProjectHandler constructs a ProjectHandler. Both the projectService and businessService dependencies are required. Business scoping is handled by the RequireBusinessAccess middleware which injects BusinessContext into every request; the businessService lets the write endpoints additionally reject mutations against a soft-deleted organization.

func (*ProjectHandler) ConversationCount

func (h *ProjectHandler) ConversationCount(w http.ResponseWriter, r *http.Request)

ConversationCount handles GET /api/v1/projects/{id}/conversation-count. Feeds the frontend delete-confirmation dialog so the user sees how many chats will be destroyed before confirming.

func (*ProjectHandler) Create

func (h *ProjectHandler) Create(w http.ResponseWriter, r *http.Request)

Create handles POST /api/v1/projects.

func (*ProjectHandler) Delete

func (h *ProjectHandler) Delete(w http.ResponseWriter, r *http.Request)

Delete handles DELETE /api/v1/projects/{id}. Hard-deletes the project plus every Mongo conversation/message assigned to it, returning the counts so the frontend can show "deleted N chats" feedback.

func (*ProjectHandler) Get

Get handles GET /api/v1/projects/{id}.

func (*ProjectHandler) List

List handles GET /api/v1/projects.

func (*ProjectHandler) SetToolsCache

func (h *ProjectHandler) SetToolsCache(c ToolsCache)

SetToolsCache wires a tools-registry cache so PUT /projects/{id} can validate approvalOverrides keys against the live orchestrator registry. Safe to call with nil to disable the field.

func (*ProjectHandler) Update

func (h *ProjectHandler) Update(w http.ResponseWriter, r *http.Request)

Update handles PUT /api/v1/projects/{id}.

type ProjectService

type ProjectService interface {
	Create(ctx context.Context, businessID, actorID uuid.UUID, input service.CreateProjectInput) (*domain.Project, error)
	GetByID(ctx context.Context, businessID, id uuid.UUID) (*domain.Project, error)
	ListByBusinessID(ctx context.Context, businessID uuid.UUID) ([]domain.Project, error)
	Update(ctx context.Context, businessID, id, actorID uuid.UUID, input service.UpdateProjectInput) (*domain.Project, error)
	DeleteCascade(ctx context.Context, businessID, id, actorID uuid.UUID) (deletedConversations, deletedMessages int, err error)
	CountConversations(ctx context.Context, businessID, id uuid.UUID) (int, error)
}

ProjectService is the handler-facing view of the ProjectService concrete type. Declared as an interface so project_test.go can inject a mock without importing the full service package in tests. Matches the public method set of *service.ProjectService one-for-one.

Create / Update / DeleteCascade take an actorID so the service layer emits project.* audit events with the correct attribution.

type RefreshTokenResponse

type RefreshTokenResponse = openapi.RefreshTokenResponse

Re-export spec-owned shapes under the historic handler.* names.

type RegistrationAccessGate

type RegistrationAccessGate interface {
	HasRegistrationAccess(ctx context.Context, email string) (bool, error)
}

RegistrationAccessGate is the persistence seam used by closed-beta mode. Implementations must return one opaque boolean for missing, unconsented, and not-yet-approved waitlist rows so the handler exposes no list status detail.

type ReviewHandler

type ReviewHandler struct {
	// contains filtered or unexported fields
}

ReviewHandler handles review-related HTTP requests

func NewReviewHandler

func NewReviewHandler(reviewService ReviewService) (*ReviewHandler, error)

NewReviewHandler creates a new review handler instance

func (*ReviewHandler) BatchDraftReviews

func (h *ReviewHandler) BatchDraftReviews(w http.ResponseWriter, r *http.Request)

BatchDraftReviews handles POST /businesses/{id}/reviews/batch-draft. It drafts AI replies for a bounded set of unanswered reviews (explicit ids, or the auto-selected pending backlog when none are supplied) and returns a per-item result. Each draft is one metered LLM call through the existing single-draft path, so the request is bounded to keep the credit cost predictable. Authz mirrors the manual reply path: RequireBusinessAccess + PermContentUpdate + writeLimit (router) + the service soft-delete gate.

func (*ReviewHandler) BulkApproveReviews

func (h *ReviewHandler) BulkApproveReviews(w http.ResponseWriter, r *http.Request)

BulkApproveReviews handles POST /businesses/{id}/reviews/bulk-approve. It publishes the stored draft for every POSITIVE review in the request (needs_review and negatives are excluded and never published) via the same dispatch path a manual reply uses, and returns a per-item result. reviewIds is required. Authz mirrors the manual reply path.

func (*ReviewHandler) GetDelegationMetrics

func (h *ReviewHandler) GetDelegationMetrics(w http.ResponseWriter, r *http.Request)

func (*ReviewHandler) GetReview

func (h *ReviewHandler) GetReview(w http.ResponseWriter, r *http.Request)

GetReview handles GET /api/v1/reviews/{id}

func (*ReviewHandler) GetReviewSLA

func (h *ReviewHandler) GetReviewSLA(w http.ResponseWriter, r *http.Request)

GetReviewSLA handles GET /businesses/{id}/reviews/sla. It returns aggregate-only response-SLA metrics (counts, unanswered-age buckets, median / average response time, and percent answered within a target window). No author name, review text, or reply text is fetched or returned. Authz mirrors the other GET review endpoints: RequireBusinessAccess + content:read.

func (*ReviewHandler) ListReviews

func (h *ReviewHandler) ListReviews(w http.ResponseWriter, r *http.Request)

ListReviews handles GET /api/v1/reviews

func (*ReviewHandler) RefreshReviews

func (h *ReviewHandler) RefreshReviews(w http.ResponseWriter, r *http.Request)

RefreshReviews handles POST /api/v1/reviews/refresh — triggers an on-demand pull from every connected platform for the caller's single business (resolved from the request's BusinessContext). Synchronous: the response returns once the dispatch completes (or fails) so the frontend can immediately invalidate its query and show the new rows. The endpoint is rate-naturally-bounded by the Cloudflare-style 60s gateway timeout combined with the syncer's own per-platform 60s per-NATS-request budget. It is a write-class action that drives external platform work, so it requires PermContentUpdate — a read-only viewer must not trigger the fanout.

func (*ReviewHandler) ReplyToReview

func (h *ReviewHandler) ReplyToReview(w http.ResponseWriter, r *http.Request)

ReplyToReview handles PUT /api/v1/reviews/{id}/reply

func (*ReviewHandler) RetryReviewReply

func (h *ReviewHandler) RetryReviewReply(w http.ResponseWriter, r *http.Request)

RetryReviewReply handles POST /businesses/{id}/reviews/{reviewID}/reply/retry — re-sends the stored reply of a review whose previous send failed. No body: the failed manual reply persisted the attempted text, so the retry re-uses it verbatim (and the same dispatch idempotency key, so an already-delivered reply is never posted twice). A review not in the error reply state → 409 with the stable reply_not_retryable code. Authz mirrors ReplyToReview: RequireBusinessAccess + PermContentUpdate + writeLimit (router) + the service soft-delete gate.

func (*ReviewHandler) SetSSECounter

func (h *ReviewHandler) SetSSECounter(c *ssecounter.Counter, defaultTier string)

SetSSECounter wires the per-user concurrency cap (optional). Mirrors ChatProxyHandler.SetSSECounter so refresh shares the chat/resume budget.

type ReviewService

type ReviewService interface {
	List(ctx context.Context, businessID uuid.UUID, filter domain.ReviewFilter) ([]domain.Review, int, error)
	GetByID(ctx context.Context, businessID uuid.UUID, id string) (*domain.Review, error)
	Reply(ctx context.Context, businessID uuid.UUID, id string, replyText string) error
	RetryReply(ctx context.Context, businessID uuid.UUID, id string) error
	Refresh(ctx context.Context, userID uuid.UUID) error
	BatchDraft(ctx context.Context, businessID uuid.UUID, reviewIDs []string) ([]service.BatchItemResult, error)
	BulkApprove(ctx context.Context, businessID uuid.UUID, reviewIDs []string) ([]service.BatchItemResult, error)
	SLA(ctx context.Context, businessID uuid.UUID, targetHours int) (service.SLAStats, error)
	DelegationMetrics(ctx context.Context, businessID uuid.UUID) (service.DelegationMetrics, error)
}

ReviewService defines the interface for review operations used by handler. List/GetByID/Reply receive businessID (extracted from /businesses/{id} URL by RequireBusinessAccess middleware); Refresh resolves the same single business from the request's BusinessContext and takes userID only for logging/audit.

type RolesHandler

type RolesHandler struct {
	// contains filtered or unexported fields
}

RolesHandler serves the business role CRUD + MyPermissions endpoints. See docs/api/handlers/roles.md.

func NewRolesHandler

func NewRolesHandler(
	rr domain.RoleRepository,
	mr domain.BusinessMembershipRepository,
	bs businessGetter,
	pool poolBeginner,
	inv roleCacheInvalidator,
	auditLogger audit.Logger,
) (*RolesHandler, error)

NewRolesHandler constructs a RolesHandler; every dep is required. businessService lets the write endpoints reject mutations against a soft-deleted (erasure-pending) organization that RequireBusinessAccess (membership-only) does not gate.

func (*RolesHandler) Create

func (h *RolesHandler) Create(w http.ResponseWriter, r *http.Request)

Create handles POST /api/v1/businesses/{id}/roles (PermRolesCreate). See docs/api/handlers/roles.md.

Note: `?clone_from=` is intentionally ignored server-side — the frontend pre-fills permissions on the client and POSTs the result here.

func (*RolesHandler) Delete

func (h *RolesHandler) Delete(w http.ResponseWriter, r *http.Request)

Delete handles DELETE /api/v1/businesses/{id}/roles/{roleId}?reassign_to=<uuid> (PermRolesDelete). See docs/api/handlers/roles.md.

func (*RolesHandler) List

func (h *RolesHandler) List(w http.ResponseWriter, r *http.Request)

List handles GET /api/v1/businesses/{id}/roles (PermRolesRead). See docs/api/handlers/roles.md.

func (*RolesHandler) MyPermissions

func (h *RolesHandler) MyPermissions(w http.ResponseWriter, r *http.Request)

MyPermissions handles GET /api/v1/businesses/{id}/me/permissions. See docs/api/handlers/roles.md.

No additional authz.Can gate — BY DESIGN. RequireBusinessAccess on the parent route already rejects non-members (404) and suspended members (403); any active member can read their own permissions.

func (*RolesHandler) Update

func (h *RolesHandler) Update(w http.ResponseWriter, r *http.Request)

Update handles PATCH /api/v1/businesses/{id}/roles/{roleId} (PermRolesUpdate). See docs/api/handlers/roles.md.

type SearchHandler

type SearchHandler struct {
	// contains filtered or unexported fields
}

SearchHandler implements GET /api/v1/search?q=&project_id=&limit=20.

Maps the three search sentinel errors:

  • domain.ErrSearchIndexNotReady → 503 + Retry-After: 5 header.
  • domain.ErrInvalidScope → 500 (defense-in-depth; should not be reachable because we resolve scope server-side, but the repo guards anyway).
  • other errors → 500 with metadata-only log line.

Logging contract: every log line carries {user_id, business_id, query_length}. NEVER the literal query text. NO `"query"` slog field key.

func NewSearchHandler

func NewSearchHandler(searcher *service.Searcher) (*SearchHandler, error)

NewSearchHandler — searcher dep is mandatory. Pattern parallels NewConversationHandler (returns *Handler, error). Nil dep returns a nil handler + descriptive error so cmd/main.go fails at boot.

func (*SearchHandler) Search

func (h *SearchHandler) Search(w http.ResponseWriter, r *http.Request)

Search handles GET /api/v1/search.

Query params:

  • q (required, 2 ≤ length ≤ maxQueryLength) — search query
  • project_id (optional) — UUID-shaped scope filter
  • limit (optional, ≤ 50) — max rows (default 20)

Response:

  • 200 OK + JSON [SearchResult ...] on success
  • 400 on missing/short/over-long q
  • 403 on missing permission
  • 500 on missing BusinessContext (middleware misconfiguration)
  • 503 + Retry-After: 5 on cold-boot before indexes are ready
  • 500 on unexpected error or scope-guard surfacing (server-side bug)

type SoleOwnerEntry

type SoleOwnerEntry struct {
	ID   uuid.UUID
	Name string
}

SoleOwnerEntry is the public shape SoleOwnerExtractor returns. Mirrors service.SoleOwnerBusiness without an import cycle.

type TelemetryHandler

type TelemetryHandler struct {
	// contains filtered or unexported fields
}

TelemetryHandler handles frontend telemetry ingestion.

func NewTelemetryHandler

func NewTelemetryHandler(svc telemetryIngester) *TelemetryHandler

NewTelemetryHandler creates a new TelemetryHandler.

func (*TelemetryHandler) Ingest

func (h *TelemetryHandler) Ingest(w http.ResponseWriter, r *http.Request)

Ingest accepts an array of frontend telemetry events and persists them with the authenticated user_id stamped server-side. The request context carries a correlation_id from CorrelationID middleware.

func (*TelemetryHandler) IngestForBusiness

func (h *TelemetryHandler) IngestForBusiness(w http.ResponseWriter, r *http.Request)

IngestForBusiness accepts frontend telemetry for the authorized business in the request context. Both identifiers are stamped from server-owned context.

type TitlerHandler

type TitlerHandler struct {
	// contains filtered or unexported fields
}

TitlerHandler handles the auto-titler regenerate endpoint.

The titler field is typed as *service.Titler concretely: there is exactly ONE canonical mocking seam — the package-private chatCaller interface inside services/api/internal/service. Handler tests construct a real *service.Titler with a fake chatCaller via service.FakeChatCaller. NO parallel titlerCaller interface is introduced anywhere in this phase.

titler is allowed to be nil (graceful disable when TITLER_MODEL/LLM_MODEL is unset OR no LLM provider key is configured — Pitfall 1). The RegenerateTitle handler returns 503 in that case.

func NewTitlerHandler

func NewTitlerHandler(
	titler *service.Titler,
	businessService BusinessService,
	conversationRepo domain.ConversationRepository,
	messageRepo domain.MessageRepository,
	historyLimit int,
) *TitlerHandler

NewTitlerHandler constructs a TitlerHandler. titler may be nil (graceful disable). conversationRepo and messageRepo MUST be non-nil — they are wiring-time invariants and a nil there is a programmer bug. Mirror the service/hitl.go:92-123 nil-guard convention. A non-positive historyLimit falls back to defaultHistoryLimit.

func (*TitlerHandler) RegenerateTitle

func (h *TitlerHandler) RegenerateTitle(w http.ResponseWriter, r *http.Request)

RegenerateTitle handles POST /api/v1/conversations/{id}/regenerate-title.

State machine:

  1. middleware.GetUserID — 401 on miss. 1a. business soft-delete gate: h.businessService.GetByID — 404 on ErrBusinessNotFound (GetByID filters deleted_at IS NULL, so a soft-deleted organization reads as not-found and its conversations become unwritable), 500 on other err.
  2. h.conversationRepo.GetByID — 404 on ErrConversationNotFound, 500 on other err.
  3. ownership: conv.UserID != userID.String() → 403.
  4. titler-disabled gate: h.titler == nil → 503 (graceful disable).
  5. status check (verbatim Russian copy): - status == "manual" → 409 "Нельзя регенерировать — вы уже переименовали чат вручную" - status == "auto_pending" → 409 "Заголовок уже генерируется"
  6. atomic transition via TransitionToAutoPending; on ErrConversationNotFound → 409 "title_state_changed" (race window between the read in step 2 and this atomic write — manual rename arrived mid-flight).
  7. fetch user + assistant text; spawn titler goroutine on a fresh detached ctx with 30s timeout (r.Context() is unsafe — it's canceled at HTTP response close, the cheap-LLM call takes 3-8s).
  8. respond 200 (empty body) — fire-and-forget.

type TokenService

type TokenService interface {
	GetDecryptedToken(ctx context.Context, businessID uuid.UUID, platform, externalID, reason string) (*service.TokenResponse, error)
}

TokenService defines the interface for decrypted token retrieval

type ToolsCache

type ToolsCache interface {
	Has(toolName string) bool
}

ToolsCache is the narrow interface this handler needs from *service.ToolsRegistryCache. Declared locally so business_test doesn't need to import service for test-only fakes.

type UserDeletionHandler

type UserDeletionHandler struct {
	// contains filtered or unexported fields
}

UserDeletionHandler owns DELETE /users/me + POST /users/me/restore.

func NewUserDeletionHandler

func NewUserDeletionHandler(svc AccountDeletionServiceAPI, allowedOrigins []string) *UserDeletionHandler

NewUserDeletionHandler constructs the handler. allowedOrigins holds the CORS_ALLOWED_ORIGINS values for Restore's Origin-header CSRF check.

func (*UserDeletionHandler) Delete

Delete handles DELETE /api/v1/users/me. See <api_contract> in the plan for the canonical response shapes (204 / 401 / 403 / 409 / 423).

CSRF defense: the Origin header MUST match an allowed CORS origin (mirrors Restore). The endpoint is side-effecting (schedules account deletion) on an ambient cookie/Bearer session, so a CSRF-vulnerable path could trigger deletion from a hostile origin. Origin is set by browsers on every fetch/XHR and cannot be forged by JS in the victim's session.

func (*UserDeletionHandler) Restore

Restore handles POST /api/v1/users/me/restore. See <api_contract> in the plan for the canonical response shapes (204 / 403 / 404 / 410).

CSRF defense (T-DEL-10): Origin header MUST match an allowed CORS origin. The endpoint is side-effecting (clears deletion state) with no body, so a CSRF-vulnerable cookie-only path could trigger restore from a hostile origin. Origin is set by browsers on every fetch/XHR POST and cannot be forged by JS in the victim's session.

type UserService

type UserService interface {
	Register(ctx context.Context, email, password string) (*domain.User, error)
	// RegisterWithContext records three consents in the same tx as the user row.
	RegisterWithContext(ctx context.Context, email, password string, regCtx service.RegistrationContext) (*domain.User, error)
	Login(ctx context.Context, email, password string) (user *domain.User, accessToken, refreshToken string, err error)
	RefreshToken(ctx context.Context, refreshToken string) (user *domain.User, accessToken, newRefreshToken string, err error)
	Logout(ctx context.Context, refreshToken string) error
	GetByID(ctx context.Context, userID uuid.UUID) (*domain.User, error)
	ChangePassword(ctx context.Context, userID uuid.UUID, currentPassword, newPassword string) error
	UpdatePreferredLocale(ctx context.Context, userID uuid.UUID, locale string) error
	UpdateName(ctx context.Context, userID uuid.UUID, name string) error
}

UserService defines user-related operations.

type ValidationErrorResponse

type ValidationErrorResponse = openapi.ValidationErrorResponse

Re-export spec-owned error envelopes under the historic handler.* names so the existing test suite continues to compile against named handler types. Wire shape (keys + nesting) is identical to the legacy local definitions — both `error` and `fields` are required, matching writeJSONError / writeValidationError output.

type WeeklyValueRecapHandler

type WeeklyValueRecapHandler struct {
	// contains filtered or unexported fields
}

func NewWeeklyValueRecapHandler

func NewWeeklyValueRecapHandler(service weeklyValueRecapReader) *WeeklyValueRecapHandler

func (*WeeklyValueRecapHandler) GetLatest

GetLatest returns only the prior completed UTC week's row. Missing and suppressed weeks are 204, so an older row can never masquerade as current.

Directories

Path Synopsis
Package connect holds the paste-flow integration handlers — Telegram (Login Widget verify + bot-token connect) and VK community access-token paste.
Package connect holds the paste-flow integration handlers — Telegram (Login Widget verify + bot-token connect) and VK community access-token paste.
Package oauth holds the true OAuth code-flow integration handlers (VK, Yandex.Business, Google Business Profile).
Package oauth holds the true OAuth code-flow integration handlers (VK, Yandex.Business, Google Business Profile).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL