Documentation
¶
Overview ¶
templ: version: v0.2.793
Package server provides a HTTP server implementation for the Apollo stack. Handlers in this stack take an application-specific state object (used for dependency injection) and a Apollo object which contains a lot of utility functions.
Basic example:
import (
"log"
"net/http"
"myapp/components"
"myapp/handlers"
"myapp/middleware"
"myapp/state"
"github.com/prior-it/apollo/server"
)
func main() {
// Create server
state := state.New()
server := server.New(state).
WithLogger(state.Logger).
WithDebug(true)
// Attach middleware
server.Use(
middleware.RequestID,
middleware.Recoverer,
middleware.Timeout(5 * time.Second),
)
// Attach routes
server.StaticFiles("/static/", "./assets/", staticFS)
server.Get("/", Home).
Get("/ping", handlers.Ping).
Post("/login", handlers.DoLogin)
// Run server
log.Fatal(http.ListenAndServe("localhost:8080", server))
}
func Home(apollo *server.Apollo, _ *state.State) error {
return apollo.RenderPage(components.HomePage())
}
templ: version: v0.2.793
templ: version: v0.2.793
Index ¶
- Constants
- Variables
- func CSRF() templ.Component
- func CSRFToken(ctx context.Context) string
- func Config(ctx context.Context) *config.Config
- func Debug(printFullRequest bool) func(http.Handler) http.Handler
- func DefaultErrorHandler(apollo *Apollo, err error)
- func DetectLanguage[state any](apollo *Apollo, _ state) (context.Context, error)
- func HTTPLogger(cfg *config.Config) func(http.Handler) http.Handler
- func HasActiveOrganisation(ctx context.Context) bool
- func HasFeature(ctx context.Context, feature string) bool
- func InjectApollo[state any](apollo *Apollo, _ state) (context.Context, error)
- func IsAdmin(ctx context.Context) bool
- func IsLoggedIn(ctx context.Context) bool
- func IsStaticFile(path string) bool
- func Language(ctx context.Context) string
- func OrganisationID(ctx context.Context) core.OrganisationID
- func OrganisationName(ctx context.Context) string
- func OrganisationParentID(ctx context.Context) *core.OrganisationID
- func PostHogSnippet() templ.Component
- func ReplaceSlogAttributes(cfg *config.Config) func(groups []string, a slog.Attr) slog.Attr
- func RequireLogin[state any](apollo *Apollo, _ state) (context.Context, error)
- func Session(ctx context.Context) *sessions.Session
- func UserID(ctx context.Context) core.UserID
- func UserName(ctx context.Context) string
- type Apollo
- func (apollo *Apollo) AddHeader(header string, value string)
- func (apollo *Apollo) CheckCSRF() error
- func (apollo *Apollo) Context() context.Context
- func (apollo *Apollo) CreateProtocolURL(endpoint string) string
- func (apollo *Apollo) CreateURL(endpoint string) string
- func (apollo *Apollo) Debug(msg string, args ...any)
- func (apollo *Apollo) Error(msg string, args ...any)
- func (apollo *Apollo) FormValue(key string) string
- func (apollo *Apollo) GetHeader(header string) string
- func (apollo *Apollo) GetPath(key string) string
- func (apollo *Apollo) GetQuery(param string) string
- func (apollo *Apollo) Has(permission permissions.Permission) bool
- func (apollo *Apollo) HasInOrganisation(permission permissions.Permission, organisation core.OrganisationID) bool
- func (apollo *Apollo) HasInOrganisationStrict(permission permissions.Permission, organisation core.OrganisationID) bool
- func (apollo *Apollo) HasStrict(permission permissions.Permission) bool
- func (apollo *Apollo) Host() string
- func (apollo *Apollo) LogField(field string, value slog.Value)
- func (apollo *Apollo) LogString(field string, value string)
- func (apollo *Apollo) Login(user *core.User) error
- func (apollo *Apollo) Logout() error
- func (apollo *Apollo) ParseBody(v interface{}) error
- func (apollo *Apollo) Path() string
- func (apollo *Apollo) Protocol() string
- func (apollo *Apollo) Redirect(url string)
- func (apollo *Apollo) RedirectWithCode(url string, statusCode int)
- func (apollo *Apollo) RenderComponent(component templ.Component) error
- func (apollo *Apollo) RenderPage(page templ.Component, renderOptions *RenderOptions) error
- func (apollo *Apollo) Requires(permission permissions.Permission) error
- func (apollo *Apollo) RequiresInOrganisation(permission permissions.Permission, organisation core.OrganisationID) error
- func (apollo *Apollo) RequiresInOrganisationStrict(permission permissions.Permission, organisation core.OrganisationID) error
- func (apollo *Apollo) RequiresLogin() error
- func (apollo *Apollo) RequiresStrict(permission permissions.Permission) error
- func (apollo *Apollo) Session() *sessions.Session
- func (apollo *Apollo) SetActiveOrganisation(organisation *core.Organisation) error
- func (apollo *Apollo) StatusCode(code int)
- type ErrorHandler
- type Handler
- type Middleware
- type NotFoundHandler
- type RenderOptions
- type Server
- func (server *Server[state]) AttachDefaultMiddleware()
- func (server *Server[state]) CSRFTokenMiddleware() func(http.Handler) http.Handler
- func (server *Server[state]) Component(pattern string, component templ.Component) *Server[state]
- func (server *Server[state]) ContextMiddleware(next http.Handler) http.Handler
- func (server *Server[state]) Delete(pattern string, handlerFn func(apollo *Apollo, state state) error) *Server[state]
- func (server *Server[state]) FeatureFlagMiddleware(next http.Handler) http.Handler
- func (server *Server[state]) Get(pattern string, handlerFn func(apollo *Apollo, state state) error) *Server[state]
- func (server *Server[state]) Group(pattern string) *Server[state]
- func (server *Server[state]) Handle(pattern string, handler http.Handler) *Server[state]
- func (server *Server[state]) MiddlewareHandler(middleware Middleware[state]) func(http.Handler) http.Handler
- func (server *Server[state]) NewApollo(w http.ResponseWriter, r *http.Request) *Apollo
- func (server *Server[state]) Page(pattern string, component templ.Component, options *RenderOptions) *Server[state]
- func (server *Server[state]) Post(pattern string, handlerFn func(apollo *Apollo, state state) error) *Server[state]
- func (server *Server[state]) Put(pattern string, handlerFn func(apollo *Apollo, state state) error) *Server[state]
- func (server *Server[state]) RedirectSlashes(next http.Handler) http.Handler
- func (server *Server[state]) ServeHTTP(writer http.ResponseWriter, request *http.Request)
- func (server *Server[state]) SessionMiddleware() func(http.Handler) http.Handler
- func (server *Server[state]) Shutdown(ctx context.Context)
- func (server *Server[state]) Start(ctx context.Context, listener *net.Listener) error
- func (server *Server[state]) StaticFiles(pattern string, dir string, files fs.ReadDirFS)
- func (server *Server[state]) Use(middlewares ...Middleware[state]) *Server[state]
- func (server *Server[state]) UseStd(middlewares ...func(http.Handler) http.Handler) *Server[state]
- func (server *Server[state]) WithConfig(cfg *config.Config) *Server[state]
- func (server *Server[state]) WithDefaultLayout(layout templ.Component) *Server[state]
- func (server *Server[state]) WithErrorHandler(errorHandler ErrorHandler) *Server[state]
- func (s *Server[state]) WithI18n(fs fs.FS) *Server[state]
- func (server *Server[state]) WithLogger(logger *slog.Logger) *Server[state]
- func (server *Server[state]) WithNotFoundHandler(notFoundHandler NotFoundHandler) *Server[state]
- func (server *Server[state]) WithPermissionService(service permissions.Service) *Server[state]
- func (server *Server[state]) WithSessionStore(store sessions.Store) *Server[state]
- type State
Constants ¶
const CSRFTokenLength = 32
const CsrfName = "_csrf_token"
Variables ¶
var ( ErrCSRFNotFound = errors.New("CSRF token not found") ErrCSRFFail = errors.New("CSRF token does not match") )
Functions ¶
func Debug ¶
Debug is middleware that can be inserted anywhere and will print some useful debug information about the current request.
func DefaultErrorHandler ¶
func DetectLanguage ¶
DetectLanguage is middleware that automatically tries to detect a user's language by looking at the request headers. If the detected language is not found, it will fallback to the configure fallback language. This will return an error only if no bundle could be found for the configured fallback language.
func HTTPLogger ¶
HTTPLogger is middleware that will log HTTP requests, including context that might be added by the handler itself by calling apollo.LogField.
func HasActiveOrganisation ¶
func IsLoggedIn ¶
func IsStaticFile ¶
func OrganisationID ¶
func OrganisationID(ctx context.Context) core.OrganisationID
func OrganisationName ¶
func OrganisationParentID ¶
func OrganisationParentID(ctx context.Context) *core.OrganisationID
func PostHogSnippet ¶
func ReplaceSlogAttributes ¶
This is used by slog to maintain a consistent output over different logging libraries, application code should never call this directly unless you're 100% sure what you're doing.
func RequireLogin ¶
RequireLogin is middleware that requires that any user is logged in before continuing on.
Types ¶
type Apollo ¶
type Apollo struct {
Writer http.ResponseWriter
Request *http.Request
User *core.User
Cfg *config.Config
Organisation *core.Organisation
// contains filtered or unexported fields
}
func (*Apollo) AddHeader ¶
AddHeader adds the header, value pair to the response header. It appends to any existing values associated with key. Both the header and its value are case-insensitive.
func (*Apollo) CheckCSRF ¶
CheckCSRF will check if a CSRF token was added to the requests form body and if that token matches the token specified in the CSRF cookie. If either of these are false, this will return an error. If the correct CSRF token was specified, this will return nil. Note that you can only check the CSRF token once, the token will be discarded if the check passes. Also note that currently only POST requests using form values are supported. GET requests cannot use CSRF since that's less safe. JSON API requests can also not use CSRF at this moment, but that might change in the future using a CSRF header, if that need would ever arise.
func (*Apollo) Context ¶
Context returns the request's context.
The returned context is always non-nil; it defaults to the background context.
The context is canceled when the client's connection closes, the request is canceled (with HTTP/2), or when the ServeHTTP method returns.
func (*Apollo) CreateProtocolURL ¶
CreateProtocolURL will return the full url for the given endpoint, including its protocol. If you don't want the current protocol to be included, use [CreateURL] instead.
func (*Apollo) CreateURL ¶
CreateURL will return the url for the given endpoint. If you need to include the current protocol as well, use [CreateProtocolURL] instead.
func (*Apollo) Debug ¶
Log the specified debug message. args is a list of structured fields to add to the message. The arguments should alternate between a field's name (string) and its value (any). This behaves the same as log/slog.Debug
Example ¶
server.Debug("New user registered", "user", user, "id", id)
func (*Apollo) Error ¶
Log the specified error message. args is a list of structured fields to add to the error message. The arguments should alternate between a field's name (string) and its value (any). This behaves the same as log/slog.Error
Example ¶
server.Error("Something went wrong", "error", err, "user", user)
func (*Apollo) FormValue ¶
FormValue returns the first value for the named component of the POST, PUT, or PATCH request body. URL query parameters are ignored. This ignores any errors, if the key is not present, FormValue returns the empty string.
func (*Apollo) GetHeader ¶
GetHeader returns the first value associated with the given header in the request. If there are no values set for the header, this returns the empty string. Both the header and its value are case-insensitive.
func (*Apollo) GetPath ¶
GetPath returns the value for the named path wildcard in the router pattern that matched the request. It returns the empty string if the request was not matched against a pattern or there is no such wildcard in the pattern.
E.g.: A route defined as `/users/{id}` can call `GetPath("id")` to return the value for "id" in the current path.
func (*Apollo) GetQuery ¶
GetQuery returns the first value associated with the given query parameter in the request url. If there are no values set for the query param, this returns the empty string. This silently discards malformed value pairs. To check query errors use [Request().ParseQuery].
func (*Apollo) Has ¶
func (apollo *Apollo) Has(permission permissions.Permission) bool
Has returns a boolean indicating whether or not the currently logged in user has the specified permission in any of their permission groups or not. If no user is logged in, this will return false. If there is an active organisation set, this will recursively check the permissions in that organisation's lineage.
func (*Apollo) HasInOrganisation ¶
func (apollo *Apollo) HasInOrganisation( permission permissions.Permission, organisation core.OrganisationID, ) bool
HasInOrganisation returns a boolean indicating whether or not the currently logged in user has the specified permission within the specified organisation or not. If no user is logged in, this will always return false. This will recursively check the permissions in the specified organisation's lineage.
func (*Apollo) HasInOrganisationStrict ¶
func (apollo *Apollo) HasInOrganisationStrict( permission permissions.Permission, organisation core.OrganisationID, ) bool
HasInOrganisationStrict returns a boolean indicating whether or not the currently logged in user has the specified permission within the specified organisation (and only there, global permissions and parent organisations are ignored). If no user is logged in, this will always return false.
func (*Apollo) HasStrict ¶
func (apollo *Apollo) HasStrict(permission permissions.Permission) bool
HasStrict returns a boolean indicating whether or not the currently logged in user has the specified permission in the currently active organisation (and only there, global permissions and parent organisations are ignored). If no user is logged in or they haven't chosen an active organisation yet, this will always return false.
func (*Apollo) Host ¶
Host specifies the host on which the URL is sought. For HTTP/1 (per RFC 7230, section 5.4), this is either the value of the "Host" header or the host name given in the URL itself. For HTTP/2, it is the value of the ":authority" pseudo-header field. It may be of the form "host:port". For international domain names, Host may be in Punycode or Unicode form. Use golang.org/x/net/idna to convert it to either format if needed. To prevent DNS rebinding attacks, server Handlers should validate that the Host header has a value for which the Handler considers itself authoritative. The included ServeMux supports patterns registered to particular host names and thus protects its registered Handlers.
func (*Apollo) LogField ¶
LogField will add the specified field and its value to the current request's span
Example ¶
apollo.LogField("user_id", slog.IntValue(user.id)
func (*Apollo) LogString ¶
LogField will add the specified field and its value to the current request's span
func (*Apollo) ParseBody ¶
ParseBody parses the request body into an interface using the form decoder. @TODO: Allow other decoders as well, it should be possible to get the correct decoder from the request headers. @TODO: This decoder has an "IgnoreUnknownKeys" option, do we want to expose that as well?
Example: ¶
var data SomeStruct
if err := server.ParseBody(&data); err != nil {
return fmt.Errorf("cannot parse body: %w", err)
}
func (*Apollo) Protocol ¶
Protocol returns the currently used protocol (either "http://" or "https://")
func (*Apollo) Redirect ¶
Redirect will return a response that redirects the user to the specified url. If HTMX is available, this will redirect using HTMX.
func (*Apollo) RedirectWithCode ¶
Redirect will return a response that redirects the user to the specified url with the specified status code. If HTMX is available, this will redirect using HTMX with status code 200 (so that HTMX would be triggered).
func (*Apollo) RenderComponent ¶
RenderComponent renders the specified component in the response body. You can render multiple components and they will all be returned by the response, this can be used to perform out-of-band swaps with HTMX, for example.
Example: ¶
if err := server.RenderComponent(components.OOBNotification(data)); err != nil {
return err
}
return server.RenderComponent(components.ActualResponse(otherData))
func (*Apollo) RenderPage ¶
func (apollo *Apollo) RenderPage( page templ.Component, renderOptions *RenderOptions, ) error
RenderPage renders the specified page in the response body. If the request was made using HTMX, it will simply return the pages contents. If the request was made without HTMX (e.g. by refreshing), it will return the page surrounded with the current default lay-out, unless a custom layout was specified in the render options. You can change the default lay-out by calling `SetDefaultLayout` on the router during configuration. RenderPage can be combined with RenderComponent to perform out-of-band swaps in a single response.
Example: ¶
if err := server.RenderComponent(components.OOBNotification(data)); err != nil {
return err
}
return server.RenderPage(pages.ActualResponse(otherData), nil)
func (*Apollo) Requires ¶
func (apollo *Apollo) Requires(permission permissions.Permission) error
Requires will return core.ErrForbidden if the current user does not have the specified permission and nil otherwise. If no user is logged in at all, this will return core.ErrUnauthenticated.
func (*Apollo) RequiresInOrganisation ¶
func (apollo *Apollo) RequiresInOrganisation( permission permissions.Permission, organisation core.OrganisationID, ) error
RequiresInOrganisation will return core.ErrForbidden if the current user does not have the specified permission, either globally, or within the lineage of the specified organisation and nil otherwise. If no user is logged in at all, this will return core.ErrUnauthenticated.
func (*Apollo) RequiresInOrganisationStrict ¶
func (apollo *Apollo) RequiresInOrganisationStrict( permission permissions.Permission, organisation core.OrganisationID, ) error
RequiresInOrganisationStrict will return core.ErrForbidden if the current user does not have the specified permission and nil otherwise. If no user is logged in at all, this will return core.ErrUnauthenticated. This will use the strict permission check, which ignores global permissions and only checks the specified organisation, not its lineage.
func (*Apollo) RequiresLogin ¶
RequiresLogin will return core.ErrUnauthenticated if there is no user logged in and nil otherwise.
func (*Apollo) RequiresStrict ¶
func (apollo *Apollo) RequiresStrict(permission permissions.Permission) error
RequiresStrict will return core.ErrForbidden if the current user does not have the specified permission and nil otherwise. If no user is logged in at all, this will return core.ErrUnauthenticated. This will use the strict permission check, which ignores global permissions and only checks the active organisation.
func (*Apollo) SetActiveOrganisation ¶
func (apollo *Apollo) SetActiveOrganisation(organisation *core.Organisation) error
SetActiveOrganisation changes the "active organisation". This might influence some other organisation-dependent requests such as permission checks. Beware: this might update apollo.Context(), reretrieve that if you were already using it.
func (*Apollo) StatusCode ¶
type ErrorHandler ¶
type Middleware ¶
func ConvertToApolloMiddleware ¶
func ConvertToApolloMiddleware[state State]( middleware func(w http.ResponseWriter, r *http.Request), ) Middleware[state]
ConvertToApolloMiddleware will convert stdlib middleware to Apollo middleware
type NotFoundHandler ¶
type NotFoundHandler func(apollo *Apollo)
type RenderOptions ¶
type RenderOptions struct {
// If CustomLayout is set, the page will render with the specified layout instead of the default one
CustomLayout templ.Component
}
RenderOptions specifies options you can use when rendering a page with the Apollo object.
type Server ¶
type Server[state State] struct { // contains filtered or unexported fields }
func (*Server[state]) AttachDefaultMiddleware ¶
func (server *Server[state]) AttachDefaultMiddleware()
func (*Server[state]) CSRFTokenMiddleware ¶
CSRFTokenMiddleware injects a csrf token at the end of each request that can be checked on the next request using apollo.CheckCSRF.
func (*Server[state]) Component ¶
Component adds the route `pattern` that matches a GET http method to render the specified templ component without any layout.
func (*Server[state]) ContextMiddleware ¶
ContextMiddleware enriches the request context. You should always attach this before adding routes in order to use Apollo effectively.
func (*Server[state]) Delete ¶
func (server *Server[state]) Delete( pattern string, handlerFn func(apollo *Apollo, state state) error, ) *Server[state]
Delete adds the route `pattern` that matches a POST http method to execute the `handlerFn` http.HandlerFunc.
func (*Server[state]) FeatureFlagMiddleware ¶
func (*Server[state]) Get ¶
func (server *Server[state]) Get( pattern string, handlerFn func(apollo *Apollo, state state) error, ) *Server[state]
Get adds the route `pattern` that matches a GET http method to execute the `handlerFn` HandlerFunc.
func (*Server[state]) Group ¶
Group attaches another Handler or Router as a subrouter along a routing path. It's very useful to split up a large API as many independent routers and compose them as a single service. Or to attach an additional set of middleware along a group of endpoints, e.g. a subtree of authenticated endpoints.
Note that Group() does NOT return the original server but rather a subroute server that only serves routes along the specified Group pattern. This simply sets a wildcard along the `pattern` that will continue routing at return subroute server. As a result, if you define two Group() routes on the exact same pattern, the second group will panic.
func (*Server[state]) Handle ¶
Handle adds the route `pattern` that matches any http method to execute the `handler` net/http.Handler.
func (*Server[state]) MiddlewareHandler ¶
func (server *Server[state]) MiddlewareHandler( middleware Middleware[state], ) func(http.Handler) http.Handler
Utility function that converts Apollo middleware to a http handler
func (*Server[state]) Page ¶
func (server *Server[state]) Page( pattern string, component templ.Component, options *RenderOptions, ) *Server[state]
Page adds the route `pattern` that matches a GET http method to render the specified templ component in the default layout.
func (*Server[state]) Post ¶
func (server *Server[state]) Post( pattern string, handlerFn func(apollo *Apollo, state state) error, ) *Server[state]
Post adds the route `pattern` that matches a POST http method to execute the `handlerFn` http.HandlerFunc.
func (*Server[state]) Put ¶
func (server *Server[state]) Put( pattern string, handlerFn func(apollo *Apollo, state state) error, ) *Server[state]
Put adds the route `pattern` that matches a POST http method to execute the `handlerFn` http.HandlerFunc.
func (*Server[state]) RedirectSlashes ¶
RedirectSlashes is middleware that redirects all requests that end in `/` to requests without trailing slash. This differs from chi's own RedirectSlashes middleware in that it uses config.BaseURL() instead of the incoming request's host header.
func (*Server[state]) ServeHTTP ¶
func (server *Server[state]) ServeHTTP(writer http.ResponseWriter, request *http.Request)
ServeHTTP implements net/http.Handler.
func (*Server[state]) SessionMiddleware ¶
SessionMiddleware returns the Apollo session middleware. Attach this before adding routes, if you want to use sessions.
func (*Server[state]) Shutdown ¶
Shutdown will gracefully release all server resources. You generally don't need to call this manually.
func (*Server[state]) Start ¶
Start a new goroutine that runs the server. If no listener is provided, a new TCP listener will be created on the configured host and port.
func (*Server[state]) StaticFiles ¶
StaticFiles serves all files in the `dir` directory or the `fs` FileSystem at the `pattern` url. In debug mode, assets will be loaded from disk to support hot-reloading. In production mode, assets will be gzipped and embedded in the executable instead. Debug mode hot-reloading will be disabled if dir is set to the empty string. Filesystems will ignore `/static` folders and instead directly target the files inside. So if your filesystem has a file "/static/file.txt", you can get it directly with "/file.txt".
Example:
server.StaticFiles("/assets/", "./static/", assetsFS)
func (*Server[state]) Use ¶
Use appends an Apollo middleware handler to the middleware stack.
The middleware stack for any server will execute before searching for a matching route to a specific handler, which provides opportunity to respond early, change the course of the request execution, or set request-scoped values for the next Handler.
func (*Server[state]) UseStd ¶
UseStd appends a stdlib middleware handler to the middleware stack.
The middleware stack for any server will execute before searching for a matching route to a specific handler, which provides opportunity to respond early, change the course of the request execution, or set request-scoped values for the next Handler.
func (*Server[state]) WithConfig ¶
func (*Server[state]) WithDefaultLayout ¶
func (*Server[state]) WithErrorHandler ¶
func (server *Server[state]) WithErrorHandler(errorHandler ErrorHandler) *Server[state]
func (*Server[state]) WithI18n ¶
This will panic if APP_FALLBACKLANG has not been set or if no bundle could be found for the fallback language.
func (*Server[state]) WithLogger ¶
func (*Server[state]) WithNotFoundHandler ¶
func (server *Server[state]) WithNotFoundHandler(notFoundHandler NotFoundHandler) *Server[state]
func (*Server[state]) WithPermissionService ¶
func (server *Server[state]) WithPermissionService(service permissions.Service) *Server[state]