mirror of
https://github.com/MizuchiLabs/mantrae.git
synced 2026-01-06 06:19:57 -06:00
refactor and better auth verification
This commit is contained in:
@@ -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 {
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
45
main.go
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
165
pkg/traefik/config.go
Normal 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
|
||||
}
|
||||
@@ -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
133
pkg/traefik/profile.go
Normal 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
|
||||
}
|
||||
@@ -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
82
pkg/util/secrets.go
Normal 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
|
||||
}
|
||||
452
util/util.go
452
util/util.go
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user