adminapi

package
v9.2.0 Latest Latest
Warning

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

Go to latest
Published: Jul 15, 2026 License: MIT Imports: 33 Imported by: 0

Documentation

Overview

Package adminapi implements the ToughRADIUS management REST API served under /api/v1. Its handlers back the React Admin frontend and expose CRUD and action endpoints for operators, NAS devices, network nodes, RADIUS profiles and users, online sessions, accounting records, the dashboard, system settings, logs, and backups.

Handlers are plain github.com/labstack/echo/v4 HandlerFuncs registered with the shared web server (see internal/webserver). They read their dependencies — the application context, the GORM database handle, and the configuration manager — from the echo context using GetAppContext, GetDB, and GetConfig, rather than holding injected state, so a handler is a stateless function of its request.

Successful responses use the unified Response envelope (a data object with optional pagination Meta); failures use ErrorResponse with a stable, machine-readable error code. Authorization is enforced per route with the RequireLevel middleware against the operator levels LevelSuper, LevelAdmin, and LevelOperator.

Index

Constants

View Source
const (
	LevelSuper    = "super"
	LevelAdmin    = "admin"
	LevelOperator = "operator"
)

Operator privilege levels, ordered from most to least privileged.

Variables

This section is empty.

Functions

func ChangeOnlineSessionAuthorization added in v9.1.0

func ChangeOnlineSessionAuthorization(c echo.Context) error

ChangeOnlineSessionAuthorization sends a RADIUS CoA-Request (RFC 5176 §2.2) to the session's NAS to change the live session's authorization (for example a new Session-Timeout or Filter-Id) without disconnecting it, returning the structured exchange result. At least one change must be supplied.

As with DisconnectOnlineSession, a NAK or timeout is reported with HTTP 200 and success=false; the structured body carries the Error-Cause and timing.

Authorization: admin/super only (requireAdmin).

@Summary Change a live session's authorization via RADIUS CoA-Request @Tags OnlineSession @Param id path int true "Session ID" @Param body body coaChangePayload true "Authorization changes" @Success 200 {object} Response @Router /api/v1/sessions/{id}/coa [post]

func CreateCertificate added in v9.2.0

func CreateCertificate(c echo.Context) error

CreateCertificate handles POST /api/v1/system/certificate, importing a certificate from the JSON body bound to certPayload. The local name must be unique; a duplicate is rejected 409 with code NAME_EXISTS. The PEM material is parsed to derive the subject, issuer, serial, SHA-256 fingerprint, and validity window; malformed material is rejected 400 with code INVALID_CERT. Server certificates must include a private key matching the certificate (400 codes KEY_REQUIRED / KEY_MISMATCH). On success it returns the persisted domain.SysCert without the private key. This endpoint requires an admin or super operator (see requireAdmin).

@Summary import a certificate @Tags Certificate @Param certificate body certPayload true "Certificate material" @Success 200 {object} domain.SysCert @Router /api/v1/system/certificate [post]

func CreateNAS

func CreateNAS(c echo.Context) error

CreateNAS handles POST /api/v1/network/nas, creating a NAS device from the JSON body bound to nasPayload and validated against its struct tags. The IP address must be unique; a duplicate is rejected 409 with code IPADDR_EXISTS. Unset optional fields default to status "enabled" and CoA port 3799. On success it returns the persisted domain.NetNas. This endpoint requires an admin or super operator (see requireAdmin).

@Summary create a NAS device @Tags NAS @Param nas body nasPayload true "NAS device information" @Success 201 {object} domain.NetNas @Router /api/v1/network/nas [post]

func CreateProfile

func CreateProfile(c echo.Context) error

CreateProfile handles POST /api/v1/radius-profiles, creating a RADIUS profile from the JSON body bound to ProfileRequest and validated against its struct tags. The profile name must be unique; a duplicate is rejected 409 with code NAME_EXISTS. An unset status defaults to "enabled". On success it returns the persisted domain.RadiusProfile. This endpoint requires an admin or super operator (see requireAdmin).

@Summary create a RADIUS profile @Tags RadiusProfile @Param profile body ProfileRequest true "Profile information" @Success 201 {object} domain.RadiusProfile @Router /api/v1/radius-profiles [post]

func DeleteCertificate added in v9.2.0

func DeleteCertificate(c echo.Context) error

DeleteCertificate handles DELETE /api/v1/system/certificate/:id, removing the certificate with the given numeric id. It responds 400 with code INVALID_ID for a non-integer path parameter and returns {"data": {"id": id}} on success. This endpoint requires an admin or super operator (see requireAdmin).

@Summary delete a certificate @Tags Certificate @Param id path int true "Certificate ID" @Success 200 {object} map[string]interface{} @Router /api/v1/system/certificate/{id} [delete]

func DeleteNAS

func DeleteNAS(c echo.Context) error

DeleteNAS handles DELETE /api/v1/network/nas/:id, removing the NAS device with the given id. To protect accounting integrity it refuses deletion while the device still has online sessions, responding 409 with code HAS_ONLINE_SESSIONS and the active count in Details. It responds 400 with code INVALID_ID for a non-integer id. This endpoint requires an admin or super operator (see requireAdmin).

@Summary delete a NAS device @Tags NAS @Param id path int true "NAS ID" @Success 200 {object} SuccessResponse @Router /api/v1/network/nas/{id} [delete]

func DeleteOnlineSession

func DeleteOnlineSession(c echo.Context) error

DeleteOnlineSession handles DELETE /api/v1/sessions/:id, forcing the user of the online session with the given id offline. It removes the local session record and then tears the connection down on the NAS by sending an RFC 5176 Disconnect-Request through the shared, audited CoAService client path — the same path used by POST /sessions/:id/disconnect — so the request carries the full NAS and session identity triplet (NAS-IP-Address, Acct-Session-Id, User-Name, …), honors the NAS CoA port (defaulting to 3799 / DefaultCoAPort), brackets IPv6 addresses correctly, and leaves a durable M2.3 audit record. The Disconnect outcome (ACK/NAK/timeout) is logged and audited, not returned in the HTTP body. When the NAS is not found in the database the record is still deleted and only a warning is logged. A non-integer id responds 400 INVALID_ID, an unknown id 404 NOT_FOUND, and a failed delete 500 DELETE_FAILED. On success it returns a confirmation message.

Authorization: admin/super only (requireAdmin), since forcing a subscriber offline disrupts a live user; this matches the disconnect/coa POST actions.

@Summary Force user offline @Tags OnlineSession @Param id path int true "Session ID" @Success 200 {object} SuccessResponse @Router /api/v1/sessions/{id} [delete]

func DeleteProfile

func DeleteProfile(c echo.Context) error

DeleteProfile handles DELETE /api/v1/radius-profiles/:id, removing the profile with the given id. A profile still referenced by one or more users is not deleted; it is rejected 409 with code IN_USE and the offending user_count in the response details. A non-integer id responds 400 INVALID_ID. On success it invalidates the profile cache and returns a confirmation message. This endpoint requires an admin or super operator (see requireAdmin).

@Summary Delete RADIUS Profile @Tags RadiusProfile @Param id path int true "Profile ID" @Success 200 {object} SuccessResponse @Router /api/v1/radius-profiles/{id} [delete]

func DisconnectOnlineSession added in v9.1.0

func DisconnectOnlineSession(c echo.Context) error

DisconnectOnlineSession sends a RADIUS Disconnect-Request (RFC 5176 §2.1) to the NAS hosting the session, forcing the user offline, and returns the structured exchange result (ACK/NAK/timeout, Error-Cause, attempts, RTT).

Unlike DELETE /sessions/:id — which removes the local record and best-effort notifies the NAS asynchronously — this endpoint performs a synchronous, retry-bounded exchange and reports the NAS's actual response, giving the operator immediate confirmation. The local session record is left untouched; the NAS is expected to emit an Accounting-Stop that clears it.

A NAK or timeout is reported with HTTP 200 and success=false in the body (the exchange genuinely ran; its outcome is the payload). Build/transport setup failures return 4xx/5xx.

Authorization: admin/super only (requireAdmin), since it disrupts live users.

@Summary Force a session offline via RADIUS Disconnect-Request @Tags OnlineSession @Param id path int true "Session ID" @Success 200 {object} Response @Router /api/v1/sessions/{id}/disconnect [post]

func ExportCertificate added in v9.2.0

func ExportCertificate(c echo.Context) error

ExportCertificate handles GET /api/v1/system/certificate/:id/export, streaming the certificate as a PEM file attachment named "<name>.pem". By default only the public certificate is exported; include_key=true additionally appends the private key, which is a sensitive operation recorded to the application log together with the requesting operator. A request for the key when none is stored is rejected 400 with code NO_KEY. This endpoint requires an admin or super operator (see requireAdmin).

@Summary export a certificate @Tags Certificate @Param id path int true "Certificate ID" @Param include_key query bool false "Include the private key in the export" @Success 200 {string} string "PEM data" @Router /api/v1/system/certificate/{id}/export [get]

func GetAccounting

func GetAccounting(c echo.Context) error

GetAccounting handles GET /api/v1/accounting/:id and returns one accounting record by numeric id. It responds with INVALID_ID (400) when the path parameter is not an integer and NOT_FOUND (404) when the record does not exist. Any authenticated operator may call this endpoint.

@Summary get accounting record detail @Tags Accounting @Param id path int true "Accounting ID" @Success 200 {object} domain.RadiusAccounting @Router /api/v1/accounting/{id} [get]

func GetAppContext

func GetAppContext(c echo.Context) app.AppContext

GetAppContext returns the application context that request middleware stored on the echo context under the "appCtx" key. It is the entry point handlers use to reach shared services (database, configuration, scheduler).

It panics if no application context is present, which indicates the route was registered without the middleware that injects it — a programming error rather than a runtime condition, so it is surfaced immediately instead of returning a nil context that would fault later.

func GetCertificate added in v9.2.0

func GetCertificate(c echo.Context) error

GetCertificate handles GET /api/v1/system/certificate/:id, returning the single certificate with the given numeric id. It responds 400 with code INVALID_ID when the path parameter is not an integer and 404 with code NOT_FOUND when no such certificate exists. The private key is never serialized. Any authenticated operator may call it.

@Summary get certificate detail @Tags Certificate @Param id path int true "Certificate ID" @Success 200 {object} domain.SysCert @Router /api/v1/system/certificate/{id} [get]

func GetConfig

func GetConfig(c echo.Context) *app.ConfigManager

GetConfig returns the configuration manager for the current request, resolved from the application context. It is the handler-facing accessor for reading and updating dynamic system settings.

func GetDB

func GetDB(c echo.Context) *gorm.DB

GetDB returns the GORM database handle for the current request. It prefers a per-request handle stored under the "db" key (used by tests and request-scoped transactions) and otherwise falls back to the shared connection from the application context, so handlers get a usable *gorm.DB either way.

func GetDashboardStats

func GetDashboardStats(c echo.Context) error

GetDashboardStats handles GET /api/v1/dashboard/stats, returning a single DashboardStats snapshot that the admin dashboard renders in one request. It aggregates real-time counters (total, online, disabled, and expired users, and total profiles), today's volumes (authentication and accounting record counts plus today's upstream/downstream traffic in GB), and three time series — a 7-day daily authentication trend, a 24-hour hourly upload/download traffic series, and the distribution of online users across profiles — together with an IPv6 adoption summary (DashboardIPv6Stats).

"Today" is measured from the local start of day; the trend and traffic series are zero-filled so every day/hour bucket is present even with no data, and traffic is reported in GB (bytes divided by 1024^3). TodayAuthCount is an estimate derived from sessions that started today, not a dedicated authentication counter. A failure in any individual section is logged and that section falls back to zero/empty rather than failing the whole response. Any authenticated operator may call it.

@Summary get dashboard statistics @Tags Dashboard @Accept json @Produce json @Success 200 {object} DashboardStats @Router /api/v1/dashboard/stats [get]

func GetNAS

func GetNAS(c echo.Context) error

GetNAS handles GET /api/v1/network/nas/:id, returning the single NAS device with the given numeric id. It responds 400 with code INVALID_ID when the path parameter is not an integer and 404 with code NOT_FOUND when no such device exists. Any authenticated operator may call it.

@Summary get NAS device detail @Tags NAS @Param id path int true "NAS ID" @Success 200 {object} domain.NetNas @Router /api/v1/network/nas/{id} [get]

func GetOnlineSession

func GetOnlineSession(c echo.Context) error

GetOnlineSession handles GET /api/v1/sessions/:id, returning the single online session (domain.RadiusOnline) with the given id. A non-integer id responds 400 INVALID_ID and an unknown id 404 NOT_FOUND. Any authenticated operator may call it.

@Summary Get online session details @Tags OnlineSession @Param id path int true "Session ID" @Success 200 {object} domain.RadiusOnline @Router /api/v1/sessions/{id} [get]

func GetProfile

func GetProfile(c echo.Context) error

GetProfile handles GET /api/v1/radius-profiles/:id, returning the single RADIUS profile with the given numeric id. It responds 400 with code INVALID_ID when the path parameter is not an integer and 404 with code NOT_FOUND when no such profile exists. Any authenticated operator may call it.

@Summary get RADIUS profile detail @Tags RadiusProfile @Param id path int true "Profile ID" @Success 200 {object} domain.RadiusProfile @Router /api/v1/radius-profiles/{id} [get]

func Init

func Init(appCtx app.AppContext)

Init registers every admin API route group on the shared web server. It wires the handlers in this package to their paths under /api/v1 and must be called once during startup, after the application context is ready and before the web server begins serving. It is not safe for concurrent use and is not meant to be called more than once.

The appCtx parameter is accepted for symmetry with other subsystems' Init functions; handlers resolve the live application context per request from the echo context (see GetAppContext) rather than capturing it here.

func ListAccounting

func ListAccounting(c echo.Context) error

ListAccounting handles GET /api/v1/accounting and returns a paginated accounting-record page in the standard Response envelope. It accepts page and perPage (perPage is clamped to 1..100, default 10), optional sort/order (validated against allowedAcctSortFields to protect ORDER BY from injection), and optional fuzzy filters for username, NAS address, session ID, IPv4/IPv6, MAC, and delegated-prefix fields.

Time-window filters are optional: acct_start_time_gte and acct_start_time_lte accept RFC3339, datetime-local ("2006-01-02T15:04"), or date-only ("2006-01-02") values. Unparseable time values are ignored rather than rejected.

Any authenticated operator may call this endpoint.

@Summary get accounting logs table @Tags Accounting @Param page query int false "Page number" @Param perPage query int false "Items per page" @Param sort query string false "Sort field" @Param order query string false "Sort direction" @Param username query string false "Username" @Param nas_addr query string false "NAS address" @Param acct_session_id query string false "Session ID" @Param framed_ipaddr query string false "User IP address" @Param mac_addr query string false "MAC address" @Param framed_ipv6_address query string false "Framed IPv6 address" @Param framed_ipv6_prefix query string false "Framed IPv6 prefix" @Param delegated_ipv6_prefix query string false "Delegated IPv6 prefix" @Param acct_start_time_gte query string false "Start time from (RFC3339 or datetime-local format)" @Param acct_start_time_lte query string false "Start time to (RFC3339 or datetime-local format)" @Success 200 {object} ListResponse @Router /api/v1/accounting [get]

func ListCertificates added in v9.2.0

func ListCertificates(c echo.Context) error

ListCertificates handles GET /api/v1/system/certificate, returning a paginated list of locally managed certificates. It accepts the page and perPage query parameters (perPage clamped to 1..100, default 10) and optional name (matched case-insensitively) and cert_type filters. The sort and order parameters are validated against allowedCertSortFields to keep the ORDER BY clause injection-safe. The response body is {"data": []domain.SysCert, "total": int64}; the private key is never serialized and HasKey reports its presence. Any authenticated operator may call it.

@Summary get the certificate list @Tags Certificate @Param page query int false "Page number" @Param perPage query int false "Items per page" @Param sort query string false "Sort field" @Param order query string false "Sort direction" @Param name query string false "Certificate name" @Param cert_type query string false "Certificate type (server|ca)" @Success 200 {object} ListResponse @Router /api/v1/system/certificate [get]

func ListNAS

func ListNAS(c echo.Context) error

ListNAS handles GET /api/v1/network/nas, returning a paginated list of NAS devices. It accepts the page and perPage query parameters (perPage is clamped to 1..100, default 10) and optional name, status, and ipaddr filters; name matches case-insensitively and ipaddr matches by prefix. The sort and order parameters are validated against allowedNasSortFields to keep the ORDER BY clause injection-safe. The response body is {"data": []domain.NetNas, "total": int64}. Any authenticated operator may call it.

@Summary get the NAS device list @Tags NAS @Param page query int false "Page number" @Param perPage query int false "Items per page" @Param sort query string false "Sort field" @Param order query string false "Sort direction" @Param name query string false "Device name" @Param status query string false "Device status" @Success 200 {object} ListResponse @Router /api/v1/network/nas [get]

func ListOnlineSessions

func ListOnlineSessions(c echo.Context) error

ListOnlineSessions handles GET /api/v1/sessions, returning a paginated page of the currently online RADIUS sessions (domain.RadiusOnline). It accepts the page and perPage query parameters (perPage is clamped to 1..100, default 10) and orders by acct_start_time DESC unless overridden by sort and order, which are validated against allowedSessionSortFields so the ORDER BY clause cannot be used for SQL injection. Sessions may be filtered by username, framed_ipv6_address, framed_ipv6_prefix, delegated_ipv6_prefix, mac_addr, and acct_session_id (each a substring match whose LIKE wildcards are escaped), by nas_addr and framed_ipaddr (exact match), and by an acct_start_time_gte / acct_start_time_lte window whose bounds accept RFC 3339, HTML datetime-local, "2006-01-02 15:04:05", or date-only strings (an unparseable bound is ignored rather than erroring). The response is the paginated Response envelope. Any authenticated operator may call it.

@Summary List online sessions @Tags OnlineSession @Param page query int false "Page number" @Param perPage query int false "Items per page" @Param sort query string false "Sort field" @Param order query string false "Sort direction" @Param username query string false "Username" @Param nas_addr query string false "NAS addresses" @Param framed_ipaddr query string false "User IP address" @Param framed_ipv6_address query string false "Framed IPv6 address" @Param framed_ipv6_prefix query string false "Framed IPv6 prefix" @Param delegated_ipv6_prefix query string false "Delegated IPv6 prefix" @Param mac_addr query string false "MAC address" @Param acct_session_id query string false "Session ID" @Param acct_start_time_gte query string false "Start time from (RFC3339 or datetime-local)" @Param acct_start_time_lte query string false "Start time to (RFC3339 or datetime-local)" @Success 200 {object} ListResponse @Router /api/v1/sessions [get]

func ListProfiles

func ListProfiles(c echo.Context) error

ListProfiles handles GET /api/v1/radius-profiles, returning a paginated page of RADIUS profiles (the rate, address-pool, and binding plans assigned to users). It accepts the page and perPage query parameters (perPage is clamped to 1..100, default 10) and optional name, status, addr_pool, and domain filters; name, addr_pool, and domain match case-insensitively while status matches exactly. The sort and order parameters are validated against allowedProfileSortFields so the ORDER BY clause cannot be used for SQL injection. The response is the paginated Response envelope: the page of domain.RadiusProfile in Data and the total/page/page-size counters in Meta. Any authenticated operator may call it.

@Summary get the RADIUS profile list @Tags RadiusProfile @Param page query int false "Page number" @Param perPage query int false "Items per page" @Param sort query string false "Sort field" @Param order query string false "Sort direction" @Success 200 {object} ListResponse @Router /api/v1/radius-profiles [get]

func RequireLevel added in v9.1.0

func RequireLevel(levels ...string) echo.MiddlewareFunc

RequireLevel returns middleware that authorizes a request only when the authenticated operator's level is one of the allowed levels.

It must be chained after the JWT middleware so the operator can be resolved from the request context. Requests from operators whose level is not allowed receive HTTP 403; requests without a resolvable operator receive HTTP 401.

func UpdateCertificate added in v9.2.0

func UpdateCertificate(c echo.Context) error

UpdateCertificate handles PUT /api/v1/system/certificate/:id, applying a partial update to an existing certificate from the JSON body bound to certUpdatePayload. Empty cert and private_key fields leave the stored material untouched so the local name and remark can be edited without re-uploading PEM. When new certificate material is supplied it is re-parsed for metadata, and a server certificate's key (new or existing) must match the certificate. A changed name must remain unique. It responds 404 with code NOT_FOUND when the certificate does not exist and returns the updated domain.SysCert on success. This endpoint requires an admin or super operator (see requireAdmin).

@Summary update a certificate @Tags Certificate @Param id path int true "Certificate ID" @Param certificate body certUpdatePayload true "Certificate fields" @Success 200 {object} domain.SysCert @Router /api/v1/system/certificate/{id} [put]

func UpdateNAS

func UpdateNAS(c echo.Context) error

UpdateNAS handles PUT /api/v1/network/nas/:id, applying a partial update to an existing NAS device from the JSON body bound to nasUpdatePayload. Only non-empty fields are written, so omitted fields keep their stored values. A changed IP address must remain unique across other devices; a collision is rejected 409 with code IPADDR_EXISTS. It responds 404 with code NOT_FOUND when the device does not exist and returns the updated domain.NetNas on success. This endpoint requires an admin or super operator (see requireAdmin).

@Summary update a NAS device @Tags NAS @Param id path int true "NAS ID" @Param nas body nasPayload true "NAS device information" @Success 200 {object} domain.NetNas @Router /api/v1/network/nas/{id} [put]

func UpdateProfile

func UpdateProfile(c echo.Context) error

UpdateProfile handles PUT /api/v1/radius-profiles/:id, applying a partial update to the profile with the given id from the JSON body bound to ProfileUpdateRequest. Only the supplied (non-empty, non-negative) fields are written, so omitted fields keep their stored value. A non-integer id responds 400 INVALID_ID and an unknown id 404 NOT_FOUND; changing the name to one already used by another profile is rejected 409 with code NAME_EXISTS. After a successful update it invalidates the profile cache so dynamic users pick up the change, then returns the refreshed domain.RadiusProfile. This endpoint requires an admin or super operator (see requireAdmin).

@Summary update a RADIUS profile @Tags RadiusProfile @Param id path int true "Profile ID" @Param profile body ProfileRequest true "Profile information" @Success 200 {object} domain.RadiusProfile @Router /api/v1/radius-profiles/{id} [put]

Types

type DashboardAuthTrendPoint

type DashboardAuthTrendPoint struct {
	Date  string `json:"date"`  // Date label formatted as YYYY-MM-DD
	Count int64  `json:"count"` // Authentication count for the day
}

DashboardAuthTrendPoint is one day in the dashboard's 7-day authentication trend: the per-day count of accounting sessions started on that date.

type DashboardIPv6Stats added in v9.1.0

type DashboardIPv6Stats struct {
	OnlineWithIPv6            int64   `json:"online_with_ipv6"`             // Online sessions carrying any IPv6 attribute
	OnlineWithIPv6Address     int64   `json:"online_with_ipv6_address"`     // Online sessions with a Framed-IPv6-Address
	OnlineWithFramedPrefix    int64   `json:"online_with_framed_prefix"`    // Online sessions with a Framed-IPv6-Prefix
	OnlineWithDelegatedPrefix int64   `json:"online_with_delegated_prefix"` // Online sessions with a Delegated-IPv6-Prefix
	UsersWithStaticAddress    int64   `json:"users_with_static_address"`    // Users provisioned with a static IPv6 address (RadiusUser.IpV6Addr)
	UsersWithDelegatedPrefix  int64   `json:"users_with_delegated_prefix"`  // Users provisioned with a static Delegated-IPv6-Prefix (RFC 4818)
	AdoptionRate              float64 `json:"adoption_rate"`                // Percentage of online sessions carrying any IPv6 attribute
}

DashboardIPv6Stats summarizes IPv6 usage along two dimensions: live adoption across currently online sessions, and static provisioning across the user base. Together they let operators answer both "who is using IPv6 right now?" and "how many subscribers are provisioned for IPv6?" without inspecting the database manually.

type DashboardProfileSlice

type DashboardProfileSlice struct {
	ProfileID   int64  `json:"profile_id"`   // Profile primary key
	ProfileName string `json:"profile_name"` // Profile display name
	Value       int64  `json:"value"`        // Online users assigned to the profile
}

DashboardProfileSlice is one slice of the online-user-by-profile distribution: the profile identified by ProfileID/ProfileName and the number of currently online users assigned to it.

type DashboardStats

type DashboardStats struct {
	TotalUsers          int64                     `json:"total_users"`          // Total number of users
	OnlineUsers         int64                     `json:"online_users"`         // Currently online users
	TodayAuthCount      int64                     `json:"today_auth_count"`     // Authentication count for today
	TodayAcctCount      int64                     `json:"today_acct_count"`     // Accounting record count for today
	TotalProfiles       int64                     `json:"total_profiles"`       // Total number of profiles
	DisabledUsers       int64                     `json:"disabled_users"`       // Disabled users
	ExpiredUsers        int64                     `json:"expired_users"`        // Expired users
	TodayInputGB        float64                   `json:"today_input_gb"`       // Today's upstream traffic (GB)
	TodayOutputGB       float64                   `json:"today_output_gb"`      // Today's downstream traffic (GB)
	AuthTrend           []DashboardAuthTrendPoint `json:"auth_trend"`           // Daily authentication trend (last 7 days)
	Traffic24h          []DashboardTrafficPoint   `json:"traffic_24h"`          // Hourly traffic statistics (last 24 hours)
	ProfileDistribution []DashboardProfileSlice   `json:"profile_distribution"` // Online users grouped by profile
	IPv6Stats           DashboardIPv6Stats        `json:"ipv6_stats"`           // IPv6 adoption among currently online sessions
}

DashboardStats is the aggregated snapshot returned by GetDashboardStats. It combines real-time counters, today's volumes, and several time series so the admin dashboard can render its entire overview from a single response.

type DashboardTrafficPoint

type DashboardTrafficPoint struct {
	Hour       string  `json:"hour"`        // Hour label formatted as YYYY-MM-DD HH:00
	UploadGB   float64 `json:"upload_gb"`   // Upload traffic in GB within the hour
	DownloadGB float64 `json:"download_gb"` // Download traffic in GB within the hour
}

DashboardTrafficPoint is one hour in the dashboard's 24-hour traffic series, holding the upstream and downstream totals (GB) for sessions started in that hour.

type ErrorResponse

type ErrorResponse struct {
	Error   string      `json:"error"`
	Message string      `json:"message"`
	Details interface{} `json:"details,omitempty"`
}

ErrorResponse is the unified failure envelope returned by admin API handlers. Error is a stable, machine-readable code (for example "NOT_FOUND" or "VALIDATION_ERROR") that clients may branch on; Message is a human-readable explanation; Details carries optional structured context and is omitted for server-side (5xx) failures so internal information is never leaked to clients.

type ImportUserError added in v9.1.0

type ImportUserError struct {
	Row      int    `json:"row"`
	Username string `json:"username"`
	Message  string `json:"message"`
}

ImportUserError describes one failed input row during a batch user import.

Row is 1-based within the parsed payload and Username may be empty when the failure occurs before a username can be resolved (for example missing column).

type ImportUserResult added in v9.1.0

type ImportUserResult struct {
	Total   int               `json:"total"`
	Success int               `json:"success"`
	Failed  int               `json:"failed"`
	Errors  []ImportUserError `json:"errors"`
}

ImportUserResult summarizes the outcome of a batch user import request.

Success and Failed are per-row counts and Errors carries only failed rows so callers can present actionable feedback without re-reading the source file.

type Meta

type Meta struct {
	Total    int64 `json:"total"`
	Page     int   `json:"page"`
	PageSize int   `json:"pageSize"`
}

Meta carries pagination information for a list response: the total number of matching rows across all pages, the 1-based page index, and the page size that were applied to the query.

type ProfileRequest

type ProfileRequest struct {
	Name                    string      `json:"name" validate:"required,min=1,max=100"`
	Status                  interface{} `json:"status"` // Can be string or boolean
	AddrPool                string      `json:"addr_pool" validate:"omitempty,addrpool"`
	ActiveNum               int         `json:"active_num" validate:"gte=0,lte=100"`
	UpRate                  int         `json:"up_rate" validate:"gte=0,lte=10000000"`
	DownRate                int         `json:"down_rate" validate:"gte=0,lte=10000000"`
	Domain                  string      `json:"domain" validate:"omitempty,max=50"`
	IPv6PrefixPool          string      `json:"ipv6_prefix_pool" validate:"omitempty"`
	DelegatedIpv6PrefixPool string      `json:"delegated_ipv6_prefix_pool" validate:"omitempty,max=100"` // DHCPv6-PD pool (RFC 6911 §2.4)
	BindMac                 interface{} `json:"bind_mac"`                                                // Can be int or boolean
	BindVlan                interface{} `json:"bind_vlan"`                                               // Can be int or boolean
	Remark                  string      `json:"remark" validate:"omitempty,max=500"`
	NodeId                  interface{} `json:"node_id"` // Can be int64 or string
}

ProfileRequest is the JSON body accepted by CreateProfile. Several fields are typed as interface{} because the frontend may send them as either a string or a boolean/number: Status accepts a string or a boolean (true -> "enabled", false -> "disabled"); BindMac and BindVlan accept a boolean or a number (true -> 1, false -> 0); and NodeId accepts a number or a numeric string. toRadiusProfile normalizes these into a domain.RadiusProfile. The struct tags drive request validation.

type ProfileUpdateRequest

type ProfileUpdateRequest struct {
	Name                    string      `json:"name" validate:"omitempty,min=1,max=100"`
	Status                  interface{} `json:"status"` // Can be string or boolean
	AddrPool                string      `json:"addr_pool" validate:"omitempty,addrpool"`
	ActiveNum               int         `json:"active_num" validate:"gte=0,lte=100"`
	UpRate                  int         `json:"up_rate" validate:"gte=0,lte=10000000"`
	DownRate                int         `json:"down_rate" validate:"gte=0,lte=10000000"`
	Domain                  string      `json:"domain" validate:"omitempty,max=50"`
	IPv6PrefixPool          string      `json:"ipv6_prefix_pool" validate:"omitempty"`
	DelegatedIpv6PrefixPool string      `json:"delegated_ipv6_prefix_pool" validate:"omitempty,max=100"` // DHCPv6-PD pool (RFC 6911 §2.4)
	BindMac                 interface{} `json:"bind_mac"`                                                // Can be int or boolean
	BindVlan                interface{} `json:"bind_vlan"`                                               // Can be int or boolean
	Remark                  string      `json:"remark" validate:"omitempty,max=500"`
	NodeId                  interface{} `json:"node_id"` // Can be int64 or string
}

ProfileUpdateRequest is the JSON body accepted by UpdateProfile. It mirrors ProfileRequest but treats every field as optional: an empty Name leaves the stored name unchanged, and UpdateProfile applies only the supplied fields. The same mixed-type coercion rules apply, performed by toRadiusProfile.

type Response

type Response struct {
	Data interface{} `json:"data,omitempty"`
	Meta *Meta       `json:"meta,omitempty"`
}

Response is the unified success envelope returned by admin API handlers. Data holds the payload (an object or a slice) and is omitted when nil; Meta is present only for paginated list responses. Handlers populate it through the internal ok and paged helpers rather than constructing it directly.

type SystemBackup added in v9.1.0

type SystemBackup struct {
	// Version is the backup schema version in "major.minor" form.
	Version string `json:"version"`
	// CreatedAt is the server timestamp when the backup was generated.
	CreatedAt time.Time `json:"created_at"`
	// Nodes stores exported network node definitions.
	Nodes []domain.NetNode `json:"nodes"`
	// Nas stores exported NAS device records.
	Nas []domain.NetNas `json:"nas"`
	// Profiles stores exported RADIUS profile definitions.
	Profiles []domain.RadiusProfile `json:"profiles"`
	// Users stores exported RADIUS user records.
	Users []domain.RadiusUser `json:"users"`
	// Configs stores exported dynamic system configuration items.
	Configs []domain.SysConfig `json:"configs"`
	// Operators stores exported admin operator accounts.
	Operators []domain.SysOpr `json:"operators"`
	// Certs stores exported managed certificates (sys_cert), including their
	// PEM private keys, so certificate-based EAP (EAP-TLS/PEAP/TTLS) keeps
	// working after a restore. Restores older backups without this field
	// simply skip the table.
	Certs []SystemBackupCert `json:"certs,omitempty"`
}

SystemBackup is the on-disk JSON snapshot exchanged by backupSystem and restoreSystem.

The payload is versioned by Version and contains the core configuration tables that define runtime behavior (nodes, NAS, profiles, users, configs, operators, and managed certificates). The snapshot intentionally preserves primary keys so restore can upsert records deterministically.

Sensitive data notice: the payload includes security-relevant credentials (for example RadiusUser passwords, SysOpr password hashes, and SysCert private keys). Callers must treat serialized backups as secrets at rest and in transit.

type SystemBackupCert added in v9.2.0

type SystemBackupCert struct {
	domain.SysCert
	// PrivateKey is the PEM-encoded private key of the certificate, exported
	// only inside backups.
	PrivateKey string `json:"private_key,omitempty"`
}

SystemBackupCert is the backup serialization of a domain.SysCert record.

domain.SysCert deliberately hides PrivateKey from the REST API via json:"-", so marshaling SysCert directly would silently drop key material and a restored server certificate would be unusable for EAP-TLS/PEAP/TTLS. This wrapper re-exposes the private key for the access-controlled backup payload, which is already treated as a secret (it carries user passwords and operator hashes). The list/detail certificate APIs remain unaffected and never disclose the key.

type SystemRestoreResult added in v9.1.0

type SystemRestoreResult struct {
	// Nodes is the number of net_node records restored.
	Nodes int `json:"nodes"`
	// Nas is the number of net_nas records restored.
	Nas int `json:"nas"`
	// Profiles is the number of radius_profile records restored.
	Profiles int `json:"profiles"`
	// Users is the number of radius_user records restored.
	Users int `json:"users"`
	// Configs is the number of sys_config records restored.
	Configs int `json:"configs"`
	// Operators is the number of sys_opr records restored.
	Operators int `json:"operators"`
	// Certs is the number of sys_cert records restored.
	Certs int `json:"certs"`
}

SystemRestoreResult reports how many records restoreSystem upserted into each table during a successful restore transaction.

A zero value for a field means either the table was absent from the payload or the payload contained no records for that table.

type UserRequest

type UserRequest struct {
	NodeID          interface{} `json:"node_id"`                                     // Can be int64 or string
	ProfileID       interface{} `json:"profile_id" validate:"required"`              // Can be int64 or string
	LegacyProfileID interface{} `json:"profileid"`                                   // Legacy fallback field from older clients, used when profile_id is missing
	Realname        string      `json:"realname" validate:"omitempty,max=100"`       // Real name
	Email           string      `json:"email" validate:"omitempty,email,max=100"`    // Email
	Mobile          string      `json:"mobile" validate:"omitempty,max=20"`          // Mobile number (optional, max 20 characters)
	Address         string      `json:"address" validate:"omitempty,max=255"`        // addresses
	Username        string      `json:"username" validate:"required,min=3,max=50"`   // Username
	Password        string      `json:"password" validate:"omitempty,min=6,max=128"` // Password
	AddrPool        string      `json:"addr_pool" validate:"omitempty,max=50"`       // Address pool
	Vlanid1         int         `json:"vlanid1" validate:"gte=0,lte=4096"`           // VLAN ID 1
	Vlanid2         int         `json:"vlanid2" validate:"gte=0,lte=4096"`           // VLAN ID 2
	IpAddr          string      `json:"ip_addr" validate:"omitempty,ipv4"`           // IPv4addresses
	Ipv6Addr        string      `json:"ipv6_addr" validate:"omitempty"`              // IPv6addresses
	MacAddr         string      `json:"mac_addr" validate:"omitempty,mac"`           // MACaddresses
	BindVlan        interface{} `json:"bind_vlan"`                                   // Can be int or boolean
	BindMac         interface{} `json:"bind_mac"`                                    // Can be int or boolean
	ExpireTime      string      `json:"expire_time" validate:"omitempty"`            // Expiration time
	Status          interface{} `json:"status"`                                      // Can be string or boolean
	Remark          string      `json:"remark" validate:"omitempty,max=500"`         // Remark
}

UserRequest represents the request body for creating a RADIUS user via POST /api/v1/users.

It preserves compatibility with older clients that still send legacy keys such as profileid, or mixed JSON types for flags (for example bool vs number for bind_mac/bind_vlan and status). The handler normalizes those variants before persisting the user.

type UserUpdateRequest

type UserUpdateRequest struct {
	NodeID                  interface{} `json:"node_id"`                                                 // Can be int64 or string
	ProfileID               interface{} `json:"profile_id"`                                              // Can be int64 or string
	LegacyProfileID         interface{} `json:"profileid"`                                               // Legacy fallback field from older clients, used when profile_id is missing
	Realname                string      `json:"realname" validate:"omitempty,max=100"`                   // Real name
	Email                   string      `json:"email" validate:"omitempty,email,max=100"`                // Email
	Mobile                  string      `json:"mobile" validate:"omitempty,max=20"`                      // Mobile number (optional, max 20 characters)
	Address                 string      `json:"address" validate:"omitempty,max=255"`                    // addresses
	Username                string      `json:"username" validate:"omitempty,min=3,max=50"`              // Username
	Password                string      `json:"password" validate:"omitempty,min=6,max=128"`             // Password
	AddrPool                string      `json:"addr_pool" validate:"omitempty,max=50"`                   // Address pool
	Vlanid1                 int         `json:"vlanid1" validate:"gte=0,lte=4096"`                       // VLAN ID 1
	Vlanid2                 int         `json:"vlanid2" validate:"gte=0,lte=4096"`                       // VLAN ID 2
	IpAddr                  string      `json:"ip_addr" validate:"omitempty,ipv4"`                       // IPv4addresses
	Ipv6Addr                string      `json:"ipv6_addr" validate:"omitempty"`                          // IPv6addresses
	MacAddr                 string      `json:"mac_addr" validate:"omitempty,mac"`                       // MACaddresses
	BindVlan                interface{} `json:"bind_vlan"`                                               // Can be int or boolean
	BindMac                 interface{} `json:"bind_mac"`                                                // Can be int or boolean
	ExpireTime              string      `json:"expire_time" validate:"omitempty"`                        // Expiration time
	Status                  interface{} `json:"status"`                                                  // Can be string or boolean
	Remark                  string      `json:"remark" validate:"omitempty,max=500"`                     // Remark
	IPv6PrefixPool          string      `json:"ipv6_prefix_pool" validate:"omitempty,max=100"`           // IPv6 prefix pool name
	DelegatedIpv6Prefix     string      `json:"delegated_ipv6_prefix" validate:"omitempty,max=100"`      // Static Delegated-IPv6-Prefix (RFC 4818)
	DelegatedIpv6PrefixPool string      `json:"delegated_ipv6_prefix_pool" validate:"omitempty,max=100"` // Delegated-IPv6-Prefix-Pool (RFC 6911 §2.4)
	Domain                  string      `json:"domain" validate:"omitempty,max=100"`                     // User domain
	ProfileLinkMode         int         `json:"profile_link_mode" validate:"gte=0,lte=1"`                // Profile link mode (0=static, 1=dynamic)
}

UserUpdateRequest represents the request body for updating a RADIUS user via PUT /api/v1/users/:id.

Most fields are optional and are applied as partial updates. As with UserRequest, legacy and mixed-type frontend payloads are accepted and normalized so older admin clients remain compatible.

Jump to

Keyboard shortcuts

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