grafana/pkg/services/frontend/index.go
Ashley Harrison aaaeb13d32
FS: Add OpenFeature scaffolding and local setup (#117904)
* add openfeature handling + local env to frontend-service

* remove react18 manifest that was accidentally added

* fix and add some more unit tests

* review changes

* remove comment from frontend_service

* translations...
2026-02-13 18:14:36 +00:00

164 lines
4.9 KiB
Go

package frontend
import (
"embed"
"errors"
"fmt"
"html/template"
"net/http"
"syscall"
"github.com/grafana/grafana-app-sdk/logging"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/services/contexthandler"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
fswebassets "github.com/grafana/grafana/pkg/services/frontend/webassets"
"github.com/grafana/grafana/pkg/services/hooks"
"github.com/grafana/grafana/pkg/services/licensing"
"github.com/grafana/grafana/pkg/setting"
)
type IndexProvider struct {
log logging.Logger
index *template.Template
hooksService *hooks.HooksService
config *setting.Cfg
license licensing.Licensing
}
type IndexViewData struct {
IsDevelopmentEnv bool
Config *setting.Cfg // TODO: remove and get from request config?
AppTitle string // TODO: remove and get from request config?
AppSubUrl string // TODO: remove and get from request config?
Settings FSFrontendSettings
Assets dtos.EntryPointAssets // Includes CDN info
DefaultUser dtos.CurrentUser
// Nonce is a cryptographic identifier for use with Content Security Policy.
Nonce string
PublicDashboardAccessToken string
}
// Templates setup.
var (
//go:embed *.html
templatesFS embed.FS
// templates
htmlTemplates = template.Must(template.New("html").Delims("[[", "]]").ParseFS(templatesFS, `*.html`))
)
func NewIndexProvider(cfg *setting.Cfg, license licensing.Licensing, hooksService *hooks.HooksService) (*IndexProvider, error) {
t := htmlTemplates.Lookup("index.html")
if t == nil {
return nil, fmt.Errorf("missing index template")
}
logger := logging.DefaultLogger.With("logger", "index-provider")
// subset of frontend settings needed for the login page
// TODO what about enterprise settings here?
return &IndexProvider{
log: logger,
index: t,
hooksService: hooksService,
config: cfg,
license: license,
}, nil
}
func (p *IndexProvider) HandleRequest(writer http.ResponseWriter, request *http.Request) {
ctx, span := tracer.Start(request.Context(), "frontend.index.HandleRequest")
defer span.End()
if request.Method != "GET" {
writer.WriteHeader(http.StatusMethodNotAllowed)
return
}
requestConfig, err := FSRequestConfigFromContext(ctx)
if err != nil {
p.log.Error("unable to get request config", "err", err)
http.Error(writer, "Internal Server Error", http.StatusInternalServerError)
return
}
assetsManifest, err := fswebassets.GetWebAssets(ctx, p.config, p.license)
if err != nil {
p.log.Error("unable to get web assets", "err", err)
http.Error(writer, "Internal Server Error", http.StatusInternalServerError)
return
}
reqCtx := contexthandler.FromContext(ctx)
// make a copy of the settings
fsSettings := requestConfig.FSFrontendSettings
data := IndexViewData{
AppTitle: "Grafana",
AppSubUrl: p.config.AppSubURL,
IsDevelopmentEnv: p.config.Env == setting.Dev,
Assets: assetsManifest,
DefaultUser: dtos.CurrentUser{},
Nonce: reqCtx.RequestNonce,
PublicDashboardAccessToken: reqCtx.PublicDashboardAccessToken,
Settings: fsSettings,
}
// TODO -- reevaluate with mt authnz
// Check for login_error cookie and set a generic error message.
// The backend sets an encrypted cookie on oauth login failures that we can't read
// so we just show a generic error if the cookie is present.
if cookie, err := request.Cookie("login_error"); err == nil && cookie.Value != "" {
p.log.Info("request has login_error cookie")
// Defaults to a translation key that the frontend will resolve to a localized message
data.Settings.LoginError = p.config.OAuthLoginErrorMessage // TODO: get from request config
cookiePath := "/"
if p.config.AppSubURL != "" {
cookiePath = data.AppSubUrl
}
http.SetCookie(writer, &http.Cookie{
Name: "login_error",
Value: "",
Path: cookiePath,
MaxAge: -1,
HttpOnly: true,
Secure: p.config.CookieSecure,
SameSite: p.config.CookieSameSiteMode,
})
}
p.runIndexDataHooks(reqCtx, &data)
writer.Header().Set("Content-Type", "text/html; charset=UTF-8")
writer.Header().Set("Cache-Control", "no-store")
writer.WriteHeader(200)
if err := p.index.Execute(writer, &data); err != nil {
if errors.Is(err, syscall.EPIPE) { // Client has stopped listening.
return
}
panic(fmt.Sprintf("Error rendering index\n %s", err.Error()))
}
}
func (p *IndexProvider) runIndexDataHooks(reqCtx *contextmodel.ReqContext, data *IndexViewData) {
// Create a dummy struct to pass to the hooks, and then extract the data back out from it
legacyIndexViewData := dtos.IndexViewData{
Settings: &dtos.FrontendSettingsDTO{
BuildInfo: data.Settings.BuildInfo,
},
}
p.hooksService.RunIndexDataHooks(&legacyIndexViewData, reqCtx)
data.Settings.BuildInfo = legacyIndexViewData.Settings.BuildInfo
}