mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-02-11 18:44:01 +00:00
refactor!: serve the static frontend trough the backend (#520)
Co-authored-by: Alessandro (Ale) Segala <43508+ItalyPaleAle@users.noreply.github.com>
This commit is contained in:
@@ -1,10 +1,7 @@
|
||||
APP_ENV=production
|
||||
PUBLIC_APP_URL=http://localhost
|
||||
# /!\ If PUBLIC_APP_URL is not a localhost address, it must be HTTPS
|
||||
DB_PROVIDER=sqlite
|
||||
# MAXMIND_LICENSE_KEY=fixme # needed for IP geolocation in the audit log
|
||||
SQLITE_DB_PATH=data/pocket-id.db
|
||||
POSTGRES_CONNECTION_STRING=postgresql://postgres:postgres@localhost:5432/pocket-id
|
||||
UPLOAD_PATH=data/uploads
|
||||
PORT=8080
|
||||
HOST=0.0.0.0
|
||||
# Sample .env file for development
|
||||
# All environment variables can be found on https://pocket-id.org/docs/configuration/environment-variables
|
||||
|
||||
APP_ENV=development
|
||||
APP_URL=http://localhost:1411
|
||||
PORT=1411
|
||||
MAXMIND_LICENSE_KEY=your_license_key
|
||||
9
backend/frontend/frontend_excluded.go
Normal file
9
backend/frontend/frontend_excluded.go
Normal file
@@ -0,0 +1,9 @@
|
||||
//go:build exclude_frontend
|
||||
|
||||
package frontend
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
func RegisterFrontend(router *gin.Engine) error {
|
||||
return ErrFrontendNotIncluded
|
||||
}
|
||||
77
backend/frontend/frontend_included.go
Normal file
77
backend/frontend/frontend_included.go
Normal file
@@ -0,0 +1,77 @@
|
||||
//go:build !exclude_frontend
|
||||
|
||||
package frontend
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
//go:embed all:dist/*
|
||||
var frontendFS embed.FS
|
||||
|
||||
func RegisterFrontend(router *gin.Engine) error {
|
||||
distFS, err := fs.Sub(frontendFS, "dist")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create sub FS: %w", err)
|
||||
}
|
||||
|
||||
cacheMaxAge := time.Hour * 24
|
||||
fileServer := NewFileServerWithCaching(http.FS(distFS), int(cacheMaxAge.Seconds()))
|
||||
|
||||
router.NoRoute(func(c *gin.Context) {
|
||||
// Try to serve the requested file
|
||||
path := strings.TrimPrefix(c.Request.URL.Path, "/")
|
||||
if _, err := fs.Stat(distFS, path); os.IsNotExist(err) {
|
||||
// File doesn't exist, serve index.html instead
|
||||
c.Request.URL.Path = "/"
|
||||
}
|
||||
|
||||
fileServer.ServeHTTP(c.Writer, c.Request)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FileServerWithCaching wraps http.FileServer to add caching headers
|
||||
type FileServerWithCaching struct {
|
||||
root http.FileSystem
|
||||
lastModified time.Time
|
||||
cacheMaxAge int
|
||||
lastModifiedHeaderValue string
|
||||
cacheControlHeaderValue string
|
||||
}
|
||||
|
||||
func NewFileServerWithCaching(root http.FileSystem, maxAge int) *FileServerWithCaching {
|
||||
return &FileServerWithCaching{
|
||||
root: root,
|
||||
lastModified: time.Now(),
|
||||
cacheMaxAge: maxAge,
|
||||
lastModifiedHeaderValue: time.Now().UTC().Format(http.TimeFormat),
|
||||
cacheControlHeaderValue: fmt.Sprintf("public, max-age=%d", maxAge),
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FileServerWithCaching) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if the client has a cached version
|
||||
if ifModifiedSince := r.Header.Get("If-Modified-Since"); ifModifiedSince != "" {
|
||||
ifModifiedSinceTime, err := time.Parse(http.TimeFormat, ifModifiedSince)
|
||||
if err == nil && f.lastModified.Before(ifModifiedSinceTime.Add(1*time.Second)) {
|
||||
// Client's cached version is up to date
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Last-Modified", f.lastModifiedHeaderValue)
|
||||
w.Header().Set("Cache-Control", f.cacheControlHeaderValue)
|
||||
|
||||
http.FileServer(f.root).ServeHTTP(w, r)
|
||||
}
|
||||
5
backend/frontend/shared.go
Normal file
5
backend/frontend/shared.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package frontend
|
||||
|
||||
import "errors"
|
||||
|
||||
var ErrFrontendNotIncluded = errors.New("frontend is not included")
|
||||
@@ -2,12 +2,15 @@ package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/frontend"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
|
||||
"golang.org/x/time/rate"
|
||||
@@ -45,6 +48,10 @@ func initRouterInternal(db *gorm.DB, svc *services) (utils.Service, error) {
|
||||
r := gin.Default()
|
||||
r.Use(gin.Logger())
|
||||
|
||||
if !common.EnvConfig.TrustProxy {
|
||||
_ = r.SetTrustedProxies(nil)
|
||||
}
|
||||
|
||||
if common.EnvConfig.TracingEnabled {
|
||||
r.Use(otelgin.Middleware("pocket-id-backend"))
|
||||
}
|
||||
@@ -55,6 +62,13 @@ func initRouterInternal(db *gorm.DB, svc *services) (utils.Service, error) {
|
||||
r.Use(middleware.NewCorsMiddleware().Add())
|
||||
r.Use(middleware.NewErrorHandlerMiddleware().Add())
|
||||
|
||||
err := frontend.RegisterFrontend(r)
|
||||
if errors.Is(err, frontend.ErrFrontendNotIncluded) {
|
||||
log.Println("Frontend is not included in the build. Skipping frontend registration.")
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("failed to register frontend: %w", err)
|
||||
}
|
||||
|
||||
// Initialize middleware for specific routes
|
||||
authMiddleware := middleware.NewAuthMiddleware(svc.apiKeyService, svc.userService, svc.jwtService)
|
||||
fileSizeLimitMiddleware := middleware.NewFileSizeLimitMiddleware()
|
||||
|
||||
@@ -25,19 +25,20 @@ const (
|
||||
|
||||
type EnvConfigSchema struct {
|
||||
AppEnv string `env:"APP_ENV"`
|
||||
AppURL string `env:"PUBLIC_APP_URL"`
|
||||
AppURL string `env:"APP_URL"`
|
||||
DbProvider DbProvider `env:"DB_PROVIDER"`
|
||||
DbConnectionString string `env:"DB_CONNECTION_STRING"`
|
||||
UploadPath string `env:"UPLOAD_PATH"`
|
||||
KeysPath string `env:"KEYS_PATH"`
|
||||
Port string `env:"BACKEND_PORT"`
|
||||
Port string `env:"PORT"`
|
||||
Host string `env:"HOST"`
|
||||
MaxMindLicenseKey string `env:"MAXMIND_LICENSE_KEY"`
|
||||
GeoLiteDBPath string `env:"GEOLITE_DB_PATH"`
|
||||
GeoLiteDBUrl string `env:"GEOLITE_DB_URL"`
|
||||
UiConfigDisabled bool `env:"PUBLIC_UI_CONFIG_DISABLED"`
|
||||
UiConfigDisabled bool `env:"UI_CONFIG_DISABLED"`
|
||||
MetricsEnabled bool `env:"METRICS_ENABLED"`
|
||||
TracingEnabled bool `env:"TRACING_ENABLED"`
|
||||
TrustProxy bool `env:"TRUST_PROXY"`
|
||||
}
|
||||
|
||||
var EnvConfig = &EnvConfigSchema{
|
||||
@@ -46,8 +47,8 @@ var EnvConfig = &EnvConfigSchema{
|
||||
DbConnectionString: "file:data/pocket-id.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(2500)&_txlock=immediate",
|
||||
UploadPath: "data/uploads",
|
||||
KeysPath: "data/keys",
|
||||
AppURL: "http://localhost",
|
||||
Port: "8080",
|
||||
AppURL: "http://localhost:1411",
|
||||
Port: "1411",
|
||||
Host: "0.0.0.0",
|
||||
MaxMindLicenseKey: "",
|
||||
GeoLiteDBPath: "data/GeoLite2-City.mmdb",
|
||||
@@ -55,6 +56,7 @@ var EnvConfig = &EnvConfigSchema{
|
||||
UiConfigDisabled: false,
|
||||
MetricsEnabled: false,
|
||||
TracingEnabled: false,
|
||||
TrustProxy: false,
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -78,9 +80,9 @@ func init() {
|
||||
|
||||
parsedAppUrl, err := url.Parse(EnvConfig.AppURL)
|
||||
if err != nil {
|
||||
log.Fatal("PUBLIC_APP_URL is not a valid URL")
|
||||
log.Fatal("APP_URL is not a valid URL")
|
||||
}
|
||||
if parsedAppUrl.Path != "" {
|
||||
log.Fatal("PUBLIC_APP_URL must not contain a path")
|
||||
log.Fatal("APP_URL must not contain a path")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,13 @@ func (acc *AppConfigController) listAppConfigHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Manually add uiConfigDisabled which isn't in the database but defined with an environment variable
|
||||
configVariablesDto = append(configVariablesDto, dto.PublicAppConfigVariableDto{
|
||||
Key: "uiConfigDisabled",
|
||||
Value: strconv.FormatBool(common.EnvConfig.UiConfigDisabled),
|
||||
Type: "boolean",
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, configVariablesDto)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user