WiseHosting
Reference

Web (Dashboard API)

Function-level reference for /api/* routes, OAuth flows, sessions, alerts, usage, and embedded SPA wiring.

Files in internal/web/. The package owns every /api/* route, the OAuth flow, and the embedded SPA.

handler.go

Wires every HTTP route and owns the central Handler struct that every route method is bound to. It embeds the frontend SPA assets, registers every API endpoint under /api, OAuth redirect endpoints, the incoming git-webhook receiver, and SPA fallback routing. The auth middleware authenticates only via the wh_session cookie (DB-backed sessions).

  • NewHandler(cfg, db, bus, dispatcher, selector, stats, publisher, alertMgr) *Handler — constructs the handler, initialises the git-provider registry and three rate limiters (apiRateLimit, oauthRateLimit, webhookRateLimit); takes the alerts.Manager so alert routes can resolve/acknowledge.
  • (h) Mount(r *gin.Engine) — registers all routes on the Gin engine; embeds static SPA assets and sets the SPA catch-all. The inbound git webhook (POST /v1/webhooks/:provider/:uuid) is wrapped in webhookRateLimit (60/min/IP).
  • securityHeaders(c) — middleware; CSP, HSTS, referrer-policy, COOP, sanitised Permissions-Policy. The CSP script-src directive includes a sha256-... hash for the inline pre-React theme bootstrap script in assets/index.html (no unsafe-inline).
  • noStore(c) — middleware; Cache-Control: no-store, Pragma: no-cache on /api.
  • (h) auth(c) — middleware; resolves wh_session cookie via LookupDBSession, sets "user" and "session_id" in context; clears stale cookies.
  • currentUser(c) *User — extracts authenticated user from context.
  • (h) getMe(c)GET /api/me — profile (incl. theme_pref), app list (incl. repo_url + auto_deploy), plan limits, linked providers, platform config.
  • (h) deploy(c)POST /api/deploy — validates repo URL, framework, env keys, plan limits; picks worker; creates app+deployment+job; async registers provider webhook.
  • (h) getApp(c)GET /api/apps/:id — metadata, latest build logs, active job.
  • (h) listAppDeployments(c)GET /api/apps/:id/deployments — last 30.
  • (h) getDeploymentLogs(c)GET /api/deployments/:id/logs — single deployment detail with ownership check.
  • (h) restartApp(c)POST /api/apps/:id/restart — queues redeploy (optionally pinned to a commit), pushes job.
  • (h) startApp(c)POST /api/apps/:id/startstart job pinned to the current worker; flips a stopped app back to running without rebuilding the image.
  • (h) stopApp(c)POST /api/apps/:id/stopstop job pinned to current worker.
  • (h) deleteApp(c)DELETE /api/apps/:id — async deletes provider webhook, queues cleanup, hard-deletes app row, publishes app.deleted. Audited as app_delete.
  • (h) clearGitToken(c)DELETE /api/git-token/:provider — removes stored OAuth token. Audited as git_token_revoke.
  • (h) loadAppForUser(c) (*App, *User, bool) — shared helper; loads app by UUID, verifies ownership.

deploy, restartApp, startApp, stopApp, and deleteApp each call recordAuditEvent with the matching app_* kind so the activity feed shows every state transition.

Types

  • JobDispatcherPushJob(workerID, jobID int) interface.
  • WorkerSelectorSelectAvailableWorker(resources) (id, zone, err).
  • StatsReaderAppStats(appID) (cpuPct, memBytes, memLimit, updatedAt, ok).
  • Handler — central struct.

domains_api.go

Custom hostnames per app. The user proves control via a TXT record at _wisehosting.<hostname>; once verified, the host enters the worker's Traefik HTTP provider config on the next 2-second poll without restarting any container.

  • domainDTO — response shape, including verification_host (the FQDN the user must add a TXT record on) and verification_token.
  • toDomainDTO(d, appName, appSubdomain) — DB row → DTO.
  • newDomainToken() stringwh- + 24 random hex bytes.
  • validateHostname(raw) (string, error) — lowercase, dotted labels, label charset [a-z0-9-], ≤253 total / ≤63/label, no leading/trailing dash. Rejects anything that could survive into a Traefik Host() rule unescaped (no backticks/quotes/whitespace/scheme/path).
  • (h) listDomains(c)GET /api/domains — all of the user's domains plus stats: {total, verified, pending}.
  • (h) createDomain(c)POST /api/domains — body {hostname, app_uuid}. 409 on hostname already-claimed; 400 if hostname == app.subdomain (system-routed already). Audited as domain_create.
  • (h) verifyDomain(c)POST /api/domains/:id/verify — DNS TXT lookup over _wisehosting.<hostname> with an 8-second context. On match: stamps verified_at, audits as domain_verify. On miss: stores last_check_error and returns {verified: false, error}.
  • (h) deleteDomain(c)DELETE /api/domains/:id — audited as domain_delete. Routing drops on the next Traefik HTTP-provider poll.

oauth.go

OAuth2 flow for git providers (GitHub, GitLab, Bitbucket, Codeberg) — used only for linking after sign-in, never for primary auth. Issues HMAC-signed state tokens bound to a nonce cookie (CSRF defence).

  • (h) cookieSecure() bool — returns true if webapp.url is HTTPS.
  • issueOAuthBind(c, secure) (string, error) — random nonce + wh_oauth_bind cookie (10-min TTL).
  • clearOAuthBind(c, secure) — expires bind cookie.
  • bindHash(secret, nonce) string — HMAC-SHA256 of "bind:"+nonce, 32 hex chars.
  • (h) redirectURIFor(providerID) string — provider callback URL.
  • (h) oauthStartDispatch(c)GET /oauth/:provider/start — Google calls startGoogleOAuth; otherwise git OAuth redirect with signed state.
  • (h) oauthCallbackDispatch(c)GET /oauth/:provider/callback — verifies state+bind cookie, exchanges code, fetches profile, saves provider token.
  • (h) startGitOAuth(c)POST /api/git-oauth/start — authenticated; bind nonce + signed state, returns auth URL as JSON.
  • (h) listMyRepos(c)GET /api/repos — uses stored token; supports query, pagination, and an optional ?org= to scope to one of the user's GitHub orgs (delegates to ListReposScoped).
  • (h) listMyOrgs(c)GET /api/orgs?provider=github — calls ListOrgs with the stored token; surfaces {login, avatar_url, description} to drive the deploy dialog's org picker.
  • (h) listProviders(c)GET /api/providers — registered providers + configured flags.
  • signOAuthState(userID, secret, nonce) (string, error) — HMAC-signed state blob.
  • verifyOAuthState(state, secret, nonce) (int64, error) — verifies state+bind+expiry.
  • renderOAuthPage(c, title, body, ok) — minimal self-contained HTML result page.
  • oauthCSS(tone) / htmlEscape(s) — helpers.
  • SignCompletionToken / VerifyCompletionToken — vestigial helpers retained from the prior Telegram deep-link flow; not wired to any current route.

google.go

Google OAuth2 sign-in (the only sign-in path). Direct calls to oauth2.googleapis.com/token and googleapis.com/oauth2/v2/userinfo, then upserts the user, creates a DB-backed session, and audits the login.

  • (h) startGoogleOAuth(c) — Google consent URL with signed state + bind nonce.
  • (h) googleOAuthCallback(c) — verifies state, exchanges code, upserts user. Branches on totp_enabled: enrolled users get a pending_totps row, the wh_2fa_pending cookie, and a redirect to /2fa/challenge; everyone else falls straight through to CreateDBSession + wh_session. Always fires recordAuditEvent("login", …).
  • (h) logout(c)POST /api/logout — revokes the current session row, clears cookie, fires recordAuditEvent("logout", …).
  • exchangeGoogleCode(...) — POST token endpoint, GET userinfo; returns profile fields.

twofa.go

Per-user TOTP enrollment and the post-OAuth challenge step. Opt-in: a user with totp_enabled = false never sees a challenge.

  • (h) twoFASetup(c)POST /api/me/2fa/setup — generates a fresh TOTP secret (32 bytes base32), stores it AES-GCM-encrypted on the user, returns the otpauth:// provisioning URI plus a base64 PNG QR for the dashboard. Does not flip totp_enabled — that happens on first successful verify.
  • (h) twoFAVerify(c)POST /api/me/2fa/verify{code: "123456"}. On success: sets totp_enabled = true, stamps totp_verified_at, generates 10 single-use backup codes, replaces any prior backup_codes rows, returns the plaintext list once. Audits as 2fa_enabled.
  • (h) twoFADisable(c)POST /api/me/2fa/disable — clears totp_secret, flips totp_enabled = false, deletes all backup_codes. Audits as 2fa_disabled.
  • (h) twoFAChallengeStatus(c)GET /api/2fa/challenge/status — checks the wh_2fa_pending cookie and returns {valid: bool, expires_at}. Drives the SPA's challenge page so an expired link bounces back to login cleanly.
  • (h) twoFAChallenge(c)POST /api/2fa/challenge — body {code, type: "totp"|"backup"}. Validates against the encrypted secret or ConsumeBackupCode. On success: deletes the pending row, calls CreateDBSession(..., twoFactor: true), sets wh_session, clears wh_2fa_pending, audits with metadata: {2fa: true} (or backup_code: true). On failure: returns 401 without burning the pending row, so a typo doesn't kick the user back through Google. Backup-code path is rate-limited to 3 attempts per 15 minutes per pending token.

session.go

DB-backed sessions. Cookie body is pure entropy (no embedded user-id, no signature) — the row in sessions is the source of truth, and we only ever store sha256(token) so a DB leak doesn't hand out live sessions.

  • hashToken(token) string — sha256 hex of the cookie body.
  • newSessionToken() string — random opaque token (sent to the browser, never persisted).
  • CreateDBSession(c, db, userID, twoFactor bool) (token, sessionID, err) — inserts a row with IP, geo (LookupIP), user agent (capped at 512 bytes), created_at, last_seen_at, and two_factor_verified set from the caller (true only on the post-/api/2fa/challenge path).
  • LookupDBSession(c, db, token) (userID, sessionID, err) — resolves cookie to row; bumps last_seen_at at most once per minute. Treats sessions whose last_seen_at is older than SessionTTL as expired (rolling 30-day window — idle sessions die off, active ones renew transparently).
  • clientIP(c) string / capUA(ua) string — request helpers.
  • setSessionCookie(c, token, secure)wh_session HttpOnly SameSite=Lax, 30-day Max-Age.
  • clearSessionCookie(c) — expires cookie.

Constants: SessionCookieName = "wh_session", SessionTTL = 30 * 24h. A goroutine in main.go calls db.PruneRevokedSessions(SessionTTL) every 6 hours to hard-delete revoked or stale rows.


sessions_api.go

Endpoints behind Account → Security in the dashboard, plus the audit log used by the activity feed.

  • (h) recordAuditEvent(c, userID, kind, metadata) — appends to audit_events (renamed from login_events in migration 0005). On kind == "login", fires account.login_new_device webhook when this (user_id, ip) pair has never been seen.
  • (h) listSessions(c)GET /api/me/sessions — returns []sessionDTO with id, ip, city, country, user_agent, created_at, last_seen_at, current (bool), and a parsed device / browser / os triple.
  • (h) revokeSession(c)DELETE /api/me/sessions/:id — refuses to revoke the current session (caller should hit /api/logout).
  • (h) revokeOtherSessions(c)POST /api/me/sessions/revoke-others — bulk-revoke + audit each.
  • (h) listActivity(c)GET /api/me/activity?limit= (max 200) — returns []auditEventDTO newest first across every recorded kind (login/logout/session_revoked, app lifecycle, env edits, domain edits, git token revoke).
  • parseUA(ua) — cheap pattern matching to surface device/browser/os; deliberately library-free.

sessionDTO, auditEventDTO — response shapes.


iplookup.go

Best-effort IP → city/country resolver used by CreateDBSession and recordAuditEvent.

  • LookupIP(ctx, ip) IPGeo — returns {IP, City, Country, CountryCode}; uses ip-api.com's free no-auth endpoint with a 24h per-IP cache and a ~2s ceiling. Empty result for private/loopback/invalid IPs and on any provider failure — never blocks login.

preferences.go

  • (h) getPreferences(c)GET /api/me/preferences — returns {theme_pref} ("system"|"light"|"dark").
  • (h) updatePreferences(c)PATCH /api/me/preferences — validates and persists theme_pref.

env.go

Per-app env var CRUD, auto-deploy toggle, build-spec updates. All routes verify ownership via loadAppForUser.

  • (h) listAppEnv(c)GET /api/apps/:id/env.
  • (h) setAppEnv(c)PUT /api/apps/:id/env — upserts (max 100, max 8 KiB/value, key matches ^[A-Z][A-Z0-9_]*$).
  • (h) deleteAppEnv(c)DELETE /api/apps/:id/env/:key.
  • (h) updateAppBuild(c)PATCH /api/apps/:id/build — framework/root_dir/install/build/start.
  • (h) setAutoDeploy(c)PUT /api/apps/:id/auto-deploy.

env_workspace.go

Workspace-wide env var view + bulk .env import. No new DB table — reads/writes the existing app_env_vars rows across all of a user's apps.

  • (h) listWorkspaceEnv(c)GET /api/env — flat list of every env var across the user's apps, plus stats: {total, projects, projects_total, last_updated} and an apps summary list. Each entry: {id, app_id, app_uuid, app_name, key, value, created_at, updated_at}.
  • (h) bulkImportEnv(c)POST /api/apps/:id/env/import — body {content: ".env file text", replace: bool}; calls parseDotenv, validates keys, optionally deletes keys not in the upload (replace: true).
  • parseDotenv(content) (map, []errors) — line-by-line dotenv parser; returns parsed map and per-line error strings.

alert_rules_api.go

Per-app alert rule CRUD. Backed by the app_alert_rules table; the threshold engine in internal/alerts polls these on a 30s loop.

  • (h) listAppAlertRules(c)GET /api/apps/:id/alert-rules — auto-creates default rules on first call via EnsureAppAlertRules.
  • (h) updateAppAlertRules(c)PUT /api/apps/:id/alert-rules — upserts an array of {kind, enabled, threshold, sustain_minutes, severity}. Closed validRuleKinds set: cpu, memory, network, disk, offline, crashloop, deployment_failed.

alerts_api.go

Alert feed surfaced on the dashboard /alerts page.

  • (h) listAlerts(c)GET /api/alerts?status=&kind=&app_id=&limit= (limit max 500) — returns alerts plus stats: {total, active, warning, resolved_30d}.
  • (h) updateAlert(c)PATCH /api/alerts/:id — body {action: "acknowledge"|"resolve"|"reopen"}. Calls Manager.Resolve etc. for delegation to the alert manager.
  • iso(t) — nullable timestamp → optional ISO8601 string.

alertDTO — response shape (kind, severity, status, source, title, message, app, timestamps).


ratelimit.go

In-process sliding-window rate limiter; GC every 5 min.

  • newRateLimiter(limit, window) — constructor.
  • (r) allow(key) bool — checks + records hit; periodic GC.
  • (h) apiRateLimit(c) — keys by user:<id> if authed, else ip:<addr>.
  • (h) oauthRateLimit(c) — IP only.
  • (h) webhookRateLimit(c) — 60 requests / minute / IP, applied to POST /v1/webhooks/:provider/:uuid so a misbehaving git provider can't drown the inbound side.

regions.go

  • (h) listRegions(c)GET /api/regions — all workers minus stale (no heartbeat in 2 min); used_percent = max(cpu%, mem%); has_capacity = used < 95.

runtime_logs.go

  • (h) runtimeLogs(c)GET /api/apps/:id/logs/runtime — up to 500 (default) / 1000 (max) lines for one app with next_since cursor.
  • (h) runtimeLogsAll(c)GET /api/logs/runtime — up to 1000/2000 lines across all user apps, optional app filter.

usage.go

  • (h) getUsage(c)GET /api/usage — iterates user apps, calls h.stats.AppStatsLive for running apps, aggregates totals.
  • (h) getAppLiveStats(c)GET /api/apps/:id/live-stats — single-app live snapshot (CPU%, mem, mem-limit, net Mbps, net total bytes, disk Mbps).

appUsageDTO, usageDTO — response shapes.


usage_timeseries.go

Reads the usage_samples table that internal/usage.Recorder populates every minute, rolled up to the requested granularity for the dashboard's Usage page.

  • (h) getUsageTimeseries(c)GET /api/usage/timeseries — query params: from / to (RFC3339), granularity (auto|5m|1h|1d), optional app_id.
  • parseRange(c) — bounded parse; max range 365 days.
  • resolveGranularity(g, from, to) intauto rule: ≤24h → 5-min, ≤7d → 1-hour, else 1-day.
  • resolveScope(c, h, userID) ([]int, name, error) — single-app vs all-user-apps.
  • rollupBuckets(raw, sec, names, userScope) — sums same-bucket samples (avg-of-avgs approximation; max-of-maxes; cumulative net_bytes_delta).
  • sumByApp(raw, names) []timeseriesAppSum — totals for the donut chart.

timeseriesBucket, timeseriesAppSum, timeseriesResponse — response shapes ({granularity, from, to, buckets, apps}). 90-day retention is enforced by the recorder, not the endpoint.


webhooks.go

Provider-side (git) webhook lifecycle + inbound push handler.

  • generateWebhookSecret() string — 32 random hex bytes.
  • (h) registerProviderWebhook(app, gitToken) — async; p.RegisterWebhook then save hook ID.
  • (h) deleteProviderWebhook(app, gitToken) — async; p.DeleteWebhook.
  • (h) gitWebhook(c)POST /v1/webhooks/:provider/:uuid — verifies HMAC, skips non-push and auto-deploy=false, extracts commit, queues redeploy, pushes job.
  • extractPushCommit(providerID, body) (commit, branch) — parses provider-specific push payloads.

webhooks_api.go

CRUD for outbound user-configurable webhooks. Targets are either signed-HTTPS endpoints or Shoutrrr-style URLs for any of 22 supported chat channels.

  • shoutrrrSchemes — closed set of accepted non-HTTPS schemes: discord, slack, telegram, ntfy, gotify, pushover, pushbullet, matrix, teams, mattermost, rocketchat, googlechat, zulip, lark, wecom, opsgenie, ifttt, join, bark, twilio, signal, mqtt / mqtts, generic.
  • validateWebhookURL(raw) error — accepts https:// or any Shoutrrr scheme; rejects plain http://. Logs the specific reason server-side; always returns the opaque errInvalidWebhookURL ("invalid url") to the client.
  • toWebhookDTO(h, appName) — DB row to API shape.
  • (h) listUserWebhooks(c)GET /api/webhooks — list + stats summary + valid event names.
  • (h) createWebhook(c)POST /api/webhooks — returns DTO + plain-text secret (only this once).
  • (h) buildWebhook(...) — validates name, URL (via validateWebhookURL), event names (supports * and prefix.* wildcards), optional app ownership; generates secret.
  • newWebhookSecret() stringwhsec_ + 32 hex bytes.
  • (h) updateWebhook(c)PATCH /api/webhooks/:id.
  • (h) deleteWebhook(c)DELETE /api/webhooks/:id.
  • (h) testWebhook(c)POST /api/webhooks/:id/test — fires one synchronous EventTest delivery via Dispatcher.TestDeliver, persists the resulting webhook_deliveries row, and updates the webhook's last_status_code / last_success / last_delivery_at. Useful for flipping a freshly-created hook from "pending first delivery" to a real status without waiting for an actual event.
  • (h) listWebhookDeliveries(c)GET /api/webhooks/:id/deliveries — last 50 attempts.

webhookDTO — response shape.


recent.go

  • (h) listRecentDeployments(c)GET /api/deployments/recent — last 8 deployments across user's apps.

frameworks.go

  • (h) listFrameworks(c)GET /api/frameworksframeworks.All().

On this page