refactor and better auth verification

This commit is contained in:
d34dscene
2024-08-19 00:56:41 +02:00
parent 4048027c2d
commit d0615e3114
22 changed files with 695 additions and 625 deletions

View File

@@ -3,11 +3,10 @@ package api
import (
"time"
"github.com/MizuchiLabs/mantrae/pkg/util"
"github.com/golang-jwt/jwt/v5"
)
var jwtKey = []byte("your_secret_key")
type Claims struct {
Username string `json:"username"`
jwt.RegisteredClaims
@@ -24,17 +23,26 @@ func GenerateJWT(username string) (string, error) {
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtKey)
var secret util.Credentials
if err := secret.GetCreds(); err != nil {
return "", err
}
return token.SignedString(secret.Secret)
}
// ValidateJWT validates a JWT token
func ValidateJWT(tokenString string) (*Claims, error) {
claims := &Claims{}
var secret util.Credentials
if err := secret.GetCreds(); err != nil {
return nil, err
}
token, err := jwt.ParseWithClaims(
tokenString,
claims,
func(token *jwt.Token) (interface{}, error) {
return jwtKey, nil
return secret.Secret, nil
},
)
if err != nil {

View File

@@ -6,9 +6,9 @@ import (
"fmt"
"net/http"
"strings"
"time"
"github.com/MizuchiLabs/mantrae/util"
"github.com/MizuchiLabs/mantrae/pkg/traefik"
"github.com/MizuchiLabs/mantrae/pkg/util"
)
// Helper function to write JSON response
@@ -49,15 +49,28 @@ func Login(w http.ResponseWriter, r *http.Request) {
return
}
writeJSON(w, map[string]string{
"token": token,
"expiry": time.Now().Add(168 * time.Hour).Format(time.RFC3339),
})
writeJSON(w, map[string]string{"token": token})
}
func VerifyToken(w http.ResponseWriter, r *http.Request) {
tokenString := r.Header.Get("Authorization")[7:]
if tokenString == "" {
http.Error(w, "token cannot be empty", http.StatusBadRequest)
return
}
_, err := ValidateJWT(tokenString)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusOK)
}
// CreateProfile creates a new profile
func CreateProfile(w http.ResponseWriter, r *http.Request) {
var profile util.Profile
var profile traefik.Profile
if err := json.NewDecoder(r.Body).Decode(&profile); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
@@ -68,14 +81,14 @@ func CreateProfile(w http.ResponseWriter, r *http.Request) {
return
}
profiles, err := util.LoadProfiles()
profiles, err := traefik.LoadProfiles()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
profiles = append(profiles, profile)
if err := util.SaveProfiles(profiles); err != nil {
if err := traefik.SaveProfiles(profiles); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -84,7 +97,7 @@ func CreateProfile(w http.ResponseWriter, r *http.Request) {
// GetProfiles returns all profiles
func GetProfiles(w http.ResponseWriter, r *http.Request) {
profiles, err := util.LoadProfiles()
profiles, err := traefik.LoadProfiles()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -95,7 +108,7 @@ func GetProfiles(w http.ResponseWriter, r *http.Request) {
// UpdateProfile updates a single profile
func UpdateProfile(w http.ResponseWriter, r *http.Request) {
var updatedProfile util.Profile
var updatedProfile traefik.Profile
if err := json.NewDecoder(r.Body).Decode(&updatedProfile); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
@@ -111,7 +124,7 @@ func UpdateProfile(w http.ResponseWriter, r *http.Request) {
return
}
profiles, err := util.LoadProfiles()
profiles, err := traefik.LoadProfiles()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -120,7 +133,7 @@ func UpdateProfile(w http.ResponseWriter, r *http.Request) {
for i, profile := range profiles {
if profile.Name == r.PathValue("name") {
profiles[i] = updatedProfile
if err := util.SaveProfiles(profiles); err != nil {
if err := traefik.SaveProfiles(profiles); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -133,7 +146,7 @@ func UpdateProfile(w http.ResponseWriter, r *http.Request) {
// DeleteProfile deletes a single profile
func DeleteProfile(w http.ResponseWriter, r *http.Request) {
profiles, err := util.LoadProfiles()
profiles, err := traefik.LoadProfiles()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -142,7 +155,7 @@ func DeleteProfile(w http.ResponseWriter, r *http.Request) {
for i, profile := range profiles {
if profile.Name == r.PathValue("name") {
profiles = append(profiles[:i], profiles[i+1:]...)
if err := util.SaveProfiles(profiles); err != nil {
if err := traefik.SaveProfiles(profiles); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -155,7 +168,7 @@ func DeleteProfile(w http.ResponseWriter, r *http.Request) {
// UpdateRouter updates or creates a router
func UpdateRouter(w http.ResponseWriter, r *http.Request) {
var router util.Router
var router traefik.Router
if err := json.NewDecoder(r.Body).Decode(&router); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
@@ -173,7 +186,7 @@ func UpdateRouter(w http.ResponseWriter, r *http.Request) {
return
}
profiles, err := util.LoadProfiles()
profiles, err := traefik.LoadProfiles()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -182,10 +195,10 @@ func UpdateRouter(w http.ResponseWriter, r *http.Request) {
for i, profile := range profiles {
if strings.EqualFold(profile.Name, profileName) {
if routerName != router.Name {
delete(profiles[i].Instance.Dynamic.Routers, routerName)
delete(profiles[i].Client.Dynamic.Routers, routerName)
}
profiles[i].Instance.Dynamic.Routers[router.Name] = router
if err := util.SaveProfiles(profiles); err != nil {
profiles[i].Client.Dynamic.Routers[router.Name] = router
if err := traefik.SaveProfiles(profiles); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -198,7 +211,7 @@ func UpdateRouter(w http.ResponseWriter, r *http.Request) {
// DeleteRouter deletes a single router and it's services
func DeleteRouter(w http.ResponseWriter, r *http.Request) {
profiles, err := util.LoadProfiles()
profiles, err := traefik.LoadProfiles()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -206,9 +219,9 @@ func DeleteRouter(w http.ResponseWriter, r *http.Request) {
for i, profile := range profiles {
if profile.Name == r.PathValue("profile") {
delete(profiles[i].Instance.Dynamic.Routers, r.PathValue("router"))
delete(profiles[i].Instance.Dynamic.Services, r.PathValue("router"))
if err := util.SaveProfiles(profiles); err != nil {
delete(profiles[i].Client.Dynamic.Routers, r.PathValue("router"))
delete(profiles[i].Client.Dynamic.Services, r.PathValue("router"))
if err := traefik.SaveProfiles(profiles); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -221,7 +234,7 @@ func DeleteRouter(w http.ResponseWriter, r *http.Request) {
// UpdateService updates or creates a service
func UpdateService(w http.ResponseWriter, r *http.Request) {
var service util.Service
var service traefik.Service
if err := json.NewDecoder(r.Body).Decode(&service); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
@@ -239,7 +252,7 @@ func UpdateService(w http.ResponseWriter, r *http.Request) {
return
}
profiles, err := util.LoadProfiles()
profiles, err := traefik.LoadProfiles()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -248,10 +261,10 @@ func UpdateService(w http.ResponseWriter, r *http.Request) {
for i, profile := range profiles {
if strings.EqualFold(profile.Name, r.PathValue("profile")) {
if serviceName != service.Name {
delete(profiles[i].Instance.Dynamic.Services, serviceName)
delete(profiles[i].Client.Dynamic.Services, serviceName)
}
profiles[i].Instance.Dynamic.Services[service.Name] = service
if err := util.SaveProfiles(profiles); err != nil {
profiles[i].Client.Dynamic.Services[service.Name] = service
if err := traefik.SaveProfiles(profiles); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -264,7 +277,7 @@ func UpdateService(w http.ResponseWriter, r *http.Request) {
// DeleteService deletes a single service and its router
func DeleteService(w http.ResponseWriter, r *http.Request) {
profiles, err := util.LoadProfiles()
profiles, err := traefik.LoadProfiles()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -272,9 +285,9 @@ func DeleteService(w http.ResponseWriter, r *http.Request) {
for i, profile := range profiles {
if profile.Name == r.PathValue("profile") {
delete(profiles[i].Instance.Dynamic.Services, r.PathValue("service"))
delete(profiles[i].Instance.Dynamic.Routers, r.PathValue("service"))
if err := util.SaveProfiles(profiles); err != nil {
delete(profiles[i].Client.Dynamic.Services, r.PathValue("service"))
delete(profiles[i].Client.Dynamic.Routers, r.PathValue("service"))
if err := traefik.SaveProfiles(profiles); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -287,7 +300,7 @@ func DeleteService(w http.ResponseWriter, r *http.Request) {
// UpdateMiddleware updates or creates a middleware
func UpdateMiddleware(w http.ResponseWriter, r *http.Request) {
var middleware util.Middleware
var middleware traefik.Middleware
if err := json.NewDecoder(r.Body).Decode(&middleware); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
@@ -300,7 +313,7 @@ func UpdateMiddleware(w http.ResponseWriter, r *http.Request) {
return
}
profiles, err := util.LoadProfiles()
profiles, err := traefik.LoadProfiles()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -309,10 +322,10 @@ func UpdateMiddleware(w http.ResponseWriter, r *http.Request) {
for i, profile := range profiles {
if strings.EqualFold(profile.Name, profileName) {
if middlewareName != middleware.Name {
delete(profiles[i].Instance.Dynamic.Middlewares, middlewareName)
delete(profiles[i].Client.Dynamic.Middlewares, middlewareName)
}
profiles[i].Instance.Dynamic.Middlewares[middleware.Name] = middleware
if err := util.SaveProfiles(profiles); err != nil {
profiles[i].Client.Dynamic.Middlewares[middleware.Name] = middleware
if err := traefik.SaveProfiles(profiles); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -325,7 +338,7 @@ func UpdateMiddleware(w http.ResponseWriter, r *http.Request) {
// DeleteMiddleware deletes a single middleware and it's services
func DeleteMiddleware(w http.ResponseWriter, r *http.Request) {
profiles, err := util.LoadProfiles()
profiles, err := traefik.LoadProfiles()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -333,8 +346,8 @@ func DeleteMiddleware(w http.ResponseWriter, r *http.Request) {
for i, profile := range profiles {
if profile.Name == r.PathValue("profile") {
delete(profiles[i].Instance.Dynamic.Middlewares, r.PathValue("middleware"))
if err := util.SaveProfiles(profiles); err != nil {
delete(profiles[i].Client.Dynamic.Middlewares, r.PathValue("middleware"))
if err := traefik.SaveProfiles(profiles); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -347,7 +360,7 @@ func DeleteMiddleware(w http.ResponseWriter, r *http.Request) {
// GetConfig returns the traefik config for a single profile
func GetConfig(w http.ResponseWriter, r *http.Request) {
profiles, err := util.LoadProfiles()
profiles, err := traefik.LoadProfiles()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -359,7 +372,7 @@ func GetConfig(w http.ResponseWriter, r *http.Request) {
w.Header().
Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.yaml", profile.Name))
yamlConfig, err := util.ParseConfig(profile.Instance.Dynamic)
yamlConfig, err := traefik.GenerateConfig(profile.Client.Dynamic)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return

View File

@@ -6,7 +6,7 @@ import (
"strings"
"time"
"github.com/MizuchiLabs/mantrae/util"
"github.com/MizuchiLabs/mantrae/pkg/util"
)
// statusRecorder is a wrapper around http.ResponseWriter to capture the status code

View File

@@ -6,6 +6,8 @@ func Routes() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("POST /api/login", Login)
mux.HandleFunc("POST /api/verify", VerifyToken)
mux.HandleFunc("POST /api/profiles", JWT(CreateProfile))
mux.HandleFunc("GET /api/profiles", JWT(GetProfiles))
mux.HandleFunc("PUT /api/profiles/{name}", JWT(UpdateProfile))

45
main.go
View File

@@ -1,18 +1,20 @@
package main
import (
"context"
"embed"
"flag"
"io/fs"
"log"
"log/slog"
"net/http"
"os"
"os/signal"
"strconv"
"sync"
"time"
"github.com/MizuchiLabs/mantrae/api"
"github.com/MizuchiLabs/mantrae/util"
"github.com/MizuchiLabs/mantrae/internal/api"
"github.com/MizuchiLabs/mantrae/pkg/traefik"
"github.com/MizuchiLabs/mantrae/pkg/util"
"github.com/lmittmann/tint"
)
@@ -26,7 +28,7 @@ func init() {
if err := util.GenerateCreds(); err != nil {
slog.Error("Failed to generate creds", "error", err)
}
go util.FetchTraefikConfig()
go traefik.GetTraefikConfig()
}
func main() {
@@ -45,14 +47,31 @@ func main() {
mux.Handle("/", http.FileServer(http.FS(staticContent)))
// Start the background sync process
wg := &sync.WaitGroup{}
wg.Add(1)
go util.Sync(wg)
go traefik.Sync()
log.Println("Listening on port", *port)
if err := http.ListenAndServe(":"+strconv.Itoa(*port), middle(mux)); err != nil {
slog.Error("ListenAndServe", "error", err)
return
srv := &http.Server{
Addr: ":" + strconv.Itoa(*port),
Handler: middle(mux),
}
wg.Wait()
slog.Info("Listening on port", "port", *port)
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
slog.Error("ListenAndServe", "error", err)
}
}()
// Wait for interrupt signal to gracefully shutdown the server
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
<-quit
slog.Info("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
slog.Error("Server forced to shutdown:", "error", err)
}
slog.Info("Server exiting")
}

View File

@@ -1,4 +1,4 @@
package util
package traefik
import (
"crypto/tls"
@@ -27,9 +27,9 @@ const (
type BaseFields struct {
Name string `json:"name,omitempty"`
Provider string `json:"provider,omitempty"`
Status string `json:"status,omitempty"`
Type string `json:"type,omitempty"`
Status string `json:"status,omitempty"`
Provider string `json:"provider,omitempty"`
}
type HTTPRouter struct {
@@ -106,8 +106,8 @@ func (r UDPRouter) ToRouter() Router {
}
}
func fetchRouters[T Routerable](instance Instance, endpoint string) []Router {
body, err := get(instance, endpoint)
func getRouters[T Routerable](c Client, endpoint string) []Router {
body, err := c.fetch(endpoint)
if err != nil {
slog.Error("Failed to get routers", "error", err)
return nil
@@ -199,8 +199,8 @@ func (s UDPService) ToService() Service {
}
}
func fetchServices[T Serviceable](instance Instance, endpoint string) []Service {
body, err := get(instance, endpoint)
func getServices[T Serviceable](c Client, endpoint string) []Service {
body, err := c.fetch(endpoint)
if err != nil {
slog.Error("Failed to get services", "error", err)
return nil
@@ -309,8 +309,8 @@ func (m TCPMiddleware) ToMiddleware() Middleware {
}
}
func fetchMiddlewares[T Middlewareable](instance Instance, endpoint string) []Middleware {
body, err := get(instance, endpoint)
func getMiddlewares[T Middlewareable](c Client, endpoint string) []Middleware {
body, err := c.fetch(endpoint)
if err != nil {
slog.Error("Failed to get middlewares", "error", err)
return nil
@@ -331,8 +331,111 @@ func fetchMiddlewares[T Middlewareable](instance Instance, endpoint string) []Mi
return middlewares
}
func get(instance Instance, endpoint string) (io.ReadCloser, error) {
apiURL := instance.URL + endpoint
func GetTraefikConfig() {
profiles, err := LoadProfiles()
if err != nil {
slog.Error("Failed to load profiles", "error", err)
return
}
for idx, profile := range profiles {
c := profile.Client
d := Dynamic{
Routers: make(map[string]Router),
Services: make(map[string]Service),
Middlewares: make(map[string]Middleware),
}
// Retrieve routers
var tRouter []Router
tRouter = append(tRouter, getRouters[HTTPRouter](c, HTTPRouterAPI)...)
tRouter = append(tRouter, getRouters[TCPRouter](c, TCPRouterAPI)...)
tRouter = append(tRouter, getRouters[UDPRouter](c, UDPRouterAPI)...)
for _, r := range tRouter {
d.Routers[r.Name] = r
}
for _, r := range profile.Client.Dynamic.Routers {
d.Routers[r.Name] = r
}
// Retrieve services
var tServices []Service
tServices = append(tServices, getServices[HTTPService](c, HTTPServiceAPI)...)
tServices = append(tServices, getServices[TCPService](c, TCPServiceAPI)...)
tServices = append(tServices, getServices[UDPService](c, UDPServiceAPI)...)
for _, s := range tServices {
d.Services[s.Name] = s
}
for _, s := range profile.Client.Dynamic.Services {
d.Services[s.Name] = s
}
// Fetch middlewares
var tMiddlewares []Middleware
tMiddlewares = append(
tMiddlewares,
getMiddlewares[HTTPMiddleware](c, HTTPMiddlewaresAPI)...)
tMiddlewares = append(
tMiddlewares,
getMiddlewares[TCPMiddleware](c, TCPMiddlewaresAPI)...)
for _, m := range tMiddlewares {
d.Middlewares[m.Name] = m
}
for _, m := range profile.Client.Dynamic.Middlewares {
d.Middlewares[m.Name] = m
}
// Retrieve entrypoints
entrypoints, err := c.fetch(EntrypointsAPI)
if err != nil {
slog.Error("Failed to get entrypoints", "error", err)
return
}
defer entrypoints.Close()
if err = json.NewDecoder(entrypoints).Decode(&d.Entrypoints); err != nil {
slog.Error("Failed to decode entrypoints", "error", err)
return
}
// Fetch version
version, err := c.fetch(VersionAPI)
if err != nil {
slog.Error("Failed to get version", "error", err)
return
}
defer version.Close()
var v struct {
Version string `json:"version"`
}
if err = json.NewDecoder(version).Decode(&v); err != nil {
slog.Error("Failed to decode version", "error", err)
return
}
d.Version = v.Version
profiles[idx].Client.Dynamic = d
}
if err := SaveProfiles(profiles); err != nil {
slog.Error("Failed to save profiles", "error", err)
}
}
// Sync periodically syncs the Traefik configuration
func Sync() {
ticker := time.NewTicker(time.Second * 60)
defer ticker.Stop()
for range ticker.C {
GetTraefikConfig()
}
}
func (c *Client) fetch(endpoint string) (io.ReadCloser, error) {
apiURL := c.URL + endpoint
client := http.Client{
Timeout: time.Second * 10,
Transport: &http.Transport{
@@ -345,8 +448,8 @@ func get(instance Instance, endpoint string) (io.ReadCloser, error) {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
if instance.Username != "" && instance.Password != "" {
req.SetBasicAuth(instance.Username, instance.Password)
if c.Username != "" && c.Password != "" {
req.SetBasicAuth(c.Username, c.Password)
}
resp, err := client.Do(req)

165
pkg/traefik/config.go Normal file
View File

@@ -0,0 +1,165 @@
package traefik
import (
"strings"
"github.com/traefik/genconf/dynamic"
ttls "github.com/traefik/genconf/dynamic/tls"
"sigs.k8s.io/yaml"
)
func GenerateConfig(d Dynamic) ([]byte, error) {
config := &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: make(map[string]*dynamic.Router),
Middlewares: make(map[string]*dynamic.Middleware),
Services: make(map[string]*dynamic.Service),
ServersTransports: make(map[string]*dynamic.ServersTransport),
},
TCP: &dynamic.TCPConfiguration{
Routers: make(map[string]*dynamic.TCPRouter),
Services: make(map[string]*dynamic.TCPService),
Middlewares: make(map[string]*dynamic.TCPMiddleware),
},
UDP: &dynamic.UDPConfiguration{
Routers: make(map[string]*dynamic.UDPRouter),
Services: make(map[string]*dynamic.UDPService),
},
TLS: &dynamic.TLSConfiguration{
Stores: make(map[string]ttls.Store),
Options: make(map[string]ttls.Options),
},
}
for _, router := range d.Routers {
// Only add routers by our provider
if router.Provider == "http" {
switch router.RouterType {
case "http":
config.HTTP.Routers[router.Service] = &dynamic.Router{
EntryPoints: router.Entrypoints,
Middlewares: router.Middlewares,
Service: router.Service,
Rule: router.Rule,
// Priority: int(router.Priority.Int64()),
TLS: router.TLS,
}
case "tcp":
config.TCP.Routers[router.Service] = &dynamic.TCPRouter{
EntryPoints: router.Entrypoints,
Middlewares: router.Middlewares,
Service: router.Service,
// Priority: int(router.Priority.Int64()),
Rule: router.Rule,
}
case "udp":
config.UDP.Routers[router.Service] = &dynamic.UDPRouter{
EntryPoints: router.Entrypoints,
Service: router.Service,
}
}
}
}
for _, service := range d.Services {
// Only add services by our provider
if service.Provider == "http" {
name := strings.Split(service.Name, "@")[0]
switch service.ServiceType {
case "http":
config.HTTP.Services[name] = &dynamic.Service{
LoadBalancer: service.LoadBalancer,
Weighted: service.Weighted,
Mirroring: service.Mirroring,
Failover: service.Failover,
}
case "tcp":
config.TCP.Services[name], _ = convertService(service)
case "udp":
_, config.UDP.Services[name] = convertService(service)
}
}
}
for _, middleware := range d.Middlewares {
if middleware.Provider == "http" {
config.HTTP.Middlewares[middleware.Name] = &dynamic.Middleware{
AddPrefix: middleware.AddPrefix,
StripPrefix: middleware.StripPrefix,
StripPrefixRegex: middleware.StripPrefixRegex,
ReplacePath: middleware.ReplacePath,
ReplacePathRegex: middleware.ReplacePathRegex,
Chain: middleware.Chain,
IPWhiteList: middleware.IPWhiteList,
IPAllowList: middleware.IPAllowList,
Headers: middleware.Headers,
Errors: middleware.Errors,
RateLimit: middleware.RateLimit,
RedirectRegex: middleware.RedirectRegex,
RedirectScheme: middleware.RedirectScheme,
BasicAuth: middleware.BasicAuth,
DigestAuth: middleware.DigestAuth,
ForwardAuth: middleware.ForwardAuth,
InFlightReq: middleware.InFlightReq,
Buffering: middleware.Buffering,
CircuitBreaker: middleware.CircuitBreaker,
Compress: middleware.Compress,
PassTLSClientCert: middleware.PassTLSClientCert,
Retry: middleware.Retry,
ContentType: middleware.ContentType,
Plugin: middleware.Plugin,
}
config.TCP.Middlewares[middleware.Name] = &dynamic.TCPMiddleware{
InFlightConn: middleware.InFlightConn,
}
}
}
// Remove empty configurations
if len(config.HTTP.Routers) == 0 && len(config.HTTP.Services) == 0 &&
len(config.HTTP.Middlewares) == 0 {
config.HTTP = nil
}
if len(config.TCP.Routers) == 0 && len(config.TCP.Services) == 0 &&
len(config.TCP.Middlewares) == 0 {
config.TCP = nil
}
if len(config.UDP.Routers) == 0 && len(config.UDP.Services) == 0 {
config.UDP = nil
}
if len(config.TLS.Stores) == 0 && len(config.TLS.Options) == 0 {
config.TLS = nil
}
yamlConfig, err := yaml.Marshal(config)
if err != nil {
return nil, err
}
return yamlConfig, nil
}
func convertService(service Service) (*dynamic.TCPService, *dynamic.UDPService) {
var tcpServer []dynamic.TCPServer
var udpServer []dynamic.UDPServer
for _, lb := range service.LoadBalancer.Servers {
if lb.URL != "" {
tcpServer = append(tcpServer, dynamic.TCPServer{
Address: lb.URL,
})
udpServer = append(udpServer, dynamic.UDPServer{
Address: lb.URL,
})
}
}
tcpService := &dynamic.TCPService{
LoadBalancer: &dynamic.TCPServersLoadBalancer{
Servers: tcpServer,
},
}
udpService := &dynamic.UDPService{
LoadBalancer: &dynamic.UDPServersLoadBalancer{
Servers: udpServer,
},
}
return tcpService, udpService
}

View File

@@ -1,12 +1,27 @@
package util
// Package traefik provides a client for the Traefik API
// Here are all the models used to convert between the API and the UI
package traefik
import (
"github.com/traefik/genconf/dynamic"
)
import "github.com/traefik/genconf/dynamic"
type Credentials struct {
Username string `json:"username"`
Password string `json:"password"`
type Profile struct {
Name string `json:"name"`
Client Client `json:"client,omitempty"`
}
type Client struct {
URL string `json:"url"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
Dynamic Dynamic `json:"dynamic,omitempty"`
}
type Dynamic struct {
Entrypoints []Entrypoint `json:"entrypoints,omitempty"`
Routers map[string]Router `json:"routers,omitempty"`
Services map[string]Service `json:"services,omitempty"`
Middlewares map[string]Middleware `json:"middlewares,omitempty"`
Version string `json:"version,omitempty"`
}
type Entrypoint struct {
@@ -98,23 +113,3 @@ type Middleware struct {
TCPIPWhiteList *dynamic.TCPIPWhiteList `json:"tcpIpWhiteList,omitempty"`
TCPIPAllowList *dynamic.TCPIPAllowList `json:"tcpIpAllowList,omitempty"`
}
type Dynamic struct {
Entrypoints []Entrypoint `json:"entrypoints,omitempty"`
Routers map[string]Router `json:"routers,omitempty"`
Services map[string]Service `json:"services,omitempty"`
Middlewares map[string]Middleware `json:"middlewares,omitempty"`
Version string `json:"version,omitempty"`
}
type Instance struct {
URL string `json:"url"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
Dynamic Dynamic `json:"dynamic,omitempty"`
}
type Profile struct {
Name string `json:"name"`
Instance Instance `json:"instance,omitempty"`
}

133
pkg/traefik/profile.go Normal file
View File

@@ -0,0 +1,133 @@
package traefik
import (
"encoding/json"
"fmt"
"io"
"log"
"log/slog"
"os"
"path/filepath"
"strings"
"sync"
)
var rwMutex sync.RWMutex
func profilePath() string {
cwd, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
newFilePath := filepath.Join(cwd, "profiles.json")
return newFilePath
}
func defaultProfile() Profile {
return Profile{
Name: "default",
Client: Client{
URL: "http://127.0.0.1:8080",
Username: "",
Password: "",
Dynamic: Dynamic{},
},
}
}
func LoadProfiles() ([]Profile, error) {
rwMutex.RLock()
defer rwMutex.RUnlock()
profiles := []Profile{}
if _, err := os.Stat(profilePath()); os.IsNotExist(err) {
profiles = append(profiles, defaultProfile())
if err := SaveProfiles(profiles); err != nil {
slog.Error("Failed to save profiles", "error", err)
}
return profiles, nil
}
file, err := os.ReadFile(profilePath())
if err != nil {
return []Profile{}, fmt.Errorf("failed to read profiles file: %w", err)
}
if err := json.Unmarshal(file, &profiles); err != nil {
return []Profile{}, fmt.Errorf("failed to unmarshal profiles: %w", err)
}
return profiles, nil
}
func SaveProfiles(profiles []Profile) error {
rwMutex.Lock()
defer rwMutex.Unlock()
tmpFile, err := os.CreateTemp(os.TempDir(), "profiles-*.json")
if err != nil {
return fmt.Errorf("failed to create temp file: %w", err)
}
defer os.Remove(tmpFile.Name())
profileBytes, err := json.Marshal(profiles)
if err != nil {
return fmt.Errorf("failed to marshal profiles: %w", err)
}
_, err = tmpFile.Write(profileBytes)
if err != nil {
return fmt.Errorf("failed to write profiles: %w", err)
}
if err := tmpFile.Sync(); err != nil {
return fmt.Errorf("failed to sync temp file: %w", err)
}
tmpFile.Close()
if err := Move(tmpFile.Name(), profilePath()); err != nil {
return fmt.Errorf("failed to move temp file: %w", err)
}
return nil
}
func Move(source, destination string) error {
err := os.Rename(source, destination)
if err != nil && strings.Contains(err.Error(), "invalid cross-device link") {
return moveCrossDevice(source, destination)
}
return err
}
func moveCrossDevice(source, destination string) error {
src, err := os.Open(source)
if err != nil {
return fmt.Errorf("failed to open source file: %w", err)
}
dst, err := os.Create(destination)
if err != nil {
src.Close()
return fmt.Errorf("failed to create destination file: %w", err)
}
_, err = io.Copy(dst, src)
src.Close()
dst.Close()
if err != nil {
return fmt.Errorf("failed to copy file: %w", err)
}
fi, err := os.Stat(source)
if err != nil {
os.Remove(destination)
return fmt.Errorf("failed to stat source file: %w", err)
}
err = os.Chmod(destination, fi.Mode())
if err != nil {
os.Remove(destination)
return fmt.Errorf("failed to chmod destination file: %w", err)
}
os.Remove(source)
return nil
}

View File

@@ -1,4 +1,4 @@
package util
package traefik
import (
"fmt"
@@ -12,8 +12,8 @@ func (p *Profile) Verify() error {
return fmt.Errorf("profile name cannot be empty")
}
if p.Instance.URL != "" {
if !isValidURL(p.Instance.URL) {
if p.Client.URL != "" {
if !isValidURL(p.Client.URL) {
return fmt.Errorf("invalid url")
}
} else {

82
pkg/util/secrets.go Normal file
View File

@@ -0,0 +1,82 @@
package util
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"log/slog"
"os"
"path/filepath"
)
type Credentials struct {
Username string `json:"username"`
Password string `json:"password"`
Secret []byte `json:"secret"`
}
func randomPassword(length int) []byte {
bytes := make([]byte, length)
if _, err := rand.Read(bytes); err != nil {
return nil
}
return bytes
}
func GenerateCreds() error {
cwd, err := os.Getwd()
if err != nil {
return err
}
credsPath := filepath.Join(cwd, "creds.json")
if _, err = os.Stat(credsPath); os.IsNotExist(err) {
username := "admin"
password := randomPassword(32)
jsonCreds, err := json.MarshalIndent(Credentials{
Username: username,
Password: base64.StdEncoding.EncodeToString(password),
Secret: randomPassword(64),
}, "", " ")
if err != nil {
return err
}
if err := os.WriteFile(credsPath, jsonCreds, 0644); err != nil {
return err
}
slog.Info("Generated new credentials", "username", username, "password", password)
}
return nil
}
// GetCreds retrieves the credentials from the creds.json file or generates a new one
func (c *Credentials) GetCreds() error {
cwd, err := os.Getwd()
if err != nil {
return err
}
credsPath := filepath.Join(cwd, "creds.json")
file, err := os.ReadFile(credsPath)
if err != nil {
return err
}
if err := json.Unmarshal(file, &c); err != nil {
return err
}
if c.Username == "" || c.Password == "" || c.Secret == nil {
slog.Info("Invalid credentials, regenerating...")
if err := os.Remove(credsPath); err != nil {
return err
}
if err := GenerateCreds(); err != nil {
return err
}
}
return nil
}

View File

@@ -1,452 +0,0 @@
package util
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"log"
"log/slog"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/traefik/genconf/dynamic"
ttls "github.com/traefik/genconf/dynamic/tls"
"sigs.k8s.io/yaml"
)
var rwMutex sync.RWMutex
func profilePath() string {
cwd, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
newFilePath := filepath.Join(cwd, "profiles.json")
return newFilePath
}
func defaultProfile() Profile {
return Profile{
Name: "default",
Instance: Instance{
URL: "http://127.0.0.1:8080",
Username: "",
Password: "",
Dynamic: Dynamic{},
},
}
}
func LoadProfiles() ([]Profile, error) {
rwMutex.RLock()
defer rwMutex.RUnlock()
profiles := []Profile{}
if _, err := os.Stat(profilePath()); os.IsNotExist(err) {
profiles = append(profiles, defaultProfile())
if err := SaveProfiles(profiles); err != nil {
slog.Error("Failed to save profiles", "error", err)
}
return profiles, nil
}
file, err := os.ReadFile(profilePath())
if err != nil {
return []Profile{}, fmt.Errorf("failed to read profiles file: %w", err)
}
if err := json.Unmarshal(file, &profiles); err != nil {
return []Profile{}, fmt.Errorf("failed to unmarshal profiles: %w", err)
}
return profiles, nil
}
func SaveProfiles(profiles []Profile) error {
rwMutex.Lock()
defer rwMutex.Unlock()
tmpFile, err := os.CreateTemp(os.TempDir(), "profiles-*.json")
if err != nil {
return fmt.Errorf("failed to create temp file: %w", err)
}
defer os.Remove(tmpFile.Name())
profileBytes, err := json.Marshal(profiles)
if err != nil {
return fmt.Errorf("failed to marshal profiles: %w", err)
}
_, err = tmpFile.Write(profileBytes)
if err != nil {
return fmt.Errorf("failed to write profiles: %w", err)
}
if err := tmpFile.Sync(); err != nil {
return fmt.Errorf("failed to sync temp file: %w", err)
}
tmpFile.Close()
if err := Move(tmpFile.Name(), profilePath()); err != nil {
return fmt.Errorf("failed to move temp file: %w", err)
}
return nil
}
func Move(source, destination string) error {
err := os.Rename(source, destination)
if err != nil && strings.Contains(err.Error(), "invalid cross-device link") {
return moveCrossDevice(source, destination)
}
return err
}
func moveCrossDevice(source, destination string) error {
src, err := os.Open(source)
if err != nil {
return fmt.Errorf("failed to open source file: %w", err)
}
dst, err := os.Create(destination)
if err != nil {
src.Close()
return fmt.Errorf("failed to create destination file: %w", err)
}
_, err = io.Copy(dst, src)
src.Close()
dst.Close()
if err != nil {
return fmt.Errorf("failed to copy file: %w", err)
}
fi, err := os.Stat(source)
if err != nil {
os.Remove(destination)
return fmt.Errorf("failed to stat source file: %w", err)
}
err = os.Chmod(destination, fi.Mode())
if err != nil {
os.Remove(destination)
return fmt.Errorf("failed to chmod destination file: %w", err)
}
os.Remove(source)
return nil
}
func GenerateCreds() error {
cwd, err := os.Getwd()
if err != nil {
return err
}
credsPath := filepath.Join(cwd, "creds.json")
if _, err = os.Stat(credsPath); os.IsNotExist(err) {
bytes := make([]byte, 32)
if _, err = rand.Read(bytes); err != nil {
return err
}
username := "admin"
password := base64.StdEncoding.EncodeToString(bytes)
jsonCreds, err := json.Marshal(Credentials{
Username: username,
Password: password,
})
if err != nil {
return err
}
if err := os.WriteFile(credsPath, jsonCreds, 0644); err != nil {
return err
}
slog.Info("Generated new credentials", "username", username, "password", password)
}
return nil
}
// GetCreds retrieves the credentials from the creds.json file or generates a new one
func (c *Credentials) GetCreds() error {
cwd, err := os.Getwd()
if err != nil {
return err
}
credsPath := filepath.Join(cwd, "creds.json")
file, err := os.ReadFile(credsPath)
if err != nil {
return err
}
if err := json.Unmarshal(file, &c); err != nil {
return err
}
return nil
}
func ParseConfig(dyn Dynamic) ([]byte, error) {
config := &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: make(map[string]*dynamic.Router),
Middlewares: make(map[string]*dynamic.Middleware),
Services: make(map[string]*dynamic.Service),
ServersTransports: make(map[string]*dynamic.ServersTransport),
},
TCP: &dynamic.TCPConfiguration{
Routers: make(map[string]*dynamic.TCPRouter),
Services: make(map[string]*dynamic.TCPService),
Middlewares: make(map[string]*dynamic.TCPMiddleware),
},
UDP: &dynamic.UDPConfiguration{
Routers: make(map[string]*dynamic.UDPRouter),
Services: make(map[string]*dynamic.UDPService),
},
TLS: &dynamic.TLSConfiguration{
Stores: make(map[string]ttls.Store),
Options: make(map[string]ttls.Options),
},
}
for _, router := range dyn.Routers {
// Only add routers by our provider
if router.Provider == "http" {
switch router.RouterType {
case "http":
config.HTTP.Routers[router.Service] = &dynamic.Router{
EntryPoints: router.Entrypoints,
Middlewares: router.Middlewares,
Service: router.Service,
Rule: router.Rule,
// Priority: int(router.Priority.Int64()),
TLS: router.TLS,
}
case "tcp":
config.TCP.Routers[router.Service] = &dynamic.TCPRouter{
EntryPoints: router.Entrypoints,
Middlewares: router.Middlewares,
Service: router.Service,
// Priority: int(router.Priority.Int64()),
Rule: router.Rule,
}
case "udp":
config.UDP.Routers[router.Service] = &dynamic.UDPRouter{
EntryPoints: router.Entrypoints,
Service: router.Service,
}
}
}
}
for _, service := range dyn.Services {
// Only add services by our provider
if service.Provider == "http" {
name := strings.Split(service.Name, "@")[0]
switch service.ServiceType {
case "http":
config.HTTP.Services[name] = &dynamic.Service{
LoadBalancer: service.LoadBalancer,
Weighted: service.Weighted,
Mirroring: service.Mirroring,
Failover: service.Failover,
}
case "tcp":
config.TCP.Services[name], _ = convertService(service)
case "udp":
_, config.UDP.Services[name] = convertService(service)
}
}
}
for _, middleware := range dyn.Middlewares {
if middleware.Provider == "http" {
config.HTTP.Middlewares[middleware.Name] = &dynamic.Middleware{
AddPrefix: middleware.AddPrefix,
StripPrefix: middleware.StripPrefix,
StripPrefixRegex: middleware.StripPrefixRegex,
ReplacePath: middleware.ReplacePath,
ReplacePathRegex: middleware.ReplacePathRegex,
Chain: middleware.Chain,
IPWhiteList: middleware.IPWhiteList,
IPAllowList: middleware.IPAllowList,
Headers: middleware.Headers,
Errors: middleware.Errors,
RateLimit: middleware.RateLimit,
RedirectRegex: middleware.RedirectRegex,
RedirectScheme: middleware.RedirectScheme,
BasicAuth: middleware.BasicAuth,
DigestAuth: middleware.DigestAuth,
ForwardAuth: middleware.ForwardAuth,
InFlightReq: middleware.InFlightReq,
Buffering: middleware.Buffering,
CircuitBreaker: middleware.CircuitBreaker,
Compress: middleware.Compress,
PassTLSClientCert: middleware.PassTLSClientCert,
Retry: middleware.Retry,
ContentType: middleware.ContentType,
Plugin: middleware.Plugin,
}
config.TCP.Middlewares[middleware.Name] = &dynamic.TCPMiddleware{
InFlightConn: middleware.InFlightConn,
}
}
}
// Remove empty configurations
if len(config.HTTP.Routers) == 0 && len(config.HTTP.Services) == 0 &&
len(config.HTTP.Middlewares) == 0 {
config.HTTP = nil
}
if len(config.TCP.Routers) == 0 && len(config.TCP.Services) == 0 &&
len(config.TCP.Middlewares) == 0 {
config.TCP = nil
}
if len(config.UDP.Routers) == 0 && len(config.UDP.Services) == 0 {
config.UDP = nil
}
if len(config.TLS.Stores) == 0 && len(config.TLS.Options) == 0 {
config.TLS = nil
}
yamlConfig, err := yaml.Marshal(config)
if err != nil {
return nil, err
}
return yamlConfig, nil
}
func convertService(service Service) (*dynamic.TCPService, *dynamic.UDPService) {
var tcpServer []dynamic.TCPServer
var udpServer []dynamic.UDPServer
for _, lb := range service.LoadBalancer.Servers {
if lb.URL != "" {
tcpServer = append(tcpServer, dynamic.TCPServer{
Address: lb.URL,
})
udpServer = append(udpServer, dynamic.UDPServer{
Address: lb.URL,
})
}
}
tcpService := &dynamic.TCPService{
LoadBalancer: &dynamic.TCPServersLoadBalancer{
Servers: tcpServer,
},
}
udpService := &dynamic.UDPService{
LoadBalancer: &dynamic.UDPServersLoadBalancer{
Servers: udpServer,
},
}
return tcpService, udpService
}
func FetchTraefikConfig() {
profiles, err := LoadProfiles()
if err != nil {
slog.Error("Failed to load profiles", "error", err)
return
}
for idx, profile := range profiles {
i := profile.Instance
d := Dynamic{
Routers: make(map[string]Router),
Services: make(map[string]Service),
Middlewares: make(map[string]Middleware),
}
// Retrieve routers
var tRouter []Router
tRouter = append(tRouter, fetchRouters[HTTPRouter](i, HTTPRouterAPI)...)
tRouter = append(tRouter, fetchRouters[TCPRouter](i, TCPRouterAPI)...)
tRouter = append(tRouter, fetchRouters[UDPRouter](i, UDPRouterAPI)...)
for _, r := range tRouter {
d.Routers[r.Name] = r
}
for _, r := range profile.Instance.Dynamic.Routers {
d.Routers[r.Name] = r
}
// Retrieve services
var tServices []Service
tServices = append(tServices, fetchServices[HTTPService](i, HTTPServiceAPI)...)
tServices = append(tServices, fetchServices[TCPService](i, TCPServiceAPI)...)
tServices = append(tServices, fetchServices[UDPService](i, UDPServiceAPI)...)
for _, s := range tServices {
d.Services[s.Name] = s
}
for _, s := range profile.Instance.Dynamic.Services {
d.Services[s.Name] = s
}
// Fetch middlewares
var tMiddlewares []Middleware
tMiddlewares = append(
tMiddlewares,
fetchMiddlewares[HTTPMiddleware](i, HTTPMiddlewaresAPI)...)
tMiddlewares = append(
tMiddlewares,
fetchMiddlewares[TCPMiddleware](i, TCPMiddlewaresAPI)...)
for _, m := range tMiddlewares {
d.Middlewares[m.Name] = m
}
for _, m := range profile.Instance.Dynamic.Middlewares {
d.Middlewares[m.Name] = m
}
// Retrieve entrypoints
entrypoints, err := get(i, EntrypointsAPI)
if err != nil {
slog.Error("Failed to get entrypoints", "error", err)
return
}
defer entrypoints.Close()
if err = json.NewDecoder(entrypoints).Decode(&d.Entrypoints); err != nil {
slog.Error("Failed to decode entrypoints", "error", err)
return
}
// Fetch version
version, err := get(i, VersionAPI)
if err != nil {
slog.Error("Failed to get version", "error", err)
return
}
defer version.Close()
var v struct {
Version string `json:"version"`
}
if err = json.NewDecoder(version).Decode(&v); err != nil {
slog.Error("Failed to decode version", "error", err)
return
}
d.Version = v.Version
profiles[idx].Instance.Dynamic = d
}
if err := SaveProfiles(profiles); err != nil {
slog.Error("Failed to save profiles", "error", err)
}
}
// Sync periodically syncs the Traefik configuration
func Sync(wg *sync.WaitGroup) {
defer wg.Done()
ticker := time.NewTicker(time.Second * 60)
defer ticker.Stop()
for range ticker.C {
FetchTraefikConfig()
}
}

View File

@@ -1,15 +1,29 @@
import { get, writable, type Writable } from 'svelte/store';
import { derived, get, writable, type Writable } from 'svelte/store';
import { toast } from 'svelte-sonner';
import type { Profile } from './types/dynamic';
import type { Router, Service } from './types/config';
import type { Middleware } from './types/middlewares';
import { goto } from '$app/navigation';
export const loggedIn = writable(false);
export const profiles: Writable<Profile[]> = writable([]);
export const activeProfile: Writable<Profile> = writable({} as Profile);
export const loggedIn = writable(false);
export const API_URL = import.meta.env.PROD ? '/api' : 'http://localhost:3000/api';
export const entrypoints = derived(
activeProfile,
($activeProfile) => $activeProfile?.client?.dynamic?.entrypoints ?? []
);
export const routers = derived(activeProfile, ($activeProfile) =>
Object.values($activeProfile?.client?.dynamic?.routers ?? [])
);
export const services = derived(activeProfile, ($activeProfile) =>
Object.values($activeProfile?.client?.dynamic?.services ?? [])
);
export const middlewares = derived(activeProfile, ($activeProfile) =>
Object.values($activeProfile?.client?.dynamic?.middlewares ?? [])
);
async function handleError(response: Response) {
if (!response.ok) {
toast.error('Request failed', {
@@ -41,9 +55,8 @@ export async function login(username: string, password: string) {
});
handleError(response);
const { token, expiry } = await response.json();
const { token } = await response.json();
localStorage.setItem('token', token);
localStorage.setItem('expiry', expiry);
loggedIn.set(true);
await getProfiles();
goto('/');
@@ -51,7 +64,6 @@ export async function login(username: string, password: string) {
export async function logout() {
localStorage.removeItem('token');
localStorage.removeItem('expiry');
loggedIn.set(false);
}

View File

@@ -8,7 +8,7 @@
let profile: Profile = {
name: '',
instance: { url: '', username: '', password: '', dynamic: {} }
client: { url: '', username: '', password: '', dynamic: {} }
};
const create = () => {
@@ -18,7 +18,7 @@
createProfile(profile);
profile = {
name: '',
instance: { url: '', username: '', password: '', dynamic: {} }
client: { url: '', username: '', password: '', dynamic: {} }
};
};
@@ -41,9 +41,7 @@
<Dialog.Content class="sm:max-w-[425px]">
<Dialog.Header>
<Dialog.Title>New profile</Dialog.Title>
<Dialog.Description>
Create a new profile to manage your Traefik instances.
</Dialog.Description>
<Dialog.Description>Create a new profile to manage your Traefik clients.</Dialog.Description>
</Dialog.Header>
<div class="grid gap-4 py-4" on:keydown={onKeydown} aria-hidden>
<div class="grid grid-cols-4 items-center gap-4">
@@ -63,8 +61,8 @@
name="url"
type="text"
class="col-span-3"
bind:value={profile.instance.url}
placeholder="URL of your traefik instance"
bind:value={profile.client.url}
placeholder="URL of your traefik client"
required
/>
</div>
@@ -74,8 +72,8 @@
name="username"
type="text"
class="col-span-3"
bind:value={profile.instance.username}
placeholder="Username of your instance"
bind:value={profile.client.username}
placeholder="Username of your client"
required
/>
</div>
@@ -85,8 +83,8 @@
name="password"
type="password"
class="col-span-3"
bind:value={profile.instance.password}
placeholder="Password of your instance"
bind:value={profile.client.password}
placeholder="Password of your client"
required
/>
</div>

View File

@@ -8,14 +8,13 @@
import { Label } from '$lib/components/ui/label/index.js';
import { Switch } from '$lib/components/ui/switch/index.js';
import type { Selected } from 'bits-ui';
import { activeProfile, updateRouter, updateService } from '$lib/api';
import { activeProfile, entrypoints, middlewares, updateRouter, updateService } from '$lib/api';
import { newRouter, newService, type Router } from '$lib/types/config';
import RuleEditor from '../utils/ruleEditor.svelte';
import { toast } from 'svelte-sonner';
let router = newRouter();
let service = newService();
$: middlewares = Object.values($activeProfile?.instance?.dynamic?.middlewares ?? []);
$: servers = service?.loadBalancer?.servers?.length || 0;
const create = async () => {
@@ -132,7 +131,7 @@
<Select.Value placeholder="Select an entrypoint" />
</Select.Trigger>
<Select.Content>
{#each $activeProfile?.instance?.dynamic?.entrypoints || [] as entrypoint}
{#each $entrypoints as entrypoint}
<Select.Item value={entrypoint.name}>
<div class="flex flex-row items-center gap-2">
{entrypoint.name}
@@ -161,7 +160,7 @@
<Select.Value placeholder="Select a middleware" />
</Select.Trigger>
<Select.Content>
{#each middlewares as middleware}
{#each $middlewares as middleware}
{#if router.routerType === middleware.middlewareType}
<Select.Item value={middleware.name}>
{middleware.name}

View File

@@ -57,8 +57,8 @@
name="url"
type="text"
class="col-span-3"
bind:value={profile.instance.url}
placeholder="URL of your instance"
bind:value={profile.client.url}
placeholder="URL of your client"
required
/>
</div>
@@ -68,8 +68,8 @@
name="username"
type="text"
class="col-span-3"
bind:value={profile.instance.username}
placeholder="Username of your instance"
bind:value={profile.client.username}
placeholder="Username of your client"
required
/>
</div>
@@ -79,8 +79,8 @@
name="password"
type="password"
class="col-span-3"
bind:value={profile.instance.password}
placeholder="Password of your instance"
bind:value={profile.client.password}
placeholder="Password of your client"
required
/>
</div>

View File

@@ -7,7 +7,7 @@
import { Input } from '$lib/components/ui/input/index.js';
import { Label } from '$lib/components/ui/label/index.js';
import { Switch } from '$lib/components/ui/switch/index.js';
import { activeProfile, updateRouter, updateService } from '$lib/api';
import { activeProfile, entrypoints, middlewares, updateRouter, updateService } from '$lib/api';
import { newService, type Router, type Service } from '$lib/types/config';
import RuleEditor from '../utils/ruleEditor.svelte';
import type { Selected } from 'bits-ui';
@@ -17,9 +17,8 @@
let oldRouter = router.name;
let oldService = router.service + '@' + router.provider;
$: middlewares = Object.values($activeProfile?.instance?.dynamic?.middlewares || []);
let service: Service | undefined =
$activeProfile?.instance?.dynamic?.services?.[router.service + '@' + router.provider];
$activeProfile?.client?.dynamic?.services?.[router.service + '@' + router.provider];
$: servers = service?.loadBalancer?.servers?.length || 0;
const update = async () => {
@@ -114,7 +113,7 @@
<Select.Value placeholder="Select an entrypoint" />
</Select.Trigger>
<Select.Content>
{#each $activeProfile?.instance?.dynamic?.entrypoints || [] as entrypoint}
{#each $entrypoints || [] as entrypoint}
<Select.Item value={entrypoint.name}>
<div class="flex flex-row items-center gap-2">
{entrypoint.name}
@@ -143,7 +142,7 @@
<Select.Value placeholder="Select a middleware" />
</Select.Trigger>
<Select.Content>
{#each middlewares as middleware}
{#each $middlewares as middleware}
{#if router.routerType === middleware.type}
<Select.Item value={middleware.name}>
{middleware.name}

View File

@@ -3,9 +3,9 @@
</script>
<footer class="flex flex-row items-center justify-end sm:px-6">
{#if $activeProfile?.instance?.dynamic?.version}
{#if $activeProfile?.client?.dynamic?.version}
<span class="text-xs text-muted-foreground">
Traefik v{$activeProfile?.instance?.dynamic?.version}
Traefik v{$activeProfile?.client?.dynamic?.version}
</span>
{/if}
</footer>

View File

@@ -4,10 +4,10 @@ import type { CertAndStores, Options, Store } from './tls';
export interface Profile {
name: string;
instance: Instance;
client: Client;
}
export interface Instance {
export interface Client {
url: string;
username: string;
password: string;

View File

@@ -1,15 +1,14 @@
import { goto } from '$app/navigation';
import { loggedIn, logout } from '$lib/api';
import { API_URL, loggedIn, logout } from '$lib/api';
import type { LayoutLoad } from './$types';
export const ssr = false;
export const prerender = true;
export const load: LayoutLoad = async ({ url }) => {
export const load: LayoutLoad = async ({ fetch, url }) => {
const token = localStorage.getItem('token');
const expiry = localStorage.getItem('expiry') as string;
if (token === null || expiry === null) {
if (token === null) {
logout();
if (url.pathname !== '/login') {
goto('/login');
@@ -17,8 +16,11 @@ export const load: LayoutLoad = async ({ url }) => {
return {};
}
const expiryDate = new Date(expiry);
if (Date.now() > expiryDate.getTime()) {
const response = await fetch(`${API_URL}/verify`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` }
});
if (!response.ok) {
logout();
if (url.pathname !== '/login') {
goto('/login');
@@ -26,15 +28,5 @@ export const load: LayoutLoad = async ({ url }) => {
return {};
}
try {
if (url.pathname === '/login') {
goto('/');
}
loggedIn.set(true);
} catch (e) {
logout();
if (url.pathname !== '/login') {
goto('/login');
}
}
loggedIn.set(true);
};

View File

@@ -3,7 +3,15 @@
import * as Card from '$lib/components/ui/card/index.js';
import * as Table from '$lib/components/ui/table/index.js';
import * as Select from '$lib/components/ui/select';
import { activeProfile, deleteRouter, updateProfile } from '$lib/api';
import {
activeProfile,
deleteRouter,
entrypoints,
middlewares,
routers,
services,
updateProfile
} from '$lib/api';
import CreateRouter from '$lib/components/modals/createRouter.svelte';
import UpdateRouter from '$lib/components/modals/updateRouter.svelte';
import Pagination from '$lib/components/tables/pagination.svelte';
@@ -17,11 +25,7 @@
let currentPage = 1;
let fRouters: Router[] = [];
let perPage: Selected<number> | undefined = { value: 10, label: '10' }; // Items per page
$: routers = Object.values($activeProfile?.instance?.dynamic?.routers ?? []);
$: services = Object.values($activeProfile?.instance?.dynamic?.services ?? []);
$: middlewares = Object.values($activeProfile?.instance?.dynamic?.middlewares ?? []);
$: search, routers, currentPage, searchRouter();
$: search, $routers, currentPage, searchRouter();
// Reset the page to 1 when the search input changes
$: {
@@ -31,7 +35,7 @@
}
const searchRouter = () => {
let items: Router[] = [...routers];
let items: Router[] = [...$routers];
if (search) {
const searchParts = search.split(' ').map((part) => part.toLowerCase());
@@ -108,7 +112,7 @@
};
const getServiceStatus = (router: Router): Record<string, string | boolean> => {
let service = services.find((s) => s.name === router.service + '@' + router.provider);
let service = $services.find((s) => s.name === router.service + '@' + router.provider);
let totalServices = service?.loadBalancer?.servers?.length || 0;
let upServices = 0;
@@ -252,7 +256,7 @@
<Select.Value placeholder="Select an entrypoint" />
</Select.Trigger>
<Select.Content class="text-sm">
{#each $activeProfile?.instance?.dynamic?.entrypoints || [] as entrypoint}
{#each $entrypoints as entrypoint}
<Select.Item value={entrypoint.name}>
<div class="flex flex-row items-center gap-2">
{entrypoint.name}
@@ -279,7 +283,7 @@
<Select.Value placeholder="Select a middleware" />
</Select.Trigger>
<Select.Content class="text-sm">
{#each middlewares as middleware}
{#each $middlewares as middleware}
{#if router.routerType === middleware.middlewareType}
<Select.Item value={middleware.name}>
{middleware.name}
@@ -319,7 +323,7 @@
<Card.Footer>
<div class="text-xs text-muted-foreground">
Showing <strong>{fRouters.length > 0 ? 1 : 0}-{fRouters.length}</strong> of
<strong>{routers.length}</strong> routers
<strong>{$routers.length}</strong> routers
</div>
</Card.Footer>
</Card.Root>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { activeProfile, deleteMiddleware } from '$lib/api';
import { activeProfile, deleteMiddleware, middlewares } from '$lib/api';
import CreateMiddleware from '$lib/components/modals/createMiddleware.svelte';
import Pagination from '$lib/components/tables/pagination.svelte';
import { Button, buttonVariants } from '$lib/components/ui/button';
@@ -16,9 +16,7 @@
let currentPage = 1;
let fMiddlewares: Middleware[] = [];
let perPage: Selected<number> | undefined = { value: 10, label: '10' }; // Items per page
$: middlewares = Object.values($activeProfile?.instance?.dynamic?.middlewares ?? []);
$: search, middlewares, currentPage, searchMiddleware();
$: search, $middlewares, currentPage, searchMiddleware();
// Reset the page to 1 when the search input changes
$: {
@@ -28,7 +26,7 @@
}
function searchMiddleware() {
let items: Middleware[] = [...middlewares];
let items: Middleware[] = [...$middlewares];
if (search) {
const searchParts = search.split(' ').map((part) => part.toLowerCase());
@@ -174,7 +172,7 @@
<div class="text-xs text-muted-foreground">
Showing <strong>{fMiddlewares.length > 0 ? 1 : 0}-{fMiddlewares.length}</strong>
of
<strong>{middlewares.length}</strong> middlewares
<strong>{$middlewares.length}</strong> middlewares
</div>
</Card.Footer>
</Card.Root>