mirror of
https://github.com/mudler/LocalAI.git
synced 2025-05-20 02:24:59 +00:00

Makes the web app honour the `X-Forwarded-Prefix` HTTP request header that may be sent by a reverse-proxy in order to inform the app that its public routes contain a path prefix.
For instance this allows to serve the webapp via a reverse-proxy/ingress controller under a path prefix/sub path such as e.g. `/localai/` while still being able to use the regular LocalAI routes/paths without prefix when directly connecting to the LocalAI server.
Changes:
* Add new `StripPathPrefix` middleware to strip the path prefix (provided with the `X-Forwarded-Prefix` HTTP request header) from the request path prior to matching the HTTP route.
* Add a `BaseURL` utility function to build the base URL, honouring the `X-Forwarded-Prefix` HTTP request header.
* Generate the derived base URL into the HTML (`head.html` template) as `<base/>` tag.
* Make all webapp-internal URLs (within HTML+JS) relative in order to make the browser resolve them against the `<base/>` URL specified within each HTML page's header.
* Make font URLs within the CSS files relative to the CSS file.
* Generate redirect location URLs using the new `BaseURL` function.
* Use the new `BaseURL` function to generate absolute URLs within gallery JSON responses.
Closes #3095
TL;DR:
The header-based approach allows to move the path prefix configuration concern completely to the reverse-proxy/ingress as opposed to having to align the path prefix configuration between LocalAI, the reverse-proxy and potentially other internal LocalAI clients.
The gofiber swagger handler already supports path prefixes this way, see e2d9e9916d/swagger.go (L79)
Signed-off-by: Max Goltzsche <max.goltzsche@gmail.com>
104 lines
2.9 KiB
Go
104 lines
2.9 KiB
Go
package explorer
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"sort"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/mudler/LocalAI/core/explorer"
|
|
"github.com/mudler/LocalAI/core/http/utils"
|
|
"github.com/mudler/LocalAI/internal"
|
|
)
|
|
|
|
func Dashboard() func(*fiber.Ctx) error {
|
|
return func(c *fiber.Ctx) error {
|
|
summary := fiber.Map{
|
|
"Title": "LocalAI API - " + internal.PrintableVersion(),
|
|
"Version": internal.PrintableVersion(),
|
|
"BaseURL": utils.BaseURL(c),
|
|
}
|
|
|
|
if string(c.Context().Request.Header.ContentType()) == "application/json" || len(c.Accepts("html")) == 0 {
|
|
// The client expects a JSON response
|
|
return c.Status(fiber.StatusOK).JSON(summary)
|
|
} else {
|
|
// Render index
|
|
return c.Render("views/explorer", summary)
|
|
}
|
|
}
|
|
}
|
|
|
|
type AddNetworkRequest struct {
|
|
Token string `json:"token"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
type Network struct {
|
|
explorer.TokenData
|
|
Token string `json:"token"`
|
|
}
|
|
|
|
func ShowNetworks(db *explorer.Database) func(*fiber.Ctx) error {
|
|
return func(c *fiber.Ctx) error {
|
|
results := []Network{}
|
|
for _, token := range db.TokenList() {
|
|
networkData, exists := db.Get(token) // get the token data
|
|
hasWorkers := false
|
|
for _, cluster := range networkData.Clusters {
|
|
if len(cluster.Workers) > 0 {
|
|
hasWorkers = true
|
|
break
|
|
}
|
|
}
|
|
if exists && hasWorkers {
|
|
results = append(results, Network{TokenData: networkData, Token: token})
|
|
}
|
|
}
|
|
|
|
// order by number of clusters
|
|
sort.Slice(results, func(i, j int) bool {
|
|
return len(results[i].Clusters) > len(results[j].Clusters)
|
|
})
|
|
|
|
return c.JSON(results)
|
|
}
|
|
}
|
|
|
|
func AddNetwork(db *explorer.Database) func(*fiber.Ctx) error {
|
|
return func(c *fiber.Ctx) error {
|
|
request := new(AddNetworkRequest)
|
|
if err := c.BodyParser(request); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Cannot parse JSON"})
|
|
}
|
|
|
|
if request.Token == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Token is required"})
|
|
}
|
|
|
|
if request.Name == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Name is required"})
|
|
}
|
|
|
|
if request.Description == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Description is required"})
|
|
}
|
|
|
|
// TODO: check if token is valid, otherwise reject
|
|
// try to decode the token from base64
|
|
_, err := base64.StdEncoding.DecodeString(request.Token)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid token"})
|
|
}
|
|
|
|
if _, exists := db.Get(request.Token); exists {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Token already exists"})
|
|
}
|
|
err = db.Set(request.Token, explorer.TokenData{Name: request.Name, Description: request.Description})
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Cannot add token"})
|
|
}
|
|
|
|
return c.Status(fiber.StatusOK).JSON(fiber.Map{"message": "Token added"})
|
|
}
|
|
}
|