server

package
v0.0.0-...-2757dea Latest Latest
Warning

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

Go to latest
Published: Jun 6, 2025 License: MIT Imports: 32 Imported by: 0

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

View Source
const CSRFTokenLength = 32
View Source
const CsrfName = "_csrf_token"

Variables

View Source
var (
	ErrCSRFNotFound = errors.New("CSRF token not found")
	ErrCSRFFail     = errors.New("CSRF token does not match")
)

Functions

func CSRF

func CSRF() templ.Component

func CSRFToken

func CSRFToken(ctx context.Context) string

func Config

func Config(ctx context.Context) *config.Config

func Debug

func Debug(printFullRequest bool) func(http.Handler) http.Handler

Debug is middleware that can be inserted anywhere and will print some useful debug information about the current request.

func DefaultErrorHandler

func DefaultErrorHandler(apollo *Apollo, err error)

func DetectLanguage

func DetectLanguage[state any](apollo *Apollo, _ state) (context.Context, error)

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

func HTTPLogger(cfg *config.Config) func(http.Handler) http.Handler

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 HasActiveOrganisation(ctx context.Context) bool

func HasFeature

func HasFeature(ctx context.Context, feature string) bool

func InjectApollo

func InjectApollo[state any](apollo *Apollo, _ state) (context.Context, error)

func IsAdmin

func IsAdmin(ctx context.Context) bool

func IsLoggedIn

func IsLoggedIn(ctx context.Context) bool

func IsStaticFile

func IsStaticFile(path string) bool

func Language

func Language(ctx context.Context) string

Return the 2-letter code for the language that is currently active

func OrganisationID

func OrganisationID(ctx context.Context) core.OrganisationID

func OrganisationName

func OrganisationName(ctx context.Context) string

func OrganisationParentID

func OrganisationParentID(ctx context.Context) *core.OrganisationID

func PostHogSnippet

func PostHogSnippet() templ.Component

func ReplaceSlogAttributes

func ReplaceSlogAttributes(cfg *config.Config) func(groups []string, a slog.Attr) slog.Attr

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

func RequireLogin[state any](apollo *Apollo, _ state) (context.Context, error)

RequireLogin is middleware that requires that any user is logged in before continuing on.

func Session

func Session(ctx context.Context) *sessions.Session

Session provides access to the current user's session. Applications can use this to attach or retrieve custom data from this session. Make sure to prefix all custom keys with "app-" so they won't interfere with the Apollo session context.

func UserID

func UserID(ctx context.Context) core.UserID

func UserName

func UserName(ctx context.Context) string

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 Ap

func Ap(ctx context.Context) *Apollo

func (*Apollo) AddHeader

func (apollo *Apollo) AddHeader(header string, value string)

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

func (apollo *Apollo) CheckCSRF() error

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

func (apollo *Apollo) Context() context.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

func (apollo *Apollo) CreateProtocolURL(endpoint string) string

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

func (apollo *Apollo) CreateURL(endpoint string) string

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

func (apollo *Apollo) Debug(msg string, args ...any)

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

func (apollo *Apollo) Error(msg string, args ...any)

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

func (apollo *Apollo) FormValue(key string) string

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

func (apollo *Apollo) GetHeader(header string) string

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

func (apollo *Apollo) GetPath(key string) string

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

func (apollo *Apollo) GetQuery(param string) string

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

func (apollo *Apollo) Host() string

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

func (apollo *Apollo) LogField(field string, value slog.Value)

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

func (apollo *Apollo) LogString(field string, value string)

LogField will add the specified field and its value to the current request's span

func (*Apollo) Login

func (apollo *Apollo) Login(user *core.User) error

Login will log in with the specified user.

func (*Apollo) Logout

func (apollo *Apollo) Logout() error

Logout will log the current user out.

func (*Apollo) ParseBody

func (apollo *Apollo) ParseBody(v interface{}) error

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) Path

func (apollo *Apollo) Path() string

Path returns the full path of the request.

func (*Apollo) Protocol

func (apollo *Apollo) Protocol() string

Protocol returns the currently used protocol (either "http://" or "https://")

func (*Apollo) Redirect

func (apollo *Apollo) Redirect(url string)

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

func (apollo *Apollo) RedirectWithCode(url string, statusCode int)

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

func (apollo *Apollo) RenderComponent(
	component templ.Component,
) error

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

func (apollo *Apollo) RequiresLogin() error

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) Session

func (apollo *Apollo) Session() *sessions.Session

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

func (apollo *Apollo) StatusCode(code int)

type ErrorHandler

type ErrorHandler func(apollo *Apollo, err error)

type Handler

type Handler[state any] func(apollo *Apollo, state state) error

type Middleware

type Middleware[state any] func(apollo *Apollo, state state) (context.Context, error)

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 New

func New[state State](s state, cfg *config.Config) *Server[state]

New creates a new server with the specified state object and configuration.

func (*Server[state]) AttachDefaultMiddleware

func (server *Server[state]) AttachDefaultMiddleware()

func (*Server[state]) CSRFTokenMiddleware

func (server *Server[state]) CSRFTokenMiddleware() func(http.Handler) http.Handler

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

func (server *Server[state]) Component(
	pattern string,
	component templ.Component,
) *Server[state]

Component adds the route `pattern` that matches a GET http method to render the specified templ component without any layout.

func (*Server[state]) ContextMiddleware

func (server *Server[state]) ContextMiddleware(next http.Handler) http.Handler

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 *Server[state]) FeatureFlagMiddleware(next http.Handler) http.Handler

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

func (server *Server[state]) Group(
	pattern string,
) *Server[state]

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

func (server *Server[state]) Handle(pattern string, handler http.Handler) *Server[state]

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]) NewApollo

func (server *Server[state]) NewApollo(w http.ResponseWriter, r *http.Request) *Apollo

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

func (server *Server[state]) RedirectSlashes(next http.Handler) http.Handler

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

func (server *Server[state]) SessionMiddleware() func(http.Handler) http.Handler

SessionMiddleware returns the Apollo session middleware. Attach this before adding routes, if you want to use sessions.

func (*Server[state]) Shutdown

func (server *Server[state]) Shutdown(ctx context.Context)

Shutdown will gracefully release all server resources. You generally don't need to call this manually.

func (*Server[state]) Start

func (server *Server[state]) Start(ctx context.Context, listener *net.Listener) error

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

func (server *Server[state]) StaticFiles(pattern string, dir string, files fs.ReadDirFS)

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

func (server *Server[state]) Use(
	middlewares ...Middleware[state],
) *Server[state]

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

func (server *Server[state]) UseStd(middlewares ...func(http.Handler) http.Handler) *Server[state]

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 *Server[state]) WithConfig(cfg *config.Config) *Server[state]

func (*Server[state]) WithDefaultLayout

func (server *Server[state]) WithDefaultLayout(layout templ.Component) *Server[state]

func (*Server[state]) WithErrorHandler

func (server *Server[state]) WithErrorHandler(errorHandler ErrorHandler) *Server[state]

func (*Server[state]) WithI18n

func (s *Server[state]) WithI18n(fs fs.FS) *Server[state]

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 *Server[state]) WithLogger(logger *slog.Logger) *Server[state]

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]

func (*Server[state]) WithSessionStore

func (server *Server[state]) WithSessionStore(store sessions.Store) *Server[state]

type State

type State interface {
	Close(ctx context.Context)
}

Jump to

Keyboard shortcuts

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