package app

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"embed"
	"encoding/pem"
	"fmt"
	"io/fs"
	"log"
	"net"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/caddyserver/certmagic"
	"github.com/gin-gonic/gin"
	"github.com/phishingclub/phishingclub/acme"
	"github.com/phishingclub/phishingclub/build"
	"github.com/phishingclub/phishingclub/config"
	"github.com/phishingclub/phishingclub/errs"
	"github.com/phishingclub/phishingclub/frontend"
	"github.com/phishingclub/phishingclub/server"
	"go.uber.org/zap"
)

const (
	// health
	ROUTE_V1_HEALTH = "/api/v1/healthz"
	ROUTE_V1_LIVE   = "/api/v1/livez"
	ROUTE_V1_READY  = "/api/v1/readyz"
	// application
	ROUTE_V1_FEATURE                 = "/api/v1/features"
	ROUTE_V1_UPDATE_AVAILABLE        = "/api/v1/update/available"
	ROUTE_V1_UPDATE_AVAILABLE_CACHED = "/api/v1/update/available/cached"
	ROUTE_V1_UPDATE                  = "/api/v1/update"
	// backup
	ROUTE_V1_BACKUP_CREATE   = "/api/v1/backup/create"
	ROUTE_V1_BACKUP_LIST     = "/api/v1/backup/list"
	ROUTE_V1_BACKUP_DOWNLOAD = "/api/v1/backup/download/:filename"
	// user
	ROUTE_V1_USER        = "/api/v1/user"
	ROUTE_V1_USER_ID     = "/api/v1/user/:id"
	ROUTE_V1_USER_LOGIN  = "/api/v1/user/login"
	ROUTE_V1_USER_LOGOUT = "/api/v1/user/logout"
	// #nosec
	ROUTE_V1_USER_PASSWORD            = "/api/v1/user/password"
	ROUTE_V1_USER_USERNAME            = "/api/v1/user/username"
	ROUTE_V1_USER_FULLNAME            = "/api/v1/user/fullname"
	ROUTE_V1_USER_EMAIL               = "/api/v1/user/email"
	ROUTE_V1_USER_SESSIONS            = "/api/v1/user/sessions"
	ROUTE_V1_USER_SESSIONS_INVALIDATE = "/api/v1/user/sessions/invalidate"
	ROUTE_V1_USER_API                 = "/api/v1/user/api"
	// sso
	// the config upsert and the enabled status are provider neutral, they serve
	// both Entra ID and generic OIDC
	ROUTE_V1_SSO                   = "/api/v1/sso"
	ROUTE_V1_SSO_ENABLED           = "/api/v1/sso/enabled"
	ROUTE_V1_SSO_ENTRA_ID_LOGIN    = "/api/v1/sso/entra-id/login"
	ROUTE_V1_SSO_ENTRA_ID_CALLBACK = "/api/v1/sso/entra-id/auth"
	ROUTE_V1_SSO_OIDC_LOGIN        = "/api/v1/sso/oidc/login"
	ROUTE_V1_SSO_OIDC_CALLBACK     = "/api/v1/sso/oidc/auth"
	// mfa
	ROUTE_V1_USER_MFA_TOTP_SETUP        = "/api/v1/user/mfa/totp/setup"
	ROUTE_V1_USER_MFA_TOTP_SETUP_VERIFY = "/api/v1/user/mfa/totp/setup/verify"
	ROUTE_V1_USER_MFA_TOTP_VERIFY       = "/api/v1/user/mfa/totp/verify"
	ROUTE_V1_USER_MFA_TOTP              = "/api/v1/user/mfa/totp"
	ROUTE_V1_QR_FROM_TOTP               = "/api/v1/qr/totp"
	ROUTE_V1_QR_URL_TO_HTML             = "/api/v1/qr/html"
	// session
	ROUTE_V1_SESSION_ID   = "/api/v1/session/:id"
	ROUTE_V1_SESSION_PING = "/api/v1/session/ping"
	// company
	ROUTE_V1_COMPANY                   = "/api/v1/company"
	ROUTE_V1_COMPANY_ID                = "/api/v1/company/:id"
	ROUTE_V1_COMPANY_ID_EXPORT         = "/api/v1/company/:id/export"
	ROUTE_V1_COMPANY_ID_EXPORT_SHARED  = "/api/v1/company/shared/export"
	ROUTE_V1_COMPANY_SCIM              = "/api/v1/company/scim/:companyID"
	ROUTE_V1_COMPANY_SCIM_TOKEN        = "/api/v1/company/scim/:companyID/token"
	ROUTE_V1_COMPANY_SCIM_PRUNE        = "/api/v1/company/scim/:companyID/prune"
	ROUTE_V1_COMPANY_SCIM_RESTORE      = "/api/v1/company/scim/:companyID/restore"
	ROUTE_V1_COMPANY_REPORT_CONFIG     = "/api/v1/company/report-config/:companyID"
	ROUTE_V1_COMPANY_REPORT_CONFIG_LOG = "/api/v1/company/report-config/:companyID/log"
	ROUTE_V1_REPORT_CONFIG_GLOBAL      = "/api/v1/report-config"
	ROUTE_V1_REPORT_CONFIG_SEND        = "/api/v1/report-config/send/:id"
	// scim v2 provisioning endpoints (public — authenticated via bearer token)
	ROUTE_SCIM_V2_SERVICE_PROVIDER_CONFIG = "/api/v1/scim/v2/:companyID/ServiceProviderConfig"
	ROUTE_SCIM_V2_RESOURCE_TYPES          = "/api/v1/scim/v2/:companyID/ResourceTypes"
	ROUTE_SCIM_V2_SCHEMAS                 = "/api/v1/scim/v2/:companyID/Schemas"
	ROUTE_SCIM_V2_SCHEMA_ID               = "/api/v1/scim/v2/:companyID/Schemas/:schemaID"
	ROUTE_SCIM_V2_RESOURCE_TYPE_ID        = "/api/v1/scim/v2/:companyID/ResourceTypes/:resourceTypeID"
	ROUTE_SCIM_V2_USERS                   = "/api/v1/scim/v2/:companyID/Users"
	ROUTE_SCIM_V2_USER_ID                 = "/api/v1/scim/v2/:companyID/Users/:userID"
	ROUTE_SCIM_V2_GROUPS                  = "/api/v1/scim/v2/:companyID/Groups"
	ROUTE_SCIM_V2_GROUP_ID                = "/api/v1/scim/v2/:companyID/Groups/:groupID"
	// option
	ROUTE_V1_OPTION                = "/api/v1/option"
	ROUTE_V1_OPTION_GET            = "/api/v1/option/:key"
	ROUTE_V1_OPTION_SCIM_DOMAIN    = "/api/v1/option/scim-domain"
	ROUTE_V1_OPTION_SCIM_RETENTION = "/api/v1/option/scim-retention-days"
	// auto-prune options
	ROUTE_V1_OPTION_AUTO_PRUNE         = "/api/v1/option/auto-prune"
	ROUTE_V1_COMPANY_OPTION_AUTO_PRUNE = "/api/v1/company/:id/option/auto-prune"
	// installation
	ROUTE_V1_INSTALL           = "/api/v1/install"
	ROUTE_V1_INSTALL_TEMPLATES = "/api/v1/install/templates"
	// domain
	ROUTE_V1_DOMAIN                   = "/api/v1/domain"
	ROUTE_V1_DOMAIN_SUBSET            = "/api/v1/domain/subset"
	ROUTE_V1_DOMAIN_SUBSET_NO_PROXIES = "/api/v1/domain/subset/noproxies"
	ROUTE_V1_DOMAIN_ID                = "/api/v1/domain/:id"
	ROUTE_V1_DOMAIN_NAME              = "/api/v1/domain/name/:domain"
	// page
	ROUTE_V1_PAGE            = "/api/v1/page"
	ROUTE_V1_PAGE_OVERVIEW   = "/api/v1/page/overview"
	ROUTE_V1_PAGE_ID         = "/api/v1/page/:id"
	ROUTE_V1_PAGE_CONTENT_ID = "/api/v1/page/:id/content"
	// proxy
	ROUTE_V1_PROXY          = "/api/v1/proxy"
	ROUTE_V1_PROXY_OVERVIEW = "/api/v1/proxy/overview"
	ROUTE_V1_PROXY_ID       = "/api/v1/proxy/:id"
	// ip allow list
	ROUTE_V1_IP_ALLOW_LIST_PROXY_CONFIG       = "/api/v1/ip-allow-list/proxy-config/:id"
	ROUTE_V1_IP_ALLOW_LIST_CLEAR_PROXY_CONFIG = "/api/v1/ip-allow-list/clear-proxy-config/:id"
	// recipient and groups
	ROUTE_V1_RECIPIENT                  = "/api/v1/recipient"
	ROUTE_V1_RECIPIENT_IMPORT           = "/api/v1/recipient/import"
	ROUTE_V1_RECIPIENT_EXPORT           = "/api/v1/recipient/:id/export"
	ROUTE_V1_RECIPIENT_ID               = "/api/v1/recipient/:id"
	ROUTE_V1_RECIPIENT_ID_EVENTS        = "/api/v1/recipient/:id/events"
	ROUTE_V1_RECIPIENT_ID_STATS         = "/api/v1/recipient/:id/stats"
	ROUTE_V1_RECIPIENT_REPEAT_OFFENDERS = "/api/v1/recipient/repeat-offenders"
	ROUTE_V1_RECIPIENT_ORPHANED         = "/api/v1/recipient/orphaned"
	ROUTE_V1_RECIPIENT_ORPHANED_DELETE  = "/api/v1/recipient/orphaned/delete"
	ROUTE_V1_RECIPIENT_GROUP            = "/api/v1/recipient/group"
	ROUTE_V1_RECIPIENT_GROUP_ID         = "/api/v1/recipient/group/:id"
	ROUTE_V1_RECIPIENT_GROUP_ID_IMPORT  = "/api/v1/recipient/group/:id/import"
	ROUTE_V1_RECIPIENT_GROUP_RECIPIENTS = "/api/v1/recipient/group/:id/recipients"
	// logging
	ROUTE_V1_LOG      = "/api/v1/log"
	ROUTE_V1_LOG_TEST = "/api/v1/log/test"
	// smtp configuration
	ROUTE_V1_SMTP_CONFIGURATION               = "/api/v1/smtp-configuration"
	ROUTE_V1_SMTP_CONFIGURATION_ID            = "/api/v1/smtp-configuration/:id"
	ROUTE_V1_SMTP_CONFIGURATION_ID_TEST_EMAIL = "/api/v1/smtp-configuration/:id/test-email"
	ROUTE_V1_SMTP_CONFIGURATION_HEADERS       = "/api/v1/smtp-configuration/:id/header"
	ROUTE_V1_SMTP_HEADER_ID                   = "/api/v1/smtp-configuration/:id/header/:headerID"
	// email
	ROUTE_V1_EMAIL            = "/api/v1/email"
	ROUTE_V1_EMAIL_OVERVIEW   = "/api/v1/email/overview"
	ROUTE_V1_EMAIL_ID         = "/api/v1/email/:id"
	ROUTE_V1_EMAIL_SEND_TEST  = "/api/v1/email/:id/send-test"
	ROUTE_V1_EMAIL_CONTENT_ID = "/api/v1/email/:id/content"
	// campaign
	ROUTE_V1_CAMPAIGN_TEMPLATE           = "/api/v1/campaign/template"
	ROUTE_v1_CAMPAIGN_TEMPLATE_ID        = "/api/v1/campaign/template/:id"
	ROUTE_V1_CAMPAIGN                    = "/api/v1/campaign"
	ROUTE_V1_CAMPAIGN_CALENDAR           = "/api/v1/campaign/calendar"
	ROUTE_V1_CAMPAIGN_ACTIVE             = "/api/v1/campaign/active"
	ROUTE_V1_CAMPAIGN_UPCOMING           = "/api/v1/campaign/upcoming"
	ROUTE_V1_CAMPAIGN_FINISHED           = "/api/v1/campaign/finished"
	ROUTE_V1_CAMPAIGN_CLOSE              = "/api/v1/campaign/:id/close"
	ROUTE_V1_CAMPAIGN_EXPORT_EVENTS      = "/api/v1/campaign/:id/export/events"
	ROUTE_V1_CAMPAIGN_EXPORT_SUBMISSIONS = "/api/v1/campaign/:id/export/submissions"
	ROUTE_V1_CAMPAIGN_ANONYMIZE          = "/api/v1/campaign/:id/anonymize"
	ROUTE_V1_CAMPAIGN_ANONYMIZE_DATA     = "/api/v1/campaign/:id/anonymize-data"
	ROUTE_V1_CAMPAIGN_ID                 = "/api/v1/campaign/:id"
	ROUTE_V1_CAMPAIGN_NAME               = "/api/v1/campaign/name/:name"
	ROUTE_V1_CAMPAIGN_RECIPIENTS         = "/api/v1/campaign/:id/recipients"
	ROUTE_V1_CAMPAIGN_RESULT_STATS       = "/api/v1/campaign/:id/statistics"
	ROUTE_V1_CAMPAIGN_EVENTS             = "/api/v1/campaign/:id/events"
	ROUTE_V1_CAMPAIGN_ALL_EVENTS         = "/api/v1/campaign/events"
	ROUTE_V1_CAMPAIGN_EVENT_ID           = "/api/v1/campaign/event/:id"
	ROUTE_V1_CAMPAIGN_EVENT_NAMES        = "/api/v1/campaign/event-types"
	ROUTE_V1_CAMPAIGN_STATS              = "/api/v1/campaign/statistics"
	ROUTE_V1_CAMPAIGN_STATS_ID           = "/api/v1/campaign/:id/stats"
	ROUTE_V1_CAMPAIGN_STATS_ALL          = "/api/v1/campaign/stats/all"
	ROUTE_V1_CAMPAIGN_STATS_CREATE       = "/api/v1/campaign/stats"
	ROUTE_V1_CAMPAIGN_STATS_MANUAL       = "/api/v1/campaign/stats/manual"
	ROUTE_V1_CAMPAIGN_STATS_UPDATE       = "/api/v1/campaign/stats/:id"
	ROUTE_V1_CAMPAIGN_STATS_DELETE       = "/api/v1/campaign/stats/:id"
	ROUTE_V1_CAMPAIGN_UPLOAD_REPORTED    = "/api/v1/campaign/:id/upload/reported"
	ROUTE_V1_CAMPAIGN_DEVICE_CODES       = "/api/v1/campaign/:id/device-codes"
	ROUTE_V1_CAMPAIGN_REPORT             = "/api/v1/campaign/:id/report"
	// report templates
	ROUTE_V1_REPORT_TEMPLATE          = "/api/v1/report-template"
	ROUTE_V1_REPORT_TEMPLATE_ID       = "/api/v1/report-template/:id"
	ROUTE_V1_REPORT_PDF_BROWSER_CACHE = "/api/v1/report-pdf/browser-cache"
	// campaign-recipient
	ROUTE_V1_CAMPAIGN_RECIPIENT_EMAIL      = "/api/v1/campaign/recipient/:id/email"
	ROUTE_V1_CAMPAIGN_RECIPIENT_URL        = "/api/v1/campaign/recipient/:id/url"
	ROUTE_V1_CAMPAIGN_RECIPIENT_SET_SENT   = "/api/v1/campaign/recipient/:id/sent"
	ROUTE_V1_CAMPAIGN_RECIPIENT_LURE_CODE  = "/api/v1/campaign/recipient/:id/lure-code"
	ROUTE_V1_CAMPAIGN_RECIPIENT_SEND_EMAIL = "/api/v1/campaign/recipient/:id/send"
	// asset
	ROUTE_V1_ASSET                = "/api/v1/asset"
	ROUTE_V1_ASSET_ID             = "/api/v1/asset/:id"
	ROUTE_V1_ASSET_DOMAIN_CONTEXT = "/api/v1/asset/domain/:domain"
	ROUTE_V1_ASSET_GLOBAL_CONTEXT = "/api/v1/asset/domain/"
	ROUTE_V1_ASSET_DOMAIN_VIEW    = "/api/v1/asset/view/domain/:domain/*path"
	// attachments
	ROUTE_V1_ATTACHMENT                 = "/api/v1/attachment"
	ROUTE_V1_ATTACHMENT_ID              = "/api/v1/attachment/:id"
	ROUTE_V1_ATTACHMENT_ID_CONTENT      = "/api/v1/attachment/:id/content"
	ROUTE_V1_ATTACHMENT_COMPANY_CONTEXT = "/api/v1/attachment/company/:companyID"
	ROUTE_V1_ATTACHMENT_GLOBAL_CONTEXT  = "/api/v1/attachment/company/"
	ROUTE_V1_EMAIL_ATTACHMENT           = "/api/v1/email/:id/attachment"
	// api sender
	ROUTE_V1_API_SENDER          = "/api/v1/api-sender"
	ROUTE_V1_API_SENDER_OVERVIEW = "/api/v1/api-sender/overview"
	ROUTE_V1_API_SENDER_ID       = "/api/v1/api-sender/:id"
	ROUTE_V1_API_SENDER_ID_TEST  = "/api/v1/api-sender/:id/test"
	// deny allow
	ROUTE_V1_ALLOW_DENY          = "/api/v1/allow-deny"
	ROUTE_V1_ALLOW_DENY_OVERVIEW = "/api/v1/allow-deny/overview"
	ROUTE_V1_ALLOW_DENY_ID       = "/api/v1/allow-deny/:id"
	// geoip
	ROUTE_V1_GEOIP_METADATA = "/api/v1/geoip/metadata"
	ROUTE_V1_GEOIP_LOOKUP   = "/api/v1/geoip/lookup"
	// web hooks
	ROUTE_V1_WEBHOOK         = "/api/v1/webhook"
	ROUTE_V1_WEBHOOK_ID      = "/api/v1/webhook/:id"
	ROUTE_V1_WEBHOOK_ID_TEST = "/api/v1/webhook/:id/test"
	// identifiers
	ROUTE_V1_IDENTIFIER = "/api/v1/identifier"
	// oauth providers
	ROUTE_V1_OAUTH_PROVIDER             = "/api/v1/oauth-provider"
	ROUTE_V1_OAUTH_PROVIDER_ID          = "/api/v1/oauth-provider/:id"
	ROUTE_V1_OAUTH_PROVIDER_REMOVE_AUTH = "/api/v1/oauth-provider/:id/remove-authorization"
	ROUTE_V1_OAUTH_AUTHORIZE            = "/api/v1/oauth-authorize/:id"
	ROUTE_V1_OAUTH_CALLBACK             = "/api/v1/oauth-callback"
	ROUTE_V1_OAUTH_IMPORT_TOKENS        = "/api/v1/oauth-provider/import-tokens"
	ROUTE_V1_OAUTH_EXPORT_TOKENS        = "/api/v1/oauth-provider/:id/export-tokens"
	// license
	ROUTE_V1_LICENSE = "/api/v1/license"
	// version
	ROUTE_V1_VERSION = "/api/v1/version"
	// import
	ROUTE_V1_IMPORT = "/api/v1/import"
	// remote browser
	ROUTE_V1_REMOTE_BROWSER             = "/api/v1/remote-browser"
	ROUTE_V1_REMOTE_BROWSER_OVERVIEW    = "/api/v1/remote-browser/overview"
	ROUTE_V1_REMOTE_BROWSER_ID          = "/api/v1/remote-browser/:id"
	ROUTE_V1_REMOTE_BROWSER_ID_RUN      = "/api/v1/remote-browser/:id/run"
	ROUTE_V1_REMOTE_BROWSER_LIVE        = "/api/v1/remote-browser/live"
	ROUTE_V1_REMOTE_BROWSER_LIVE_CRID   = "/api/v1/remote-browser/live/:crID"
	ROUTE_V1_REMOTE_BROWSER_LIVE_STREAM = "/api/v1/remote-browser/live/:crID/stream"
)

// administrationServer is the administrationServer app
type administrationServer struct {
	Server             *http.Server
	router             *gin.Engine
	logger             *zap.SugaredLogger
	production         bool
	embedBackendFS     *embed.FS
	certMagicConfig    *certmagic.Config
	softSessionHandler gin.HandlerFunc
}

// NewAdministrationServer creates a new administration app
func NewAdministrationServer(
	router *gin.Engine,
	controllers *Controllers,
	middlewares *Middlewares,
	logger *zap.SugaredLogger,
	certMagicConfig *certmagic.Config,
	production bool,
) *administrationServer {
	router = setupRoutes(router, controllers, middlewares)

	return &administrationServer{
		router:             router,
		logger:             logger,
		production:         production,
		certMagicConfig:    certMagicConfig,
		softSessionHandler: middlewares.SoftSessionHandler,
	}
}

func (a *administrationServer) Router() *gin.Engine {
	return a.router
}

// setupRoutes sets up the routes for the administration app.
// ip-protected admin routes are registered under a group that applies the
// IPLimiter middleware. scim v2 provisioning routes are registered directly on
// the engine so that external identity providers are never blocked by the
// admin ip allowlist.
func setupRoutes(
	r *gin.Engine,
	controllers *Controllers,
	middleware *Middlewares,
) *gin.Engine {
	// SCIM v2 provisioning endpoints are NOT served here — they live on the
	// phishing server (app/server.go AssignRoutes), gated to a single global
	// domain, so the admin port does not need public exposure for SCIM.

	// all other admin routes are protected by the ip allowlist middleware
	admin := r.Group("/", middleware.IPLimiter)
	_ = admin

	if !build.Flags.Production {
		admin.
			GET("/api/v1/_debug/panic", middleware.SessionHandler, controllers.Log.Panic).
			GET("/api/v1/_debug/slow", middleware.SessionHandler, controllers.Log.Slow)
	}

	admin.
		// log
		GET(ROUTE_V1_LOG, middleware.SessionHandler, controllers.Log.GetLevel).
		POST(ROUTE_V1_LOG, middleware.SessionHandler, controllers.Log.SetLevel).
		GET(ROUTE_V1_LOG_TEST, middleware.SessionHandler, controllers.Log.TestLog).
		// application
		GET(ROUTE_V1_UPDATE_AVAILABLE, middleware.SessionHandler, controllers.Update.CheckForUpdate).
		GET(ROUTE_V1_UPDATE_AVAILABLE_CACHED, middleware.SessionHandler, controllers.Update.CheckForUpdateCached).
		// health
		GET(ROUTE_V1_HEALTH, controllers.Health.Health).
		GET(ROUTE_V1_LIVE, controllers.Health.Health).
		GET(ROUTE_V1_READY, controllers.Health.Health).
		// login, logout and session
		GET(ROUTE_V1_SESSION_PING, middleware.SessionHandler, controllers.User.SessionPing).
		POST(ROUTE_V1_USER_LOGIN, middleware.LoginRateLimiter, controllers.User.Login).
		POST(ROUTE_V1_USER_LOGOUT, controllers.User.Logout).
		// install
		POST(ROUTE_V1_INSTALL, middleware.SessionHandler, controllers.Installer.Install).
		POST(ROUTE_V1_INSTALL_TEMPLATES, middleware.SessionHandler, controllers.Installer.InstallTemplates).
		// user
		GET(ROUTE_V1_USER, middleware.SessionHandler, controllers.User.GetAll).
		GET(ROUTE_V1_USER_ID, middleware.SessionHandler, controllers.User.GetByID).
		POST(ROUTE_V1_USER_ID, middleware.SessionHandler, controllers.User.UpdateByID).
		POST(ROUTE_V1_USER, middleware.SessionHandler, controllers.User.Create).
		DELETE(ROUTE_V1_USER_ID, middleware.SessionHandler, controllers.User.Delete).
		POST(ROUTE_V1_USER_PASSWORD, middleware.SessionHandler, controllers.User.ChangePasswordOnLoggedInUser).
		POST(ROUTE_V1_USER_USERNAME, middleware.SessionHandler, controllers.User.ChangeUsernameOnLoggedInUser).
		POST(ROUTE_V1_USER_FULLNAME, middleware.SessionHandler, controllers.User.ChangeFullnameOnLoggedInUser).
		POST(ROUTE_V1_USER_EMAIL, middleware.SessionHandler, controllers.User.ChangeEmailOnLoggedInUser).
		GET(ROUTE_V1_USER_SESSIONS, middleware.SessionHandler, controllers.User.GetSessionsOnLoggedInUser).
		POST(ROUTE_V1_USER_SESSIONS_INVALIDATE, middleware.SessionHandler, controllers.User.InvalidateAllSessionByUserID).
		DELETE(ROUTE_V1_SESSION_ID, middleware.SessionHandler, controllers.User.ExpireSessionByID).
		GET(ROUTE_V1_USER_API, middleware.SessionHandler, controllers.User.GetMaskedAPIKey).
		POST(ROUTE_V1_USER_API, middleware.SessionHandler, controllers.User.UpsertAPIKey).
		DELETE(ROUTE_V1_USER_API, middleware.SessionHandler, controllers.User.RemoveAPIKey).
		// sso
		GET(ROUTE_V1_SSO_ENABLED, controllers.SSO.IsEnabled).
		POST(ROUTE_V1_SSO, middleware.SessionHandler, controllers.SSO.Upsert).
		GET(ROUTE_V1_SSO_ENTRA_ID_LOGIN, controllers.SSO.EntreIDLogin).
		GET(ROUTE_V1_SSO_ENTRA_ID_CALLBACK, controllers.SSO.EntreIDCallBack).
		GET(ROUTE_V1_SSO_OIDC_LOGIN, controllers.SSO.OIDCLogin).
		GET(ROUTE_V1_SSO_OIDC_CALLBACK, controllers.SSO.OIDCCallback).
		// user mfa
		GET(ROUTE_V1_USER_MFA_TOTP, middleware.SessionHandler, controllers.User.IsTOTPEnabled).
		POST(ROUTE_V1_USER_MFA_TOTP_SETUP, middleware.LoginRateLimiter, middleware.SessionHandler, controllers.User.SetupTOTP).
		POST(ROUTE_V1_USER_MFA_TOTP_SETUP_VERIFY, middleware.LoginRateLimiter, middleware.SessionHandler, controllers.User.SetupVerifyTOTP).
		POST(ROUTE_V1_USER_MFA_TOTP_VERIFY, middleware.LoginRateLimiter, middleware.SessionHandler, controllers.User.VerifyTOTP).
		POST(ROUTE_V1_USER_MFA_TOTP, middleware.LoginRateLimiter, middleware.SessionHandler, controllers.User.DisableTOTP).
		// qr
		POST(ROUTE_V1_QR_FROM_TOTP, middleware.SessionHandler, controllers.QR.ToTOTPURL).
		POST(ROUTE_V1_QR_URL_TO_HTML, middleware.SessionHandler, controllers.QR.ToHTML).
		// company
		POST(ROUTE_V1_COMPANY, middleware.SessionHandler, controllers.Company.Create).
		POST(ROUTE_V1_COMPANY_ID, middleware.SessionHandler, controllers.Company.ChangeName).
		GET(ROUTE_V1_COMPANY, middleware.SessionHandler, controllers.Company.GetAll).
		GET(ROUTE_V1_COMPANY_ID_EXPORT, middleware.SessionHandler, controllers.Company.ExportByCompanyID).
		GET(ROUTE_V1_COMPANY_ID_EXPORT_SHARED, middleware.SessionHandler, controllers.Company.ExportShared).
		GET(ROUTE_V1_COMPANY_ID, middleware.SessionHandler, controllers.Company.GetByID).
		DELETE(ROUTE_V1_COMPANY_ID, middleware.SessionHandler, controllers.Company.DeleteByID).
		// company scim
		GET(ROUTE_V1_COMPANY_SCIM, middleware.SessionHandler, controllers.CompanyScimConfig.GetByCompanyID).
		POST(ROUTE_V1_COMPANY_SCIM, middleware.SessionHandler, controllers.CompanyScimConfig.Upsert).
		DELETE(ROUTE_V1_COMPANY_SCIM, middleware.SessionHandler, controllers.CompanyScimConfig.Delete).
		POST(ROUTE_V1_COMPANY_SCIM_TOKEN, middleware.SessionHandler, controllers.CompanyScimConfig.RotateToken).
		POST(ROUTE_V1_COMPANY_SCIM_PRUNE, middleware.SessionHandler, controllers.CompanyScimConfig.Prune).
		POST(ROUTE_V1_COMPANY_SCIM_RESTORE, middleware.SessionHandler, controllers.CompanyScimConfig.Restore).
		// company report delivery
		GET(ROUTE_V1_COMPANY_REPORT_CONFIG, middleware.SessionHandler, controllers.CompanyReportConfig.GetByCompanyID).
		POST(ROUTE_V1_COMPANY_REPORT_CONFIG, middleware.SessionHandler, controllers.CompanyReportConfig.Upsert).
		DELETE(ROUTE_V1_COMPANY_REPORT_CONFIG, middleware.SessionHandler, controllers.CompanyReportConfig.Delete).
		GET(ROUTE_V1_COMPANY_REPORT_CONFIG_LOG, middleware.SessionHandler, controllers.CompanyReportConfig.GetLogByCompanyID).
		// global default report delivery
		GET(ROUTE_V1_REPORT_CONFIG_GLOBAL, middleware.SessionHandler, controllers.CompanyReportConfig.GetGlobal).
		POST(ROUTE_V1_REPORT_CONFIG_GLOBAL, middleware.SessionHandler, controllers.CompanyReportConfig.UpsertGlobal).
		DELETE(ROUTE_V1_REPORT_CONFIG_GLOBAL, middleware.SessionHandler, controllers.CompanyReportConfig.DeleteGlobal).
		POST(ROUTE_V1_REPORT_CONFIG_SEND, middleware.SessionHandler, controllers.CompanyReportConfig.SendNow).
		// options
		GET(ROUTE_V1_OPTION_GET, middleware.SessionHandler, controllers.Option.Get).
		POST(ROUTE_V1_OPTION, middleware.SessionHandler, middleware.SessionHandler, controllers.Option.Update).
		// scim global domain setting
		GET(ROUTE_V1_OPTION_SCIM_DOMAIN, middleware.SessionHandler, controllers.Option.GetScimDomain).
		POST(ROUTE_V1_OPTION_SCIM_DOMAIN, middleware.SessionHandler, controllers.Option.SetScimDomain).
		// scim soft-delete retention window
		GET(ROUTE_V1_OPTION_SCIM_RETENTION, middleware.SessionHandler, controllers.Option.GetScimRetentionDays).
		POST(ROUTE_V1_OPTION_SCIM_RETENTION, middleware.SessionHandler, controllers.Option.SetScimRetentionDays).
		// auto-prune options
		GET(ROUTE_V1_OPTION_AUTO_PRUNE, middleware.SessionHandler, controllers.Option.GetAutoPrune).
		POST(ROUTE_V1_OPTION_AUTO_PRUNE, middleware.SessionHandler, controllers.Option.SetAutoPrune).
		GET(ROUTE_V1_COMPANY_OPTION_AUTO_PRUNE, middleware.SessionHandler, controllers.Option.GetCompanyAutoPrune).
		POST(ROUTE_V1_COMPANY_OPTION_AUTO_PRUNE, middleware.SessionHandler, controllers.Option.SetCompanyAutoPrune).
		// domain
		GET(ROUTE_V1_DOMAIN, middleware.SessionHandler, controllers.Domain.GetAll).
		GET(ROUTE_V1_DOMAIN_SUBSET, middleware.SessionHandler, controllers.Domain.GetAllOverview).
		GET(ROUTE_V1_DOMAIN_SUBSET_NO_PROXIES, middleware.SessionHandler, controllers.Domain.GetAllOverviewWithoutProxies).
		GET(ROUTE_V1_DOMAIN_ID, middleware.SessionHandler, controllers.Domain.GetByID).
		GET(ROUTE_V1_DOMAIN_NAME, middleware.SessionHandler, controllers.Domain.GetByName).
		POST(ROUTE_V1_DOMAIN, middleware.SessionHandler, controllers.Domain.Create).
		POST(ROUTE_V1_DOMAIN_ID, middleware.SessionHandler, controllers.Domain.UpdateByID).
		DELETE(ROUTE_V1_DOMAIN_ID, middleware.SessionHandler, controllers.Domain.DeleteByID).
		// recipient
		GET(ROUTE_V1_RECIPIENT, middleware.SessionHandler, controllers.Recipient.GetAll).
		GET(ROUTE_V1_RECIPIENT_ID, middleware.SessionHandler, controllers.Recipient.GetByID).
		GET(ROUTE_V1_RECIPIENT_ID_EVENTS, middleware.SessionHandler, controllers.Recipient.GetCampaignEvents).
		GET(ROUTE_V1_RECIPIENT_ID_STATS, middleware.SessionHandler, controllers.Recipient.GetStatsByID).
		POST(ROUTE_V1_RECIPIENT, middleware.SessionHandler, controllers.Recipient.Create).
		POST(ROUTE_V1_RECIPIENT_IMPORT, middleware.SessionHandler, controllers.Recipient.Import).
		GET(ROUTE_V1_RECIPIENT_EXPORT, middleware.SessionHandler, controllers.Recipient.Export).
		PATCH(ROUTE_V1_RECIPIENT_ID, middleware.SessionHandler, controllers.Recipient.UpdateByID).
		DELETE(ROUTE_V1_RECIPIENT_ID, middleware.SessionHandler, controllers.Recipient.DeleteByID).
		GET(ROUTE_V1_RECIPIENT_REPEAT_OFFENDERS, middleware.SessionHandler, controllers.Recipient.GetRepeatOffenderCount).
		GET(ROUTE_V1_RECIPIENT_ORPHANED, middleware.SessionHandler, controllers.Recipient.GetOrphaned).
		DELETE(ROUTE_V1_RECIPIENT_ORPHANED_DELETE, middleware.SessionHandler, controllers.Recipient.DeleteAllOrphaned).
		// recipient group
		GET(ROUTE_V1_RECIPIENT_GROUP, middleware.SessionHandler, controllers.RecipientGroup.GetAll).
		GET(ROUTE_V1_RECIPIENT_GROUP_ID, middleware.SessionHandler, controllers.RecipientGroup.GetByID).
		GET(ROUTE_V1_RECIPIENT_GROUP_RECIPIENTS, middleware.SessionHandler, controllers.RecipientGroup.GetRecipientsByGroupID).
		POST(ROUTE_V1_RECIPIENT_GROUP_RECIPIENTS, middleware.SessionHandler, controllers.RecipientGroup.AddRecipients).
		DELETE(ROUTE_V1_RECIPIENT_GROUP_RECIPIENTS, middleware.SessionHandler, controllers.RecipientGroup.RemoveRecipients).
		POST(ROUTE_V1_RECIPIENT_GROUP, middleware.SessionHandler, controllers.RecipientGroup.Create).
		PATCH(ROUTE_V1_RECIPIENT_GROUP_ID, middleware.SessionHandler, controllers.RecipientGroup.UpdateByID).
		PUT(ROUTE_V1_RECIPIENT_GROUP_ID_IMPORT, middleware.SessionHandler, controllers.RecipientGroup.Import).
		DELETE(ROUTE_V1_RECIPIENT_GROUP_ID, middleware.SessionHandler, controllers.RecipientGroup.DeleteByID).
		// page
		GET(ROUTE_V1_PAGE, middleware.SessionHandler, controllers.Page.GetAll).
		GET(ROUTE_V1_PAGE_OVERVIEW, middleware.SessionHandler, controllers.Page.GetOverview).
		GET(ROUTE_V1_PAGE_ID, middleware.SessionHandler, controllers.Page.GetByID).
		Any(ROUTE_V1_PAGE_CONTENT_ID, middleware.SessionHandler, controllers.Page.GetContentByID).
		POST(ROUTE_V1_PAGE, middleware.SessionHandler, controllers.Page.Create).
		PATCH(ROUTE_V1_PAGE_ID, middleware.SessionHandler, controllers.Page.UpdateByID).
		DELETE(ROUTE_V1_PAGE_ID, middleware.SessionHandler, controllers.Page.DeleteByID).
		// proxy
		GET(ROUTE_V1_PROXY, middleware.SessionHandler, controllers.Proxy.GetAll).
		GET(ROUTE_V1_PROXY_OVERVIEW, middleware.SessionHandler, controllers.Proxy.GetOverview).
		GET(ROUTE_V1_PROXY_ID, middleware.SessionHandler, controllers.Proxy.GetByID).
		POST(ROUTE_V1_PROXY, middleware.SessionHandler, controllers.Proxy.Create).
		PATCH(ROUTE_V1_PROXY_ID, middleware.SessionHandler, controllers.Proxy.UpdateByID).
		DELETE(ROUTE_V1_PROXY_ID, middleware.SessionHandler, controllers.Proxy.DeleteByID).
		// ip allow list
		GET(ROUTE_V1_IP_ALLOW_LIST_PROXY_CONFIG, middleware.SessionHandler, controllers.IPAllowList.GetEntriesForProxyConfig).
		DELETE(ROUTE_V1_IP_ALLOW_LIST_CLEAR_PROXY_CONFIG, middleware.SessionHandler, controllers.IPAllowList.ClearForProxyConfig).
		// smtp configuration
		GET(ROUTE_V1_SMTP_CONFIGURATION, middleware.SessionHandler, controllers.SMTPConfiguration.GetAll).
		GET(ROUTE_V1_SMTP_CONFIGURATION_ID, middleware.SessionHandler, controllers.SMTPConfiguration.GetByID).
		POST(ROUTE_V1_SMTP_CONFIGURATION, middleware.SessionHandler, controllers.SMTPConfiguration.Create).
		POST(ROUTE_V1_SMTP_CONFIGURATION_ID_TEST_EMAIL, middleware.SessionHandler, controllers.SMTPConfiguration.TestEmail).
		PATCH(ROUTE_V1_SMTP_CONFIGURATION_ID, middleware.SessionHandler, controllers.SMTPConfiguration.UpdateByID).
		DELETE(ROUTE_V1_SMTP_CONFIGURATION_ID, middleware.SessionHandler, controllers.SMTPConfiguration.DeleteByID).
		// smtp configuration headers
		PATCH(ROUTE_V1_SMTP_CONFIGURATION_HEADERS, middleware.SessionHandler, controllers.SMTPConfiguration.AddHeader).
		DELETE(ROUTE_V1_SMTP_HEADER_ID, middleware.SessionHandler, controllers.SMTPConfiguration.RemoveHeader).
		// oauth providers
		GET(ROUTE_V1_OAUTH_PROVIDER, middleware.SessionHandler, controllers.OAuthProvider.GetAll).
		GET(ROUTE_V1_OAUTH_PROVIDER_ID, middleware.SessionHandler, controllers.OAuthProvider.GetByID).
		POST(ROUTE_V1_OAUTH_PROVIDER, middleware.SessionHandler, controllers.OAuthProvider.Create).
		PATCH(ROUTE_V1_OAUTH_PROVIDER_ID, middleware.SessionHandler, controllers.OAuthProvider.UpdateByID).
		DELETE(ROUTE_V1_OAUTH_PROVIDER_ID, middleware.SessionHandler, controllers.OAuthProvider.DeleteByID).
		POST(ROUTE_V1_OAUTH_PROVIDER_REMOVE_AUTH, middleware.SessionHandler, controllers.OAuthProvider.RemoveAuthorization).
		GET(ROUTE_V1_OAUTH_AUTHORIZE, middleware.SessionHandler, controllers.OAuthProvider.GetAuthorizationURL).
		GET(ROUTE_V1_OAUTH_CALLBACK, controllers.OAuthProvider.HandleCallback).
		POST(ROUTE_V1_OAUTH_IMPORT_TOKENS, middleware.SessionHandler, controllers.OAuthProvider.ImportAuthorizedTokens).
		GET(ROUTE_V1_OAUTH_EXPORT_TOKENS, middleware.SessionHandler, controllers.OAuthProvider.ExportAuthorizedTokens).
		// emails
		GET(ROUTE_V1_EMAIL, middleware.SessionHandler, controllers.Email.GetAll).
		GET(ROUTE_V1_EMAIL_OVERVIEW, middleware.SessionHandler, controllers.Email.GetOverviews).
		GET(ROUTE_V1_EMAIL_ID, middleware.SessionHandler, controllers.Email.GetByID).
		GET(ROUTE_V1_EMAIL_CONTENT_ID, middleware.SessionHandler, controllers.Email.GetContentByID).
		POST(ROUTE_V1_EMAIL_SEND_TEST, middleware.SessionHandler, controllers.Email.SendTestEmail).
		POST(ROUTE_V1_EMAIL, middleware.SessionHandler, controllers.Email.Create).
		// TODO PATCH
		POST(ROUTE_V1_EMAIL_ID, middleware.SessionHandler, controllers.Email.UpdateByID).
		DELETE(ROUTE_V1_EMAIL_ID, middleware.SessionHandler, controllers.Email.DeleteByID).
		// email attachments
		POST(ROUTE_V1_EMAIL_ATTACHMENT, middleware.SessionHandler, controllers.Email.AddAttachments).
		DELETE(ROUTE_V1_EMAIL_ATTACHMENT, middleware.SessionHandler, controllers.Email.RemoveAttachment).
		// campaign templates
		GET(ROUTE_V1_CAMPAIGN_TEMPLATE, middleware.SessionHandler, controllers.CampaignTemplate.GetAll).
		GET(ROUTE_v1_CAMPAIGN_TEMPLATE_ID, middleware.SessionHandler, controllers.CampaignTemplate.GetByID).
		// TODO PATCH
		POST(ROUTE_V1_CAMPAIGN_TEMPLATE, middleware.SessionHandler, controllers.CampaignTemplate.Create).
		POST(ROUTE_v1_CAMPAIGN_TEMPLATE_ID, middleware.SessionHandler, controllers.CampaignTemplate.UpdateByID).
		DELETE(ROUTE_v1_CAMPAIGN_TEMPLATE_ID, middleware.SessionHandler, controllers.CampaignTemplate.DeleteByID).
		// campaigns
		GET(ROUTE_V1_CAMPAIGN, middleware.SessionHandler, controllers.Campaign.GetAll).
		GET(ROUTE_V1_CAMPAIGN_CALENDAR, middleware.SessionHandler, controllers.Campaign.GetAllWithinDates).
		GET(ROUTE_V1_CAMPAIGN_ACTIVE, middleware.SessionHandler, controllers.Campaign.GetAllActive).
		GET(ROUTE_V1_CAMPAIGN_UPCOMING, middleware.SessionHandler, controllers.Campaign.GetAllUpcoming).
		GET(ROUTE_V1_CAMPAIGN_FINISHED, middleware.SessionHandler, controllers.Campaign.GetAllFinished).
		GET(ROUTE_V1_CAMPAIGN_EVENT_NAMES, middleware.SessionHandler, controllers.Campaign.GetAllEventTypes).
		GET(ROUTE_V1_CAMPAIGN_ALL_EVENTS, middleware.SessionHandler, controllers.Campaign.GetAllEvents).
		GET(ROUTE_V1_CAMPAIGN_EVENTS, middleware.SessionHandler, controllers.Campaign.GetEventsByCampaignID).
		DELETE(ROUTE_V1_CAMPAIGN_EVENT_ID, middleware.SessionHandler, controllers.Campaign.DeleteEventByID).
		GET(ROUTE_V1_CAMPAIGN_STATS, middleware.SessionHandler, controllers.Campaign.GetStats).
		GET(ROUTE_V1_CAMPAIGN_RESULT_STATS, middleware.SessionHandler, controllers.Campaign.GetResultStats).
		GET(ROUTE_V1_CAMPAIGN_STATS_ID, middleware.SessionHandler, controllers.Campaign.GetCampaignStats).
		GET(ROUTE_V1_CAMPAIGN_STATS_ALL, middleware.SessionHandler, controllers.Campaign.GetAllCampaignStats).
		POST(ROUTE_V1_CAMPAIGN_STATS_CREATE, middleware.SessionHandler, controllers.Campaign.CreateCampaignStats).
		GET(ROUTE_V1_CAMPAIGN_STATS_MANUAL, middleware.SessionHandler, controllers.Campaign.GetManualCampaignStats).
		PUT(ROUTE_V1_CAMPAIGN_STATS_UPDATE, middleware.SessionHandler, controllers.Campaign.UpdateCampaignStats).
		DELETE(ROUTE_V1_CAMPAIGN_STATS_DELETE, middleware.SessionHandler, controllers.Campaign.DeleteCampaignStatsManual).
		GET(ROUTE_V1_CAMPAIGN_ID, middleware.SessionHandler, controllers.Campaign.GetByID).
		GET(ROUTE_V1_CAMPAIGN_NAME, middleware.SessionHandler, controllers.Campaign.GetByName).
		POST(ROUTE_V1_CAMPAIGN, middleware.SessionHandler, controllers.Campaign.Create).
		// TODO PATCH
		POST(ROUTE_V1_CAMPAIGN_ID, middleware.SessionHandler, controllers.Campaign.UpdateByID).
		POST(ROUTE_V1_CAMPAIGN_CLOSE, middleware.SessionHandler, controllers.Campaign.CloseCampaignByID).
		GET(ROUTE_V1_CAMPAIGN_EXPORT_EVENTS, middleware.SessionHandler, controllers.Campaign.ExportEventsAsCSV).
		GET(ROUTE_V1_CAMPAIGN_EXPORT_SUBMISSIONS, middleware.SessionHandler, controllers.Campaign.ExportSubmissionsAsCSV).
		POST(ROUTE_V1_CAMPAIGN_UPLOAD_REPORTED, middleware.SessionHandler, controllers.Campaign.UploadReportedCSV).
		POST(ROUTE_V1_CAMPAIGN_ANONYMIZE, middleware.SessionHandler, controllers.Campaign.AnonymizeByID).
		POST(ROUTE_V1_CAMPAIGN_ANONYMIZE_DATA, middleware.SessionHandler, controllers.Campaign.AnonymizeDataByID).
		DELETE(ROUTE_V1_CAMPAIGN_DEVICE_CODES, middleware.SessionHandler, controllers.Campaign.DeleteDeviceCodesByCampaignID).
		DELETE(ROUTE_V1_CAMPAIGN_ID, middleware.SessionHandler, controllers.Campaign.DeleteByID).
		// campaign PDF report — ExtendedTimeout required for headless browser rendering
		GET(ROUTE_V1_CAMPAIGN_REPORT, middleware.ExtendedTimeout(3*time.Minute), middleware.SessionHandler, controllers.ReportTemplate.GeneratePDFByCampaignID).
		// report templates
		GET(ROUTE_V1_REPORT_TEMPLATE, middleware.SessionHandler, controllers.ReportTemplate.GetAll).
		POST(ROUTE_V1_REPORT_TEMPLATE, middleware.SessionHandler, controllers.ReportTemplate.Create).
		GET(ROUTE_V1_REPORT_TEMPLATE_ID, middleware.SessionHandler, controllers.ReportTemplate.GetByID).
		PATCH(ROUTE_V1_REPORT_TEMPLATE_ID, middleware.SessionHandler, controllers.ReportTemplate.UpdateByID).
		DELETE(ROUTE_V1_REPORT_TEMPLATE_ID, middleware.SessionHandler, controllers.ReportTemplate.DeleteByID).
		DELETE(ROUTE_V1_REPORT_PDF_BROWSER_CACHE, middleware.SessionHandler, controllers.ReportTemplate.WipeBrowserCache).
		// campaign-recipient
		GET(ROUTE_V1_CAMPAIGN_RECIPIENTS, middleware.SessionHandler, controllers.Campaign.GetRecipientsByCampaignID).
		GET(ROUTE_V1_CAMPAIGN_RECIPIENT_EMAIL, middleware.SessionHandler, controllers.Campaign.GetCampaignEmail).
		GET(ROUTE_V1_CAMPAIGN_RECIPIENT_URL, middleware.SessionHandler, controllers.Campaign.GetCampaignURL).
		POST(ROUTE_V1_CAMPAIGN_RECIPIENT_SET_SENT, middleware.SessionHandler, controllers.Campaign.SetSentAtByCampaignRecipientID).
		PUT(ROUTE_V1_CAMPAIGN_RECIPIENT_LURE_CODE, middleware.SessionHandler, controllers.Campaign.SetLureCode).
		POST(ROUTE_V1_CAMPAIGN_RECIPIENT_SEND_EMAIL, middleware.SessionHandler, controllers.Campaign.SendEmailByCampaignRecipientID).
		// asset
		GET(ROUTE_V1_ASSET_DOMAIN_VIEW, middleware.SessionHandler, controllers.Asset.GetContentByID).
		GET(ROUTE_V1_ASSET_ID, middleware.SessionHandler, controllers.Asset.GetByID).
		PATCH(ROUTE_V1_ASSET_ID, middleware.SessionHandler, controllers.Asset.UpdateByID).
		GET(ROUTE_V1_ASSET_DOMAIN_CONTEXT, middleware.SessionHandler, controllers.Asset.GetAllForContext).
		GET(ROUTE_V1_ASSET_GLOBAL_CONTEXT, middleware.SessionHandler, controllers.Asset.GetAllForContext).
		POST(ROUTE_V1_ASSET, middleware.SessionHandler, controllers.Asset.Create).
		DELETE(ROUTE_V1_ASSET_ID, middleware.SessionHandler, controllers.Asset.RemoveByID).
		// attachments
		POST(ROUTE_V1_ATTACHMENT, middleware.SessionHandler, controllers.Attachment.Create).
		GET(ROUTE_V1_ATTACHMENT_ID, middleware.SessionHandler, controllers.Attachment.GetByID).
		GET(ROUTE_V1_ATTACHMENT_ID_CONTENT, middleware.SessionHandler, controllers.Attachment.GetContentByID).
		GET(ROUTE_V1_ATTACHMENT, middleware.SessionHandler, controllers.Attachment.GetAllForContext).
		PATCH(ROUTE_V1_ATTACHMENT_ID, middleware.SessionHandler, controllers.Attachment.UpdateByID).
		DELETE(ROUTE_V1_ATTACHMENT_ID, middleware.SessionHandler, controllers.Attachment.RemoveByID).
		// api sender
		GET(ROUTE_V1_API_SENDER, middleware.SessionHandler, controllers.APISender.GetAll).
		GET(ROUTE_V1_API_SENDER_OVERVIEW, middleware.SessionHandler, controllers.APISender.GetAllOverview).
		GET(ROUTE_V1_API_SENDER_ID, middleware.SessionHandler, controllers.APISender.GetByID).
		POST(ROUTE_V1_API_SENDER, middleware.SessionHandler, controllers.APISender.Create).
		PATCH(ROUTE_V1_API_SENDER_ID, middleware.SessionHandler, controllers.APISender.UpdateByID).
		POST(ROUTE_V1_API_SENDER_ID_TEST, middleware.SessionHandler, controllers.APISender.SendTest).
		DELETE(ROUTE_V1_API_SENDER_ID, middleware.SessionHandler, controllers.APISender.DeleteByID).
		// allow deny
		GET(ROUTE_V1_ALLOW_DENY, middleware.SessionHandler, controllers.AllowDeny.GetAll).
		GET(ROUTE_V1_ALLOW_DENY_OVERVIEW, middleware.SessionHandler, controllers.AllowDeny.GetAllOverview).
		GET(ROUTE_V1_ALLOW_DENY_ID, middleware.SessionHandler, controllers.AllowDeny.GetByID).
		POST(ROUTE_V1_ALLOW_DENY, middleware.SessionHandler, controllers.AllowDeny.Create).
		PATCH(ROUTE_V1_ALLOW_DENY_ID, middleware.SessionHandler, controllers.AllowDeny.UpdateByID).
		DELETE(ROUTE_V1_ALLOW_DENY_ID, middleware.SessionHandler, controllers.AllowDeny.DeleteByID).
		// geoip
		GET(ROUTE_V1_GEOIP_METADATA, middleware.SessionHandler, controllers.GeoIP.GetMetadata).
		GET(ROUTE_V1_GEOIP_LOOKUP, middleware.SessionHandler, controllers.GeoIP.Lookup).
		// web hooks
		GET(ROUTE_V1_WEBHOOK, middleware.SessionHandler, controllers.Webhook.GetAll).
		GET(ROUTE_V1_WEBHOOK_ID, middleware.SessionHandler, controllers.Webhook.GetByID).
		POST(ROUTE_V1_WEBHOOK, middleware.SessionHandler, controllers.Webhook.Create).
		PATCH(ROUTE_V1_WEBHOOK_ID, middleware.SessionHandler, controllers.Webhook.UpdateByID).
		DELETE(ROUTE_V1_WEBHOOK_ID, middleware.SessionHandler, controllers.Webhook.DeleteByID).
		POST(ROUTE_V1_WEBHOOK_ID_TEST, middleware.SessionHandler, controllers.Webhook.SendTest).
		// identifiers
		GET(ROUTE_V1_IDENTIFIER, middleware.SessionHandler, controllers.Identifier.GetAll).
		// version
		GET(ROUTE_V1_VERSION, middleware.SessionHandler, controllers.Version.Get).
		// update
		GET(ROUTE_V1_UPDATE, middleware.SessionHandler, controllers.Update.GetUpdateDetails).
		POST(ROUTE_V1_UPDATE, middleware.ExtendedTimeout(3*time.Minute), middleware.SessionHandler, controllers.Update.RunUpdate).
		// backup
		POST(ROUTE_V1_BACKUP_CREATE, middleware.SessionHandler, controllers.Backup.CreateBackup).
		GET(ROUTE_V1_BACKUP_LIST, middleware.SessionHandler, controllers.Backup.ListBackups).
		GET(ROUTE_V1_BACKUP_DOWNLOAD, middleware.SessionHandler, controllers.Backup.DownloadBackup).
		// import
		POST(ROUTE_V1_IMPORT, middleware.SessionHandler, controllers.Import.Import).
		// remote browser
		GET(ROUTE_V1_REMOTE_BROWSER, middleware.SessionHandler, controllers.RemoteBrowser.GetAll).
		GET(ROUTE_V1_REMOTE_BROWSER_OVERVIEW, middleware.SessionHandler, controllers.RemoteBrowser.GetOverview).
		GET(ROUTE_V1_REMOTE_BROWSER_ID, middleware.SessionHandler, controllers.RemoteBrowser.GetByID).
		POST(ROUTE_V1_REMOTE_BROWSER, middleware.SessionHandler, controllers.RemoteBrowser.Create).
		PATCH(ROUTE_V1_REMOTE_BROWSER_ID, middleware.SessionHandler, controllers.RemoteBrowser.UpdateByID).
		DELETE(ROUTE_V1_REMOTE_BROWSER_ID, middleware.SessionHandler, controllers.RemoteBrowser.DeleteByID).
		GET(ROUTE_V1_REMOTE_BROWSER_ID_RUN, middleware.SessionHandler, controllers.RemoteBrowser.RunByID).
		GET(ROUTE_V1_REMOTE_BROWSER_LIVE, middleware.SessionHandler, controllers.RemoteBrowser.ListLiveSessions).
		DELETE(ROUTE_V1_REMOTE_BROWSER_LIVE_CRID, middleware.SessionHandler, controllers.RemoteBrowser.CloseLiveSession).
		GET(ROUTE_V1_REMOTE_BROWSER_LIVE_STREAM, middleware.SessionHandler, controllers.RemoteBrowser.StreamLiveSession)

	return r
}

func (a *administrationServer) handleTLSCertificate(
	conf *config.Config,
) error {
	publicCertExists := true
	privateCertExists := true
	if _, err := os.Stat(conf.TLSCertPath()); err != nil {
		if !os.IsNotExist(err) {
			return err
		}
		privateCertExists = false
	}
	if _, err := os.Stat(conf.TLSKeyPath()); err != nil {
		if !os.IsNotExist(err) {
			return err
		}
		publicCertExists = false
	}

	// determine hostnames to include in the certificate
	hostnames := []string{}
	if h := conf.TLSHost(); len(h) > 0 {
		hostnames = append(hostnames, h)
	}
	// get the address from config
	if conf.AdminNetAddress() != "" {
		host, _, err := net.SplitHostPort(conf.AdminNetAddress())
		if err == nil && host != "" && host != "0.0.0.0" && host != "::" {
			hostnames = append(hostnames, host)
		}
	}

	// try to get all non-loopback IP addresses
	addrs, err := net.InterfaceAddrs()
	if err == nil {
		for _, addr := range addrs {
			if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
				ip := ipnet.IP

				// skip private IPs (RFC 1918)
				if ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
					continue
				}

				// only add public IPs to the certificate
				hostnames = append(hostnames, ip.String())
			}
		}
	}

	needToCreateCert := !privateCertExists || !publicCertExists

	// check if we need to recreate the certificate because host/IP has changed
	if privateCertExists && publicCertExists {
		// read the existing certificate to check the hostnames
		certData, err := os.ReadFile(conf.TLSCertPath())
		if err == nil {
			block, _ := pem.Decode(certData)
			if block != nil && block.Type == "CERTIFICATE" {
				cert, err := x509.ParseCertificate(block.Bytes)
				if err == nil {
					// vheck if all requested hostnames are in the certificate
					missingHosts := false
					hostMap := make(map[string]bool)

					// add all current certificate SANs to the map
					for _, dnsName := range cert.DNSNames {
						hostMap[dnsName] = true
					}
					for _, ip := range cert.IPAddresses {
						hostMap[ip.String()] = true
					}

					// check if the common name is in our hostnames
					if cert.Subject.CommonName != "" {
						hostMap[cert.Subject.CommonName] = true
					}

					// Check if all requested hostnames are covered
					for _, host := range hostnames {
						if !hostMap[host] {
							missingHosts = true
							a.logger.Debugw("host not found in existing certificate", "host", host)
							break
						}
					}

					// if the TLSHost is specified and not in the certificate, or other hosts are missing, regenerate
					if missingHosts {
						a.logger.Debug("recreating certificate due to changed host/IP configuration")
						needToCreateCert = true
					}
				} else {
					a.logger.Warnw("could not parse existing certificate, will recreate", "error", err)
					needToCreateCert = true
				}
			} else {
				a.logger.Warn("invalid certificate format, will recreate")
				needToCreateCert = true
			}
		} else {
			a.logger.Warnw("could not read existing certificate, will recreate", "error", err)
			needToCreateCert = true
		}
	}

	// create certificates if needed
	if needToCreateCert {
		a.logger.Debug("creating self signed certificate for administration server")

		info := acme.NewInformationWithDefault()
		if len(hostnames) > 0 {
			info.CommonName = hostnames[0]
		}

		a.logger.Debugw("generating certificate with hostnames", "hostnames", hostnames)

		err = acme.CreateSelfSignedCert(
			a.logger,
			info,
			hostnames,
			conf.TLSCertPath(),
			conf.TLSKeyPath(),
		)

		if err != nil {
			return fmt.Errorf("failed to create self signed certificate: %s", err)
		}

		a.logger.Debugw(
			"saved self signed certificate for administration servers",
			"TLS certificate", conf.TLSCertPath(),
			"TLS key path", conf.TLSKeyPath(),
		)
	} else {
		a.logger.Debug("using existing certificate for administration server")
	}

	return nil
}

// LoadFrontend loads the frontend
// if this is a production build, the fronten will be embedded
// else the routes will be setup to load the frontend resources on every request
func (a *administrationServer) LoadFrontend(
	ln net.Listener,
) error {
	if build.Flags.Production {
		return a.loadEmbeddedFileSystem(
			ln,
		)
	}
	return a.loadPerRequestLoading()
}

// loadPerRequestLoading loads the frontend resources on every request
// this is only used in a dev enviroment using nodemon as is a
// backup if the current vite proxy stragegy does not work.
func (a *administrationServer) loadPerRequestLoading() error {
	a.router.GET("/", func(c *gin.Context) {
		c.File("./frontend/website/build/index.html")
	})
	// perform manual lookup for the frontend files on each request
	// build files might have been added or removed, so each request must
	// do a check if the file exists
	a.router.NoRoute(func(c *gin.Context) {
		// a.logger.Infow("serving frontend file", "path", c.Request.URL.Path)
		// check if the request url path exists in the root directory
		if _, err := os.Stat("./frontend/website/build" + c.Request.URL.Path); err == nil {
			c.File("./frontend/website/build" + c.Request.URL.Path)
			return
		}
		// if the path ends with / or does not have a file extension, then it should fallback to index.html as
		// it is a SPA path such as /company/foo/
		if c.Request.URL.Path[len(c.Request.URL.Path)-1:] == "/" || !strings.Contains(c.Request.URL.Path, ".") {
			c.File("./frontend/website/build/index.html")
		}
		// file not found - return 404
		c.AbortWithStatus(http.StatusNotFound)
	})
	return nil
}

func (a *administrationServer) loadEmbeddedFileSystem(
	ln net.Listener,
) error {
	_ = ln
	embedFS := frontend.GetEmbededFS()
	// make embedded .html work
	frontend.LoadHTMLFromEmbedFS(a.router, *embedFS, "build/*.html")
	// serve favicons only to authenticated users — unauthenticated requests get 404
	// so the file is not indexable by scanners probing common paths
	for _, faviconPath := range []string{"/favicon.ico", "/favicon.png"} {
		fp := faviconPath
		a.router.GET(fp, a.softSessionHandler, func(c *gin.Context) {
			name := fp[1:] // strip leading /
			c.FileFromFS("build/"+name, http.FS(*embedFS))
		})
	}
	rootDir, err := embedFS.ReadDir("build")
	if err != nil {
		return errs.Wrap(err)
	}
	for _, entry := range rootDir {
		path := entry.Name()
		// add root files
		if !entry.IsDir() {
			// special case for the frontpage
			if path == "index.html" {
				a.router.GET("/", func(c *gin.Context) {
					c.HTML(http.StatusOK, "build/index.html", nil)
				})
				continue
			}
			// skip favicons — registered separately with session gating
			if path == "favicon.png" || path == "favicon.ico" {
				continue
			}
			// any file in the root folder gets server as a file
			a.router.GET("/"+path, func(c *gin.Context) {
				c.FileFromFS("build/"+path, http.FS(*embedFS))
			})
			continue
		}
		// add static folders
		staticFS, err := fs.Sub(embedFS, "build/"+path)
		if err != nil {
			return errs.Wrap(err)
		}
		switch path {
		case ".well-known":
			fallthrough
		case "_app":
			a.router.StaticFS(path, http.FS(staticFS))
		}
	}
	// fall back to the root index.html
	a.router.NoRoute(func(c *gin.Context) {
		c.HTML(http.StatusOK, "build/index.html", nil)
	})

	return nil
}

func (a *administrationServer) StartServer(
	conf *config.Config,
) (chan server.StartupMessage, net.Listener, error) {
	startupMessage := server.NewStartupMessageChannel()
	ln, err := net.Listen("tcp", conf.AdminNetAddress())
	if err != nil {
		return nil, nil, fmt.Errorf("failed to listen on %s due to: %s", conf.AdminNetAddress(), err)
	}
	err = a.LoadFrontend(ln)
	if err != nil {
		return nil, nil, errs.Wrap(err)
	}
	err = a.handleTLSCertificate(conf)
	if err != nil {
		return nil, nil, errs.Wrap(err)
	}

	a.Server = &http.Server{
		Handler: a.router,
		// The maximum duration for reading the entire request, including the request line, headers, and body
		ReadTimeout: 15 * time.Second,
		// The maximum duration for writing the entire response, including the response headers and body
		WriteTimeout: 15 * time.Second, // Timeout for writing the response
		// The maximum duration to wait for the next request when the connection is in the idle state
		IdleTimeout: 10 * time.Second,
		// The maximum duration for reading the request headers.
		ReadHeaderTimeout: 2 * time.Second,
		// Maximum size of request headers (512 KB)
		MaxHeaderBytes: 1 << 19,
	}
	a.Server.ErrorLog = log.New(
		&SkipFirstTlsToZapWriter{
			logger:    a.logger,
			serverPtr: a.Server,
		}, "", 0,
	)

	a.logger.Debugw("TLS settings",
		"certPath", conf.TLSCertPath(),
		"certKeyPath", conf.TLSKeyPath(),
	)

	// start the administration server
	adminHost := "admin.test"
	err = a.certMagicConfig.ManageSync(context.Background(), []string{adminHost})
	if err != nil {
		a.logger.Errorw("certmagic managesync failed", "error", err)
		return nil, nil, errs.Wrap(err)
	}
	go func() {
		if !conf.TLSAuto() {
			a.logger.Debugw("starting administration",
				"address", ln.Addr().String(),
			)
			err := a.Server.ServeTLS(
				ln,
				conf.TLSCertPath(),
				conf.TLSKeyPath(),
			)
			if err != nil && err != http.ErrServerClosed {
				log.Fatalf("failed to start administration server due to: %s", err)
			}
		} else {
			// Setup TLS config from CertMagic
			tlsConfig := a.certMagicConfig.TLSConfig()
			tlsConfig.NextProtos = append([]string{"h2", "http/1.1"}, tlsConfig.NextProtos...)

			// Create new TLS listener with the config
			tlsLn := tls.NewListener(ln, tlsConfig)
			a.logger.Debugw("starting administration with automatic TLS",
				"address", ln.Addr().String(),
				"domain", adminHost,
			)
			err := a.Server.Serve(tlsLn)
			if err != nil && err != http.ErrServerClosed {
				log.Fatalf("failed to start administration server due to: %s", err)
			}
		}
	}()

	// test the connection to the administration server
	// and send a startup message
	// TODO the connectivity check has been disabled as it fucks up the auto tls
	// as it calls the certmagic DecisionFunc from addreses such as ::1 and I am not
	// sure we it is safe to allow list all of them or if I know all of the potential addresses.
	/*
		go func() {
			a.logger.Debug("testing connectivity to administration server...")
			// wait for connection to the server
			attempts := 1
			for {
				dialer := &net.Dialer{
					Timeout:   time.Second,
					KeepAlive: time.Second,
				}
				conn, err := tls.DialWithDialer(
					dialer,
					"tcp",
					ln.Addr().String(),
					&tls.Config{
						InsecureSkipVerify: true,
					},
				)
				if err != nil {
					a.logger.Debugw("failed to connect to administration server",
						"attempt", attempts,
					)
					time.Sleep(1 * time.Second)
					if attempts == 3 {
						startupMessage <- server.NewStartupMessage(
							false,
							fmt.Errorf("failed to connect to administration server"),
						)
						break
					}
					attempts += 1
					continue
				}
				conn.Close()
				startupMessage <- server.NewStartupMessage(true, nil)
				break
			}
		}()
	*/
	startupMessage <- server.NewStartupMessage(true, nil)

	return startupMessage, ln, nil
}

// https://stackoverflow.com/questions/52294334/net-http-set-custom-logger
type fwdToZapWriter struct {
	logger *zap.SugaredLogger
}

func (fw *fwdToZapWriter) Write(p []byte) (n int, err error) {
	fw.logger.Errorw(string(p))
	return len(p), nil
}

// SkipFirstTlsToZapWriter is a weird Writer that replaces itself
// when it has seen a TLS handshake error it is used for handling
// a special annoying case where a health check on startup creates
// a tls handshake that we want to ignore
type SkipFirstTlsToZapWriter struct {
	logger *zap.SugaredLogger
	// ignore first tls
	serverPtr *http.Server
}

func (fw *SkipFirstTlsToZapWriter) Write(p []byte) (n int, err error) {
	if strings.Contains(string(p), "TLS handshake error") {
		// After catching the first TLS error, replace the ErrorLog with direct logger
		fw.serverPtr.ErrorLog = log.New(
			&fwdToZapWriter{
				logger: fw.logger,
			},
			"",
			0,
		)
		return len(p), nil
	}
	fw.logger.Errorw(string(p))
	return len(p), nil
}
