huge refactor, switching to sqlite

This commit is contained in:
NCRoxas
2024-09-03 09:49:23 +02:00
parent c020bc4f11
commit a0640b352d
42 changed files with 2395 additions and 998 deletions

5
.gitignore vendored
View File

@@ -24,5 +24,8 @@ mantrae
# Go workspace file
go.work
*.json
*.db
*.db-shm
*.db-wal
.envrc
dist/

View File

@@ -6,11 +6,11 @@
## Features
- **Domain Management**: Easily manage your domains and assign them to specific hosts via the web interface.
- **Simplified UI**: A clean and intuitive interface that keeps the complexity to a minimum.
- **Router Configuration**: Create and manage Traefik routers with custom rules, entrypoints, and middleware configurations.
- **Middleware Management**: Add middlewares to your routers, including rate limiting, authentication, and more.
- **Service Status**: Monitor the status of your services and see their health information.
- **Simplified UI**: A clean and intuitive interface that keeps the complexity to a minimum.
- **DNS Providers**: Support for multiple DNS providers (currently Cloudflare and PowerDNS) for automatic DNS record updates.
## Getting Started
@@ -21,20 +21,16 @@
### Installation
1. Download the latest release from the [releases page](https://github.com/MizuchiLabs/mantrae/releases)
1. Generate a random secret with `openssl rand -hex 32`
1. Extract the downloaded file
1. Use docker `docker run --name mantrae -e SECRET=<secret> -d -p 3000:3000 ghcr.io/mizuchilabs/mantrae:latest`
1. Run the application `./mantrae`
1. Or use the example docker-compose.yml file to run mantrae and traefik together
1. **Access the Web UI**:
Open your web browser and navigate to `http://localhost:3000`
1. Or use docker `docker run --name mantrae -d -p 3000:3000 ghcr.io/mizuchilabs/mantrae:latest`
1. You can also use the example docker-compose.yml file to run mantrae and traefik together
1. Use the admin password, which will be printed in the logs after the first start
1. Use the admin password, which will be printed in the logs after the first start. You won't be able to access the password afterwards!
## Usage
@@ -88,6 +84,13 @@ providers:
endpoint: "<endpoint where mantrae is running>"
```
## Roadmap
- Add support for multiple DNS providers and better handling.
- Backup and restore functionality (S3)
- Support multiple database providers (currently only supports SQLite)
- Better credentials management and multi-user support.
## Contributing
Contributions are welcome! Please feel free to submit issues, fork the repository, and create pull requests.

View File

@@ -4,6 +4,8 @@ services:
mantrae:
image: ghcr.io/mizuchilabs/mantrae:latest
container_name: mantrae
environment:
- SECRET=<secret> # generate a secret with openssl rand -hex 32
command:
- --url=http://traefik:8080 # if traefik is running on the same host
- --username=admin # use if you want to enable basicauth on traefik

1
go.mod
View File

@@ -9,6 +9,7 @@ require (
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/joeig/go-powerdns/v3 v3.12.0
github.com/lmittmann/tint v1.0.5
github.com/mattn/go-sqlite3 v1.14.22
golang.org/x/crypto v0.26.0
golang.org/x/net v0.28.0
sigs.k8s.io/yaml v1.4.0

2
go.sum
View File

@@ -22,6 +22,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lmittmann/tint v1.0.5 h1:NQclAutOfYsqs2F1Lenue6OoWCajs5wJcP3DfWVpePw=
github.com/lmittmann/tint v1.0.5/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=

View File

@@ -1,9 +1,9 @@
package api
import (
"os"
"time"
"github.com/MizuchiLabs/mantrae/pkg/util"
"github.com/golang-jwt/jwt/v5"
)
@@ -23,26 +23,18 @@ func GenerateJWT(username string) (string, error) {
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
var secret util.Credentials
if err := secret.GetCreds(); err != nil {
return "", err
}
return token.SignedString(secret.Secret)
return token.SignedString([]byte(os.Getenv("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 secret.Secret, nil
return []byte(os.Getenv("SECRET")), nil
},
)
if err != nil {

View File

@@ -2,13 +2,14 @@
package api
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"github.com/MizuchiLabs/mantrae/pkg/dns"
"github.com/MizuchiLabs/mantrae/internal/db"
"github.com/MizuchiLabs/mantrae/pkg/traefik"
"github.com/MizuchiLabs/mantrae/pkg/util"
)
// Helper function to write JSON response
@@ -32,26 +33,26 @@ func updateName[K comparable, V any](m map[K]V, oldName, newName K) {
}
}
// Authentication -------------------------------------------------------------
// Login verifies the user credentials
func Login(w http.ResponseWriter, r *http.Request) {
var creds util.Credentials
var creds db.Credential
if err := json.NewDecoder(r.Body).Decode(&creds); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
http.Error(w, "Failed to decode credentials", http.StatusBadRequest)
return
}
if creds.Username == "" || creds.Password == "" {
http.Error(w, "username and password cannot be empty", http.StatusBadRequest)
http.Error(w, "Username or password cannot be empty", http.StatusBadRequest)
return
}
var valid util.Credentials
if err := valid.GetCreds(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if creds.Username != valid.Username || creds.Password != valid.Password {
http.Error(w, "invalid username or password", http.StatusUnauthorized)
if _, err := db.Query.ValidateAuth(context.Background(), db.ValidateAuthParams{
Username: creds.Username,
Password: creds.Password,
}); err != nil {
http.Error(w, "Invalid username or password", http.StatusUnauthorized)
return
}
@@ -67,431 +68,244 @@ func Login(w http.ResponseWriter, r *http.Request) {
func VerifyToken(w http.ResponseWriter, r *http.Request) {
tokenString := r.Header.Get("Authorization")
if len(tokenString) < 7 {
http.Error(w, "token cannot be empty", http.StatusBadRequest)
http.Error(w, "Token cannot be empty", http.StatusBadRequest)
return
}
_, err := ValidateJWT(tokenString[7:])
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusOK)
}
// Profiles -------------------------------------------------------------------
// GetProfiles returns all profiles but without the dynamic data
func GetProfiles(w http.ResponseWriter, r *http.Request) {
data := make(map[string]traefik.Profile, len(traefik.ProfileData.Profiles))
for name, profile := range traefik.ProfileData.Profiles {
data[name] = traefik.Profile{
Name: profile.Name,
URL: profile.URL,
Username: profile.Username,
Password: profile.Password,
TLS: profile.TLS,
}
profiles, err := db.Query.ListProfiles(context.Background())
if err != nil {
http.Error(w, "Failed to get profiles", http.StatusInternalServerError)
return
}
writeJSON(w, data)
writeJSON(w, profiles)
}
// GetProfile returns a single profile
func GetProfile(w http.ResponseWriter, r *http.Request) {
profileName := r.PathValue("name")
if profileName == "" {
http.Error(w, "profile name cannot be empty", http.StatusBadRequest)
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
http.Error(w, "Profile not found", http.StatusNotFound)
return
}
profile, ok := traefik.ProfileData.Profiles[profileName]
if !ok {
http.Error(w, "profile not found", http.StatusNotFound)
profile, err := db.Query.GetProfileByID(context.Background(), id)
if err != nil {
http.Error(w, "Profile not found", http.StatusNotFound)
return
}
writeJSON(w, profile)
}
// CreateProfile creates a new profile
func CreateProfile(w http.ResponseWriter, r *http.Request) {
var profile traefik.Profile
var profile db.CreateProfileParams
if err := json.NewDecoder(r.Body).Decode(&profile); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
http.Error(w, "Failed to decode profile", http.StatusBadRequest)
return
}
if err := profile.Verify(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
data, err := db.Query.CreateProfile(context.Background(), profile)
if err != nil {
http.Error(w, "Failed to create profile", http.StatusInternalServerError)
return
}
traefik.ProfileData.Profiles[profile.Name] = profile
if err := traefik.ProfileData.Save(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
go traefik.GetTraefikConfig()
writeJSON(w, traefik.ProfileData.Profiles)
writeJSON(w, data)
}
// UpdateProfile updates a single profile
func UpdateProfile(w http.ResponseWriter, r *http.Request) {
if len(traefik.ProfileData.Profiles) == 0 {
http.Error(w, "no profiles configured", http.StatusBadRequest)
return
}
profileName := r.PathValue("name")
if profileName == "" {
http.Error(w, "profile name cannot be empty", http.StatusBadRequest)
return
}
var profile traefik.Profile
var profile db.UpdateProfileParams
if err := json.NewDecoder(r.Body).Decode(&profile); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
http.Error(w, "Failed to decode profile", http.StatusBadRequest)
return
}
if err := profile.Verify(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
updateName(traefik.ProfileData.Profiles, profileName, profile.Name)
traefik.ProfileData.Profiles[profile.Name] = profile
if err := traefik.ProfileData.Save(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
data, err := db.Query.UpdateProfile(context.Background(), profile)
if err != nil {
http.Error(w, "Failed to update profile", http.StatusInternalServerError)
return
}
go traefik.GetTraefikConfig()
writeJSON(w, profile)
writeJSON(w, data)
}
// DeleteProfile deletes a single profile
func DeleteProfile(w http.ResponseWriter, r *http.Request) {
profileName := r.PathValue("name")
if profileName == "" {
http.Error(w, "profile name cannot be empty", http.StatusBadRequest)
return
}
if _, ok := traefik.ProfileData.Profiles[profileName]; !ok {
http.Error(w, "profile not found", http.StatusNotFound)
return
}
delete(traefik.ProfileData.Profiles, profileName)
if err := traefik.ProfileData.Save(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, traefik.ProfileData.Profiles)
}
func GetProviders(w http.ResponseWriter, r *http.Request) {
var providers dns.Providers
if err := providers.Load(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, providers.Providers)
}
func UpdateProvider(w http.ResponseWriter, r *http.Request) {
var provider dns.Provider
if err := json.NewDecoder(r.Body).Decode(&provider); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := provider.Verify(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
providerName := r.PathValue("name")
if providerName == "" {
http.Error(w, "provider name cannot be empty", http.StatusBadRequest)
return
}
var providers dns.Providers
if err := providers.Load(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if providers.Providers == nil {
providers.Providers = make(map[string]dns.Provider)
}
updateName(providers.Providers, providerName, provider.Name)
providers.Providers[provider.Name] = provider
if err := providers.Save(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, providers.Providers)
}
func DeleteProvider(w http.ResponseWriter, r *http.Request) {
providerName := r.PathValue("name")
if providerName == "" {
http.Error(w, "provider name cannot be empty", http.StatusBadRequest)
return
}
var providers dns.Providers
if err := providers.Load(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
delete(providers.Providers, providerName)
if err := providers.Save(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, providers.Providers)
}
// UpdateRouter updates or creates a router
func UpdateRouter(w http.ResponseWriter, r *http.Request) {
var router traefik.Router
if err := json.NewDecoder(r.Body).Decode(&router); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := router.Verify(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
profileName := r.PathValue("profile")
routerName := r.PathValue("router")
if profileName == "" || routerName == "" {
http.Error(w, "profile or router name cannot be empty", http.StatusBadRequest)
return
}
profile, ok := traefik.ProfileData.Profiles[profileName]
if !ok {
http.Error(w, "profile not found", http.StatusNotFound)
return
}
// Initialize the Routers map if it is nil
if profile.Dynamic.Routers == nil {
profile.Dynamic.Routers = make(map[string]traefik.Router)
}
// If the router name is being changed, delete the old entry
updateName(profile.Dynamic.Routers, routerName, router.Name)
profile.Dynamic.Routers[router.Name] = router
traefik.ProfileData.Profiles[profileName] = profile // Update the profile in the map
if err := traefik.ProfileData.Save(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
go dns.UpdateDNS()
writeJSON(w, profile)
}
// DeleteRouter deletes a single router and it's services
func DeleteRouter(w http.ResponseWriter, r *http.Request) {
profileName := r.PathValue("profile")
routerName := r.PathValue("router")
if profileName == "" || routerName == "" {
http.Error(w, "profile or router name cannot be empty", http.StatusBadRequest)
return
}
profile, ok := traefik.ProfileData.Profiles[profileName]
if !ok {
http.Error(w, "profile not found", http.StatusNotFound)
return
}
// Delete the DNS record
go dns.DeleteDNS(profile.Dynamic.Routers[routerName])
delete(profile.Dynamic.Routers, routerName)
delete(profile.Dynamic.Services, routerName)
if err := traefik.ProfileData.Save(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, profile)
}
// UpdateService updates or creates a service
func UpdateService(w http.ResponseWriter, r *http.Request) {
var service traefik.Service
if err := json.NewDecoder(r.Body).Decode(&service); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := service.Verify(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
profileName := r.PathValue("profile")
serviceName := r.PathValue("service")
if profileName == "" || serviceName == "" {
http.Error(w, "profile or service name cannot be empty", http.StatusBadRequest)
return
}
profile, ok := traefik.ProfileData.Profiles[profileName]
if !ok {
http.Error(w, "profile not found", http.StatusNotFound)
return
}
if profile.Dynamic.Services == nil {
profile.Dynamic.Services = make(map[string]traefik.Service)
}
updateName(profile.Dynamic.Services, serviceName, service.Name)
profile.Dynamic.Services[service.Name] = service
traefik.ProfileData.Profiles[profileName] = profile // Update the profile in the map
if err := traefik.ProfileData.Save(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, profile)
}
// DeleteService deletes a single service and its router
func DeleteService(w http.ResponseWriter, r *http.Request) {
profileName := r.PathValue("profile")
serviceName := r.PathValue("service")
if profileName == "" || serviceName == "" {
http.Error(w, "profile or service name cannot be empty", http.StatusBadRequest)
return
}
profile, ok := traefik.ProfileData.Profiles[profileName]
if !ok {
http.Error(w, "profile not found", http.StatusNotFound)
return
}
delete(profile.Dynamic.Services, serviceName)
delete(profile.Dynamic.Routers, serviceName)
if err := traefik.ProfileData.Save(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, profile)
}
// UpdateMiddleware updates or creates a middleware
func UpdateMiddleware(w http.ResponseWriter, r *http.Request) {
var middleware traefik.Middleware
if err := json.NewDecoder(r.Body).Decode(&middleware); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
profileName := r.PathValue("profile")
middlewareName := r.PathValue("middleware")
if profileName == "" || middlewareName == "" {
http.Error(w, "profile or middleware name cannot be empty", http.StatusBadRequest)
return
}
profile, ok := traefik.ProfileData.Profiles[profileName]
if !ok {
http.Error(w, "profile not found", http.StatusNotFound)
return
}
if profile.Dynamic.Middlewares == nil {
profile.Dynamic.Middlewares = make(map[string]traefik.Middleware)
}
updateName(profile.Dynamic.Middlewares, middlewareName, middleware.Name)
if err := middleware.Verify(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
profile.Dynamic.Middlewares[middleware.Name] = middleware
traefik.ProfileData.Profiles[profileName] = profile // Update the profile in the map
if err := traefik.ProfileData.Save(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, profile)
}
// DeleteMiddleware deletes a single middleware and it's services
func DeleteMiddleware(w http.ResponseWriter, r *http.Request) {
profileName := r.PathValue("profile")
middlewareName := r.PathValue("middleware")
if profileName == "" || middlewareName == "" {
http.Error(w, "profile or middleware name cannot be empty", http.StatusBadRequest)
return
}
profile, ok := traefik.ProfileData.Profiles[profileName]
if !ok {
http.Error(w, "profile not found", http.StatusNotFound)
return
}
delete(profile.Dynamic.Middlewares, middlewareName)
if err := traefik.ProfileData.Save(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, profile)
}
// GetConfig returns the traefik config for a single profile
func GetConfig(w http.ResponseWriter, r *http.Request) {
profileName := r.PathValue("name")
if profileName == "" {
http.Error(w, "profile name cannot be empty", http.StatusBadRequest)
return
}
profile, ok := traefik.ProfileData.Profiles[profileName]
if !ok {
http.Error(w, "profile not found", http.StatusNotFound)
return
}
yamlConfig, err := traefik.GenerateConfig(profile.Dynamic)
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
http.Error(w, "Profile not found", http.StatusNotFound)
return
}
if err := db.Query.DeleteProfileByID(context.Background(), id); err != nil {
http.Error(w, "Failed to delete profile", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
// Providers ------------------------------------------------------------------
// GetProviders returns all providers
func GetProviders(w http.ResponseWriter, r *http.Request) {
providers, err := db.Query.ListProviders(context.Background())
if err != nil {
http.Error(w, "Failed to get providers", http.StatusInternalServerError)
return
}
writeJSON(w, providers)
}
// GetProvider returns a single provider
func GetProvider(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
http.Error(w, "Provider not found", http.StatusNotFound)
return
}
provider, err := db.Query.GetProviderByID(context.Background(), id)
if err != nil {
http.Error(w, "Provider not found", http.StatusNotFound)
return
}
writeJSON(w, provider)
}
// CreateProvider creates a new provider
func CreateProvider(w http.ResponseWriter, r *http.Request) {
var provider db.CreateProviderParams
if err := json.NewDecoder(r.Body).Decode(&provider); err != nil {
http.Error(w, "Failed to decode provider", http.StatusBadRequest)
return
}
data, err := db.Query.CreateProvider(context.Background(), provider)
if err != nil {
http.Error(w, "Failed to create provider", http.StatusInternalServerError)
return
}
writeJSON(w, data)
}
// UpdateProvider updates a single provider
func UpdateProvider(w http.ResponseWriter, r *http.Request) {
var provider db.UpdateProviderParams
if err := json.NewDecoder(r.Body).Decode(&provider); err != nil {
http.Error(w, "Failed to decode provider", http.StatusBadRequest)
return
}
data, err := db.Query.UpdateProvider(context.Background(), provider)
if err != nil {
http.Error(w, "Failed to update profile", http.StatusInternalServerError)
return
}
writeJSON(w, data)
}
// DeleteProvider deletes a single provider
func DeleteProvider(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
http.Error(w, "Profile not found", http.StatusNotFound)
return
}
if err := db.Query.DeleteProviderByID(context.Background(), id); err != nil {
http.Error(w, "Failed to delete provider", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
// Config ---------------------------------------------------------------------
// GetConfig returns the config for a single profile
func GetConfig(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
http.Error(w, "Profile not found", http.StatusNotFound)
return
}
config, err := db.Query.GetConfigByProfileID(context.Background(), id)
if err != nil {
http.Error(w, "Profile not found", http.StatusNotFound)
return
}
data, err := traefik.DecodeConfig(config)
if err != nil {
http.Error(w, "Failed to decode config", http.StatusInternalServerError)
return
}
writeJSON(w, data)
}
func UpdateConfig(w http.ResponseWriter, r *http.Request) {
var config traefik.Dynamic
if err := json.NewDecoder(r.Body).Decode(&config); err != nil {
http.Error(w, "Failed to decode config", http.StatusBadRequest)
return
}
if err := traefik.UpdateConfig(config.ProfileID, &config); err != nil {
http.Error(w, "Failed to update config", http.StatusInternalServerError)
return
}
writeJSON(w, config)
}
// GetTraefikConfig returns the traefik config
func GetTraefikConfig(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
http.Error(w, "Profile not found", http.StatusNotFound)
return
}
config, err := db.Query.GetConfigByProfileID(context.Background(), id)
if err != nil {
http.Error(w, "Profile not found", http.StatusNotFound)
return
}
data, err := traefik.DecodeConfig(config)
if err != nil {
http.Error(w, "Failed to decode config", http.StatusInternalServerError)
return
}
yamlConfig, err := traefik.GenerateConfig(*data)
if err != nil {
http.Error(w, "Failed to generate traefik config", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/yaml")
w.Header().
Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.yaml", profileName))
Set("Content-Disposition", fmt.Sprintf("attachment; filename=dynamic.yaml"))
if _, err := w.Write(yamlConfig); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
http.Error(w, "Failed to write traefik config", http.StatusInternalServerError)
return
}
}

View File

@@ -1,12 +1,13 @@
package api
import (
"context"
"log/slog"
"net/http"
"strings"
"time"
"github.com/MizuchiLabs/mantrae/pkg/util"
"github.com/MizuchiLabs/mantrae/internal/db"
)
// statusRecorder is a wrapper around http.ResponseWriter to capture the status code
@@ -78,13 +79,13 @@ func BasicAuth(next http.Handler) http.Handler {
return
}
var valid util.Credentials
if err := valid.GetCreds(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
creds, err := db.Query.GetCredentialByUsername(context.Background(), username)
if err != nil {
http.Error(w, "User not found", http.StatusNotFound)
return
}
if username != valid.Username || password != valid.Password {
if password != creds.Password {
w.WriteHeader(http.StatusUnauthorized)
return
}

View File

@@ -14,26 +14,22 @@ func Routes() http.Handler {
mux.HandleFunc("POST /api/login", Login)
mux.HandleFunc("POST /api/verify", VerifyToken)
mux.HandleFunc("GET /api/profiles", JWT(GetProfiles))
mux.HandleFunc("GET /api/profile/{name}", JWT(GetProfile))
mux.HandleFunc("POST /api/profiles", JWT(CreateProfile))
mux.HandleFunc("PUT /api/profiles/{name}", JWT(UpdateProfile))
mux.HandleFunc("DELETE /api/profiles/{name}", JWT(DeleteProfile))
mux.HandleFunc("GET /api/profile", JWT(GetProfiles))
mux.HandleFunc("GET /api/profile/{id}", JWT(GetProfile))
mux.HandleFunc("POST /api/profile", JWT(CreateProfile))
mux.HandleFunc("PUT /api/profile", JWT(UpdateProfile))
mux.HandleFunc("DELETE /api/profile/{id}", JWT(DeleteProfile))
mux.HandleFunc("GET /api/providers", JWT(GetProviders))
mux.HandleFunc("PUT /api/providers/{name}", JWT(UpdateProvider))
mux.HandleFunc("DELETE /api/providers/{name}", JWT(DeleteProvider))
mux.HandleFunc("GET /api/provider", JWT(GetProviders))
mux.HandleFunc("GET /api/provider/{id}", JWT(GetProvider))
mux.HandleFunc("POST /api/provider", JWT(CreateProvider))
mux.HandleFunc("PUT /api/provider", JWT(UpdateProvider))
mux.HandleFunc("DELETE /api/provider/{id}", JWT(DeleteProvider))
mux.HandleFunc("PUT /api/routers/{profile}/{router}", JWT(UpdateRouter))
mux.HandleFunc("DELETE /api/routers/{profile}/{router}", JWT(DeleteRouter))
mux.HandleFunc("GET /api/config/{id}", JWT(GetConfig))
mux.HandleFunc("PUT /api/config/{id}", JWT(UpdateConfig))
mux.HandleFunc("PUT /api/services/{profile}/{service}", JWT(UpdateService))
mux.HandleFunc("DELETE /api/services/{profile}/{service}", JWT(DeleteService))
mux.HandleFunc("PUT /api/middlewares/{profile}/{middleware}", JWT(UpdateMiddleware))
mux.HandleFunc("DELETE /api/middlewares/{profile}/{middleware}", JWT(DeleteMiddleware))
mux.HandleFunc("GET /api/{name}", GetConfig)
mux.HandleFunc("GET /api/{id}", GetTraefikConfig)
staticContent, err := fs.Sub(web.StaticFS, "build")
if err != nil {

78
internal/config/flags.go Normal file
View File

@@ -0,0 +1,78 @@
package config
import (
"context"
"flag"
"fmt"
"log/slog"
"os"
"github.com/MizuchiLabs/mantrae/internal/db"
"github.com/MizuchiLabs/mantrae/pkg/util"
)
type Flags struct {
Version bool
Port int
URL string
Username string
Password string
}
func ParseFlags() *Flags {
var flags Flags
flag.BoolVar(&flags.Version, "version", false, "Print version and exit")
flag.IntVar(&flags.Port, "port", 3000, "Port to listen on")
flag.StringVar(
&flags.URL,
"url",
"",
"Specify the URL of the Traefik instance (e.g. http://localhost:8080)",
)
flag.StringVar(&flags.Username, "username", "", "Specify the username for the Traefik instance")
flag.StringVar(&flags.Password, "password", "", "Specify the password for the Traefik instance")
flag.Parse()
if flags.Version {
fmt.Println(util.Version)
os.Exit(0)
}
if flags.URL != "" {
SetDefaultProfile(flags.URL, flags.Username, flags.Password)
}
return &flags
}
func SetDefaultProfile(url, username, password string) {
profile, err := db.Query.GetProfileByName(context.Background(), "default")
if err != nil {
_, err := db.Query.CreateProfile(context.Background(), db.CreateProfileParams{
Name: "default",
Url: url,
Username: &username,
Password: &password,
Tls: false,
})
if err != nil {
slog.Error("Failed to create default profile", "error", err)
}
slog.Info("Generated default profile", "url", url)
return
}
if profile.Url != url || profile.Username != &username || profile.Password != &password {
if _, err := db.Query.UpdateProfile(context.Background(), db.UpdateProfileParams{
ID: profile.ID,
Name: "default",
Url: url,
Username: &username,
Password: &password,
Tls: false,
}); err != nil {
slog.Error("Failed to update default profile", "error", err)
}
}
}

368
internal/db/db.go Normal file
View File

@@ -0,0 +1,368 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
package db
import (
"context"
"database/sql"
"fmt"
)
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
q := Queries{db: db}
var err error
if q.createConfigStmt, err = db.PrepareContext(ctx, createConfig); err != nil {
return nil, fmt.Errorf("error preparing query CreateConfig: %w", err)
}
if q.createCredentialStmt, err = db.PrepareContext(ctx, createCredential); err != nil {
return nil, fmt.Errorf("error preparing query CreateCredential: %w", err)
}
if q.createProfileStmt, err = db.PrepareContext(ctx, createProfile); err != nil {
return nil, fmt.Errorf("error preparing query CreateProfile: %w", err)
}
if q.createProviderStmt, err = db.PrepareContext(ctx, createProvider); err != nil {
return nil, fmt.Errorf("error preparing query CreateProvider: %w", err)
}
if q.deleteConfigByProfileIDStmt, err = db.PrepareContext(ctx, deleteConfigByProfileID); err != nil {
return nil, fmt.Errorf("error preparing query DeleteConfigByProfileID: %w", err)
}
if q.deleteConfigByProfileNameStmt, err = db.PrepareContext(ctx, deleteConfigByProfileName); err != nil {
return nil, fmt.Errorf("error preparing query DeleteConfigByProfileName: %w", err)
}
if q.deleteCredentialByIDStmt, err = db.PrepareContext(ctx, deleteCredentialByID); err != nil {
return nil, fmt.Errorf("error preparing query DeleteCredentialByID: %w", err)
}
if q.deleteCredentialByUsernameStmt, err = db.PrepareContext(ctx, deleteCredentialByUsername); err != nil {
return nil, fmt.Errorf("error preparing query DeleteCredentialByUsername: %w", err)
}
if q.deleteProfileByIDStmt, err = db.PrepareContext(ctx, deleteProfileByID); err != nil {
return nil, fmt.Errorf("error preparing query DeleteProfileByID: %w", err)
}
if q.deleteProfileByNameStmt, err = db.PrepareContext(ctx, deleteProfileByName); err != nil {
return nil, fmt.Errorf("error preparing query DeleteProfileByName: %w", err)
}
if q.deleteProviderByIDStmt, err = db.PrepareContext(ctx, deleteProviderByID); err != nil {
return nil, fmt.Errorf("error preparing query DeleteProviderByID: %w", err)
}
if q.deleteProviderByNameStmt, err = db.PrepareContext(ctx, deleteProviderByName); err != nil {
return nil, fmt.Errorf("error preparing query DeleteProviderByName: %w", err)
}
if q.getConfigByProfileIDStmt, err = db.PrepareContext(ctx, getConfigByProfileID); err != nil {
return nil, fmt.Errorf("error preparing query GetConfigByProfileID: %w", err)
}
if q.getConfigByProfileNameStmt, err = db.PrepareContext(ctx, getConfigByProfileName); err != nil {
return nil, fmt.Errorf("error preparing query GetConfigByProfileName: %w", err)
}
if q.getCredentialByIDStmt, err = db.PrepareContext(ctx, getCredentialByID); err != nil {
return nil, fmt.Errorf("error preparing query GetCredentialByID: %w", err)
}
if q.getCredentialByUsernameStmt, err = db.PrepareContext(ctx, getCredentialByUsername); err != nil {
return nil, fmt.Errorf("error preparing query GetCredentialByUsername: %w", err)
}
if q.getProfileByIDStmt, err = db.PrepareContext(ctx, getProfileByID); err != nil {
return nil, fmt.Errorf("error preparing query GetProfileByID: %w", err)
}
if q.getProfileByNameStmt, err = db.PrepareContext(ctx, getProfileByName); err != nil {
return nil, fmt.Errorf("error preparing query GetProfileByName: %w", err)
}
if q.getProviderByIDStmt, err = db.PrepareContext(ctx, getProviderByID); err != nil {
return nil, fmt.Errorf("error preparing query GetProviderByID: %w", err)
}
if q.getProviderByNameStmt, err = db.PrepareContext(ctx, getProviderByName); err != nil {
return nil, fmt.Errorf("error preparing query GetProviderByName: %w", err)
}
if q.listConfigsStmt, err = db.PrepareContext(ctx, listConfigs); err != nil {
return nil, fmt.Errorf("error preparing query ListConfigs: %w", err)
}
if q.listCredentialsStmt, err = db.PrepareContext(ctx, listCredentials); err != nil {
return nil, fmt.Errorf("error preparing query ListCredentials: %w", err)
}
if q.listProfilesStmt, err = db.PrepareContext(ctx, listProfiles); err != nil {
return nil, fmt.Errorf("error preparing query ListProfiles: %w", err)
}
if q.listProvidersStmt, err = db.PrepareContext(ctx, listProviders); err != nil {
return nil, fmt.Errorf("error preparing query ListProviders: %w", err)
}
if q.updateConfigStmt, err = db.PrepareContext(ctx, updateConfig); err != nil {
return nil, fmt.Errorf("error preparing query UpdateConfig: %w", err)
}
if q.updateCredentialStmt, err = db.PrepareContext(ctx, updateCredential); err != nil {
return nil, fmt.Errorf("error preparing query UpdateCredential: %w", err)
}
if q.updateProfileStmt, err = db.PrepareContext(ctx, updateProfile); err != nil {
return nil, fmt.Errorf("error preparing query UpdateProfile: %w", err)
}
if q.updateProviderStmt, err = db.PrepareContext(ctx, updateProvider); err != nil {
return nil, fmt.Errorf("error preparing query UpdateProvider: %w", err)
}
if q.validateAuthStmt, err = db.PrepareContext(ctx, validateAuth); err != nil {
return nil, fmt.Errorf("error preparing query ValidateAuth: %w", err)
}
return &q, nil
}
func (q *Queries) Close() error {
var err error
if q.createConfigStmt != nil {
if cerr := q.createConfigStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing createConfigStmt: %w", cerr)
}
}
if q.createCredentialStmt != nil {
if cerr := q.createCredentialStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing createCredentialStmt: %w", cerr)
}
}
if q.createProfileStmt != nil {
if cerr := q.createProfileStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing createProfileStmt: %w", cerr)
}
}
if q.createProviderStmt != nil {
if cerr := q.createProviderStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing createProviderStmt: %w", cerr)
}
}
if q.deleteConfigByProfileIDStmt != nil {
if cerr := q.deleteConfigByProfileIDStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing deleteConfigByProfileIDStmt: %w", cerr)
}
}
if q.deleteConfigByProfileNameStmt != nil {
if cerr := q.deleteConfigByProfileNameStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing deleteConfigByProfileNameStmt: %w", cerr)
}
}
if q.deleteCredentialByIDStmt != nil {
if cerr := q.deleteCredentialByIDStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing deleteCredentialByIDStmt: %w", cerr)
}
}
if q.deleteCredentialByUsernameStmt != nil {
if cerr := q.deleteCredentialByUsernameStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing deleteCredentialByUsernameStmt: %w", cerr)
}
}
if q.deleteProfileByIDStmt != nil {
if cerr := q.deleteProfileByIDStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing deleteProfileByIDStmt: %w", cerr)
}
}
if q.deleteProfileByNameStmt != nil {
if cerr := q.deleteProfileByNameStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing deleteProfileByNameStmt: %w", cerr)
}
}
if q.deleteProviderByIDStmt != nil {
if cerr := q.deleteProviderByIDStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing deleteProviderByIDStmt: %w", cerr)
}
}
if q.deleteProviderByNameStmt != nil {
if cerr := q.deleteProviderByNameStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing deleteProviderByNameStmt: %w", cerr)
}
}
if q.getConfigByProfileIDStmt != nil {
if cerr := q.getConfigByProfileIDStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing getConfigByProfileIDStmt: %w", cerr)
}
}
if q.getConfigByProfileNameStmt != nil {
if cerr := q.getConfigByProfileNameStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing getConfigByProfileNameStmt: %w", cerr)
}
}
if q.getCredentialByIDStmt != nil {
if cerr := q.getCredentialByIDStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing getCredentialByIDStmt: %w", cerr)
}
}
if q.getCredentialByUsernameStmt != nil {
if cerr := q.getCredentialByUsernameStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing getCredentialByUsernameStmt: %w", cerr)
}
}
if q.getProfileByIDStmt != nil {
if cerr := q.getProfileByIDStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing getProfileByIDStmt: %w", cerr)
}
}
if q.getProfileByNameStmt != nil {
if cerr := q.getProfileByNameStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing getProfileByNameStmt: %w", cerr)
}
}
if q.getProviderByIDStmt != nil {
if cerr := q.getProviderByIDStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing getProviderByIDStmt: %w", cerr)
}
}
if q.getProviderByNameStmt != nil {
if cerr := q.getProviderByNameStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing getProviderByNameStmt: %w", cerr)
}
}
if q.listConfigsStmt != nil {
if cerr := q.listConfigsStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing listConfigsStmt: %w", cerr)
}
}
if q.listCredentialsStmt != nil {
if cerr := q.listCredentialsStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing listCredentialsStmt: %w", cerr)
}
}
if q.listProfilesStmt != nil {
if cerr := q.listProfilesStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing listProfilesStmt: %w", cerr)
}
}
if q.listProvidersStmt != nil {
if cerr := q.listProvidersStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing listProvidersStmt: %w", cerr)
}
}
if q.updateConfigStmt != nil {
if cerr := q.updateConfigStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing updateConfigStmt: %w", cerr)
}
}
if q.updateCredentialStmt != nil {
if cerr := q.updateCredentialStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing updateCredentialStmt: %w", cerr)
}
}
if q.updateProfileStmt != nil {
if cerr := q.updateProfileStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing updateProfileStmt: %w", cerr)
}
}
if q.updateProviderStmt != nil {
if cerr := q.updateProviderStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing updateProviderStmt: %w", cerr)
}
}
if q.validateAuthStmt != nil {
if cerr := q.validateAuthStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing validateAuthStmt: %w", cerr)
}
}
return err
}
func (q *Queries) exec(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) (sql.Result, error) {
switch {
case stmt != nil && q.tx != nil:
return q.tx.StmtContext(ctx, stmt).ExecContext(ctx, args...)
case stmt != nil:
return stmt.ExecContext(ctx, args...)
default:
return q.db.ExecContext(ctx, query, args...)
}
}
func (q *Queries) query(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) (*sql.Rows, error) {
switch {
case stmt != nil && q.tx != nil:
return q.tx.StmtContext(ctx, stmt).QueryContext(ctx, args...)
case stmt != nil:
return stmt.QueryContext(ctx, args...)
default:
return q.db.QueryContext(ctx, query, args...)
}
}
func (q *Queries) queryRow(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) *sql.Row {
switch {
case stmt != nil && q.tx != nil:
return q.tx.StmtContext(ctx, stmt).QueryRowContext(ctx, args...)
case stmt != nil:
return stmt.QueryRowContext(ctx, args...)
default:
return q.db.QueryRowContext(ctx, query, args...)
}
}
type Queries struct {
db DBTX
tx *sql.Tx
createConfigStmt *sql.Stmt
createCredentialStmt *sql.Stmt
createProfileStmt *sql.Stmt
createProviderStmt *sql.Stmt
deleteConfigByProfileIDStmt *sql.Stmt
deleteConfigByProfileNameStmt *sql.Stmt
deleteCredentialByIDStmt *sql.Stmt
deleteCredentialByUsernameStmt *sql.Stmt
deleteProfileByIDStmt *sql.Stmt
deleteProfileByNameStmt *sql.Stmt
deleteProviderByIDStmt *sql.Stmt
deleteProviderByNameStmt *sql.Stmt
getConfigByProfileIDStmt *sql.Stmt
getConfigByProfileNameStmt *sql.Stmt
getCredentialByIDStmt *sql.Stmt
getCredentialByUsernameStmt *sql.Stmt
getProfileByIDStmt *sql.Stmt
getProfileByNameStmt *sql.Stmt
getProviderByIDStmt *sql.Stmt
getProviderByNameStmt *sql.Stmt
listConfigsStmt *sql.Stmt
listCredentialsStmt *sql.Stmt
listProfilesStmt *sql.Stmt
listProvidersStmt *sql.Stmt
updateConfigStmt *sql.Stmt
updateCredentialStmt *sql.Stmt
updateProfileStmt *sql.Stmt
updateProviderStmt *sql.Stmt
validateAuthStmt *sql.Stmt
}
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
tx: tx,
createConfigStmt: q.createConfigStmt,
createCredentialStmt: q.createCredentialStmt,
createProfileStmt: q.createProfileStmt,
createProviderStmt: q.createProviderStmt,
deleteConfigByProfileIDStmt: q.deleteConfigByProfileIDStmt,
deleteConfigByProfileNameStmt: q.deleteConfigByProfileNameStmt,
deleteCredentialByIDStmt: q.deleteCredentialByIDStmt,
deleteCredentialByUsernameStmt: q.deleteCredentialByUsernameStmt,
deleteProfileByIDStmt: q.deleteProfileByIDStmt,
deleteProfileByNameStmt: q.deleteProfileByNameStmt,
deleteProviderByIDStmt: q.deleteProviderByIDStmt,
deleteProviderByNameStmt: q.deleteProviderByNameStmt,
getConfigByProfileIDStmt: q.getConfigByProfileIDStmt,
getConfigByProfileNameStmt: q.getConfigByProfileNameStmt,
getCredentialByIDStmt: q.getCredentialByIDStmt,
getCredentialByUsernameStmt: q.getCredentialByUsernameStmt,
getProfileByIDStmt: q.getProfileByIDStmt,
getProfileByNameStmt: q.getProfileByNameStmt,
getProviderByIDStmt: q.getProviderByIDStmt,
getProviderByNameStmt: q.getProviderByNameStmt,
listConfigsStmt: q.listConfigsStmt,
listCredentialsStmt: q.listCredentialsStmt,
listProfilesStmt: q.listProfilesStmt,
listProvidersStmt: q.listProvidersStmt,
updateConfigStmt: q.updateConfigStmt,
updateCredentialStmt: q.updateCredentialStmt,
updateProfileStmt: q.updateProfileStmt,
updateProviderStmt: q.updateProviderStmt,
validateAuthStmt: q.validateAuthStmt,
}
}

43
internal/db/init.go Normal file
View File

@@ -0,0 +1,43 @@
package db
import (
"context"
"database/sql"
_ "embed"
"fmt"
_ "github.com/mattn/go-sqlite3"
)
//go:embed schema.sql
var ddl string
var Query *Queries
func InitDB() (*sql.DB, error) {
ctx := context.Background()
db, err := sql.Open("sqlite3", "file:mantrae.db?mode=rwc&_journal=WAL&_fk=1&_sync=NORMAL")
if err != nil {
db.Close()
return nil, fmt.Errorf("failed to open database: %w", err)
}
// Check if the database is empty
var count int
err = db.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type='table'").Scan(&count)
if err != nil {
db.Close()
return nil, fmt.Errorf("failed to check database: %w", err)
}
if count == 0 {
if _, err := db.ExecContext(ctx, ddl); err != nil {
db.Close()
return nil, fmt.Errorf("failed to execute schema: %w", err)
}
}
Query = New(db)
return db, nil
}

38
internal/db/models.go Normal file
View File

@@ -0,0 +1,38 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
package db
type Config struct {
ProfileID int64 `json:"profile_id"`
Entrypoints interface{} `json:"entrypoints"`
Routers interface{} `json:"routers"`
Services interface{} `json:"services"`
Middlewares interface{} `json:"middlewares"`
Version *string `json:"version"`
}
type Credential struct {
ID int64 `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
}
type Profile struct {
ID int64 `json:"id"`
Name string `json:"name"`
Url string `json:"url"`
Username *string `json:"username"`
Password *string `json:"password"`
Tls bool `json:"tls"`
}
type Provider struct {
ID int64 `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
ExternalIp string `json:"external_ip"`
ApiKey string `json:"api_key"`
ApiUrl *string `json:"api_url"`
}

43
internal/db/querier.go Normal file
View File

@@ -0,0 +1,43 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
package db
import (
"context"
)
type Querier interface {
CreateConfig(ctx context.Context, arg CreateConfigParams) (Config, error)
CreateCredential(ctx context.Context, arg CreateCredentialParams) error
CreateProfile(ctx context.Context, arg CreateProfileParams) (Profile, error)
CreateProvider(ctx context.Context, arg CreateProviderParams) (Provider, error)
DeleteConfigByProfileID(ctx context.Context, profileID int64) error
DeleteConfigByProfileName(ctx context.Context, name string) error
DeleteCredentialByID(ctx context.Context, id int64) error
DeleteCredentialByUsername(ctx context.Context, username string) error
DeleteProfileByID(ctx context.Context, id int64) error
DeleteProfileByName(ctx context.Context, name string) error
DeleteProviderByID(ctx context.Context, id int64) error
DeleteProviderByName(ctx context.Context, name string) error
GetConfigByProfileID(ctx context.Context, profileID int64) (Config, error)
GetConfigByProfileName(ctx context.Context, name string) (Config, error)
GetCredentialByID(ctx context.Context, id int64) (Credential, error)
GetCredentialByUsername(ctx context.Context, username string) (Credential, error)
GetProfileByID(ctx context.Context, id int64) (Profile, error)
GetProfileByName(ctx context.Context, name string) (Profile, error)
GetProviderByID(ctx context.Context, id int64) (Provider, error)
GetProviderByName(ctx context.Context, name string) (Provider, error)
ListConfigs(ctx context.Context) ([]Config, error)
ListCredentials(ctx context.Context) ([]Credential, error)
ListProfiles(ctx context.Context) ([]Profile, error)
ListProviders(ctx context.Context) ([]Provider, error)
UpdateConfig(ctx context.Context, arg UpdateConfigParams) (Config, error)
UpdateCredential(ctx context.Context, arg UpdateCredentialParams) error
UpdateProfile(ctx context.Context, arg UpdateProfileParams) (Profile, error)
UpdateProvider(ctx context.Context, arg UpdateProviderParams) (Provider, error)
ValidateAuth(ctx context.Context, arg ValidateAuthParams) (ValidateAuthRow, error)
}
var _ Querier = (*Queries)(nil)

239
internal/db/query.sql Normal file
View File

@@ -0,0 +1,239 @@
-- name: GetProfileByID :one
SELECT
*
FROM
profiles
WHERE
id = ?
LIMIT
1;
-- name: GetProfileByName :one
SELECT
*
FROM
profiles
WHERE
name = ?
LIMIT
1;
-- name: ListProfiles :many
SELECT
*
FROM
profiles;
-- name: CreateProfile :one
INSERT INTO
profiles (name, url, username, password, tls)
VALUES
(?, ?, ?, ?, ?) RETURNING *;
-- name: UpdateProfile :one
UPDATE profiles
SET
name = ?,
url = ?,
username = ?,
password = ?,
tls = ?
WHERE
id = ? RETURNING *;
-- name: DeleteProfileByID :exec
DELETE FROM profiles
WHERE
id = ?;
-- name: DeleteProfileByName :exec
DELETE FROM profiles
WHERE
name = ?;
-- name: GetConfigByProfileID :one
SELECT
*
FROM
config
WHERE
profile_id = ?
LIMIT
1;
-- name: GetConfigByProfileName :one
SELECT
*
FROM
config
WHERE
profile_id = (
SELECT
id
FROM
profiles
WHERE
name = ?
)
LIMIT
1;
-- name: ListConfigs :many
SELECT
*
FROM
config;
-- name: CreateConfig :one
INSERT INTO
config (
profile_id,
entrypoints,
routers,
services,
middlewares,
version
)
VALUES
(?, ?, ?, ?, ?, ?) RETURNING *;
-- name: UpdateConfig :one
UPDATE config
SET
entrypoints = ?,
routers = ?,
services = ?,
middlewares = ?,
version = ?
WHERE
profile_id = ? RETURNING *;
-- name: DeleteConfigByProfileID :exec
DELETE FROM config
WHERE
profile_id = ?;
-- name: DeleteConfigByProfileName :exec
DELETE FROM config
WHERE
profile_id = (
SELECT
id
FROM
profiles
WHERE
name = ?
);
-- name: GetProviderByID :one
SELECT
*
FROM
providers
WHERE
id = ?
LIMIT
1;
-- name: GetProviderByName :one
SELECT
*
FROM
providers
WHERE
name = ?
LIMIT
1;
-- name: ListProviders :many
SELECT
*
FROM
providers;
-- name: CreateProvider :one
INSERT INTO
providers (name, type, external_ip, api_key, api_url)
VALUES
(?, ?, ?, ?, ?) RETURNING *;
-- name: UpdateProvider :one
UPDATE providers
SET
name = ?,
type = ?,
external_ip = ?,
api_key = ?,
api_url = ?
WHERE
id = ? RETURNING *;
-- name: DeleteProviderByID :exec
DELETE FROM providers
WHERE
id = ?;
-- name: DeleteProviderByName :exec
DELETE FROM providers
WHERE
name = ?;
-- name: GetCredentialByID :one
SELECT
*
FROM
credentials
WHERE
id = ?
LIMIT
1;
-- name: GetCredentialByUsername :one
SELECT
*
FROM
credentials
WHERE
username = ?
LIMIT
1;
-- name: ListCredentials :many
SELECT
*
FROM
credentials;
-- name: CreateCredential :exec
INSERT INTO
credentials (username, password)
VALUES
(?, ?);
-- name: UpdateCredential :exec
UPDATE credentials
SET
username = ?,
password = ?
WHERE
id = ?;
-- name: DeleteCredentialByID :exec
DELETE FROM credentials
WHERE
id = ?;
-- name: DeleteCredentialByUsername :exec
DELETE FROM credentials
WHERE
username = ?;
-- name: ValidateAuth :one
SELECT
id,
username
FROM
credentials
WHERE
username = ?
AND password = ?;

744
internal/db/query.sql.go Normal file
View File

@@ -0,0 +1,744 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// source: query.sql
package db
import (
"context"
)
const createConfig = `-- name: CreateConfig :one
INSERT INTO
config (
profile_id,
entrypoints,
routers,
services,
middlewares,
version
)
VALUES
(?, ?, ?, ?, ?, ?) RETURNING profile_id, entrypoints, routers, services, middlewares, version
`
type CreateConfigParams struct {
ProfileID int64 `json:"profile_id"`
Entrypoints interface{} `json:"entrypoints"`
Routers interface{} `json:"routers"`
Services interface{} `json:"services"`
Middlewares interface{} `json:"middlewares"`
Version *string `json:"version"`
}
func (q *Queries) CreateConfig(ctx context.Context, arg CreateConfigParams) (Config, error) {
row := q.queryRow(ctx, q.createConfigStmt, createConfig,
arg.ProfileID,
arg.Entrypoints,
arg.Routers,
arg.Services,
arg.Middlewares,
arg.Version,
)
var i Config
err := row.Scan(
&i.ProfileID,
&i.Entrypoints,
&i.Routers,
&i.Services,
&i.Middlewares,
&i.Version,
)
return i, err
}
const createCredential = `-- name: CreateCredential :exec
INSERT INTO
credentials (username, password)
VALUES
(?, ?)
`
type CreateCredentialParams struct {
Username string `json:"username"`
Password string `json:"password"`
}
func (q *Queries) CreateCredential(ctx context.Context, arg CreateCredentialParams) error {
_, err := q.exec(ctx, q.createCredentialStmt, createCredential, arg.Username, arg.Password)
return err
}
const createProfile = `-- name: CreateProfile :one
INSERT INTO
profiles (name, url, username, password, tls)
VALUES
(?, ?, ?, ?, ?) RETURNING id, name, url, username, password, tls
`
type CreateProfileParams struct {
Name string `json:"name"`
Url string `json:"url"`
Username *string `json:"username"`
Password *string `json:"password"`
Tls bool `json:"tls"`
}
func (q *Queries) CreateProfile(ctx context.Context, arg CreateProfileParams) (Profile, error) {
row := q.queryRow(ctx, q.createProfileStmt, createProfile,
arg.Name,
arg.Url,
arg.Username,
arg.Password,
arg.Tls,
)
var i Profile
err := row.Scan(
&i.ID,
&i.Name,
&i.Url,
&i.Username,
&i.Password,
&i.Tls,
)
return i, err
}
const createProvider = `-- name: CreateProvider :one
INSERT INTO
providers (name, type, external_ip, api_key, api_url)
VALUES
(?, ?, ?, ?, ?) RETURNING id, name, type, external_ip, api_key, api_url
`
type CreateProviderParams struct {
Name string `json:"name"`
Type string `json:"type"`
ExternalIp string `json:"external_ip"`
ApiKey string `json:"api_key"`
ApiUrl *string `json:"api_url"`
}
func (q *Queries) CreateProvider(ctx context.Context, arg CreateProviderParams) (Provider, error) {
row := q.queryRow(ctx, q.createProviderStmt, createProvider,
arg.Name,
arg.Type,
arg.ExternalIp,
arg.ApiKey,
arg.ApiUrl,
)
var i Provider
err := row.Scan(
&i.ID,
&i.Name,
&i.Type,
&i.ExternalIp,
&i.ApiKey,
&i.ApiUrl,
)
return i, err
}
const deleteConfigByProfileID = `-- name: DeleteConfigByProfileID :exec
DELETE FROM config
WHERE
profile_id = ?
`
func (q *Queries) DeleteConfigByProfileID(ctx context.Context, profileID int64) error {
_, err := q.exec(ctx, q.deleteConfigByProfileIDStmt, deleteConfigByProfileID, profileID)
return err
}
const deleteConfigByProfileName = `-- name: DeleteConfigByProfileName :exec
DELETE FROM config
WHERE
profile_id = (
SELECT
id
FROM
profiles
WHERE
name = ?
)
`
func (q *Queries) DeleteConfigByProfileName(ctx context.Context, name string) error {
_, err := q.exec(ctx, q.deleteConfigByProfileNameStmt, deleteConfigByProfileName, name)
return err
}
const deleteCredentialByID = `-- name: DeleteCredentialByID :exec
DELETE FROM credentials
WHERE
id = ?
`
func (q *Queries) DeleteCredentialByID(ctx context.Context, id int64) error {
_, err := q.exec(ctx, q.deleteCredentialByIDStmt, deleteCredentialByID, id)
return err
}
const deleteCredentialByUsername = `-- name: DeleteCredentialByUsername :exec
DELETE FROM credentials
WHERE
username = ?
`
func (q *Queries) DeleteCredentialByUsername(ctx context.Context, username string) error {
_, err := q.exec(ctx, q.deleteCredentialByUsernameStmt, deleteCredentialByUsername, username)
return err
}
const deleteProfileByID = `-- name: DeleteProfileByID :exec
DELETE FROM profiles
WHERE
id = ?
`
func (q *Queries) DeleteProfileByID(ctx context.Context, id int64) error {
_, err := q.exec(ctx, q.deleteProfileByIDStmt, deleteProfileByID, id)
return err
}
const deleteProfileByName = `-- name: DeleteProfileByName :exec
DELETE FROM profiles
WHERE
name = ?
`
func (q *Queries) DeleteProfileByName(ctx context.Context, name string) error {
_, err := q.exec(ctx, q.deleteProfileByNameStmt, deleteProfileByName, name)
return err
}
const deleteProviderByID = `-- name: DeleteProviderByID :exec
DELETE FROM providers
WHERE
id = ?
`
func (q *Queries) DeleteProviderByID(ctx context.Context, id int64) error {
_, err := q.exec(ctx, q.deleteProviderByIDStmt, deleteProviderByID, id)
return err
}
const deleteProviderByName = `-- name: DeleteProviderByName :exec
DELETE FROM providers
WHERE
name = ?
`
func (q *Queries) DeleteProviderByName(ctx context.Context, name string) error {
_, err := q.exec(ctx, q.deleteProviderByNameStmt, deleteProviderByName, name)
return err
}
const getConfigByProfileID = `-- name: GetConfigByProfileID :one
SELECT
profile_id, entrypoints, routers, services, middlewares, version
FROM
config
WHERE
profile_id = ?
LIMIT
1
`
func (q *Queries) GetConfigByProfileID(ctx context.Context, profileID int64) (Config, error) {
row := q.queryRow(ctx, q.getConfigByProfileIDStmt, getConfigByProfileID, profileID)
var i Config
err := row.Scan(
&i.ProfileID,
&i.Entrypoints,
&i.Routers,
&i.Services,
&i.Middlewares,
&i.Version,
)
return i, err
}
const getConfigByProfileName = `-- name: GetConfigByProfileName :one
SELECT
profile_id, entrypoints, routers, services, middlewares, version
FROM
config
WHERE
profile_id = (
SELECT
id
FROM
profiles
WHERE
name = ?
)
LIMIT
1
`
func (q *Queries) GetConfigByProfileName(ctx context.Context, name string) (Config, error) {
row := q.queryRow(ctx, q.getConfigByProfileNameStmt, getConfigByProfileName, name)
var i Config
err := row.Scan(
&i.ProfileID,
&i.Entrypoints,
&i.Routers,
&i.Services,
&i.Middlewares,
&i.Version,
)
return i, err
}
const getCredentialByID = `-- name: GetCredentialByID :one
SELECT
id, username, password
FROM
credentials
WHERE
id = ?
LIMIT
1
`
func (q *Queries) GetCredentialByID(ctx context.Context, id int64) (Credential, error) {
row := q.queryRow(ctx, q.getCredentialByIDStmt, getCredentialByID, id)
var i Credential
err := row.Scan(&i.ID, &i.Username, &i.Password)
return i, err
}
const getCredentialByUsername = `-- name: GetCredentialByUsername :one
SELECT
id, username, password
FROM
credentials
WHERE
username = ?
LIMIT
1
`
func (q *Queries) GetCredentialByUsername(ctx context.Context, username string) (Credential, error) {
row := q.queryRow(ctx, q.getCredentialByUsernameStmt, getCredentialByUsername, username)
var i Credential
err := row.Scan(&i.ID, &i.Username, &i.Password)
return i, err
}
const getProfileByID = `-- name: GetProfileByID :one
SELECT
id, name, url, username, password, tls
FROM
profiles
WHERE
id = ?
LIMIT
1
`
func (q *Queries) GetProfileByID(ctx context.Context, id int64) (Profile, error) {
row := q.queryRow(ctx, q.getProfileByIDStmt, getProfileByID, id)
var i Profile
err := row.Scan(
&i.ID,
&i.Name,
&i.Url,
&i.Username,
&i.Password,
&i.Tls,
)
return i, err
}
const getProfileByName = `-- name: GetProfileByName :one
SELECT
id, name, url, username, password, tls
FROM
profiles
WHERE
name = ?
LIMIT
1
`
func (q *Queries) GetProfileByName(ctx context.Context, name string) (Profile, error) {
row := q.queryRow(ctx, q.getProfileByNameStmt, getProfileByName, name)
var i Profile
err := row.Scan(
&i.ID,
&i.Name,
&i.Url,
&i.Username,
&i.Password,
&i.Tls,
)
return i, err
}
const getProviderByID = `-- name: GetProviderByID :one
SELECT
id, name, type, external_ip, api_key, api_url
FROM
providers
WHERE
id = ?
LIMIT
1
`
func (q *Queries) GetProviderByID(ctx context.Context, id int64) (Provider, error) {
row := q.queryRow(ctx, q.getProviderByIDStmt, getProviderByID, id)
var i Provider
err := row.Scan(
&i.ID,
&i.Name,
&i.Type,
&i.ExternalIp,
&i.ApiKey,
&i.ApiUrl,
)
return i, err
}
const getProviderByName = `-- name: GetProviderByName :one
SELECT
id, name, type, external_ip, api_key, api_url
FROM
providers
WHERE
name = ?
LIMIT
1
`
func (q *Queries) GetProviderByName(ctx context.Context, name string) (Provider, error) {
row := q.queryRow(ctx, q.getProviderByNameStmt, getProviderByName, name)
var i Provider
err := row.Scan(
&i.ID,
&i.Name,
&i.Type,
&i.ExternalIp,
&i.ApiKey,
&i.ApiUrl,
)
return i, err
}
const listConfigs = `-- name: ListConfigs :many
SELECT
profile_id, entrypoints, routers, services, middlewares, version
FROM
config
`
func (q *Queries) ListConfigs(ctx context.Context) ([]Config, error) {
rows, err := q.query(ctx, q.listConfigsStmt, listConfigs)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Config
for rows.Next() {
var i Config
if err := rows.Scan(
&i.ProfileID,
&i.Entrypoints,
&i.Routers,
&i.Services,
&i.Middlewares,
&i.Version,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listCredentials = `-- name: ListCredentials :many
SELECT
id, username, password
FROM
credentials
`
func (q *Queries) ListCredentials(ctx context.Context) ([]Credential, error) {
rows, err := q.query(ctx, q.listCredentialsStmt, listCredentials)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Credential
for rows.Next() {
var i Credential
if err := rows.Scan(&i.ID, &i.Username, &i.Password); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listProfiles = `-- name: ListProfiles :many
SELECT
id, name, url, username, password, tls
FROM
profiles
`
func (q *Queries) ListProfiles(ctx context.Context) ([]Profile, error) {
rows, err := q.query(ctx, q.listProfilesStmt, listProfiles)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Profile
for rows.Next() {
var i Profile
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Url,
&i.Username,
&i.Password,
&i.Tls,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listProviders = `-- name: ListProviders :many
SELECT
id, name, type, external_ip, api_key, api_url
FROM
providers
`
func (q *Queries) ListProviders(ctx context.Context) ([]Provider, error) {
rows, err := q.query(ctx, q.listProvidersStmt, listProviders)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Provider
for rows.Next() {
var i Provider
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Type,
&i.ExternalIp,
&i.ApiKey,
&i.ApiUrl,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateConfig = `-- name: UpdateConfig :one
UPDATE config
SET
entrypoints = ?,
routers = ?,
services = ?,
middlewares = ?,
version = ?
WHERE
profile_id = ? RETURNING profile_id, entrypoints, routers, services, middlewares, version
`
type UpdateConfigParams struct {
Entrypoints interface{} `json:"entrypoints"`
Routers interface{} `json:"routers"`
Services interface{} `json:"services"`
Middlewares interface{} `json:"middlewares"`
Version *string `json:"version"`
ProfileID int64 `json:"profile_id"`
}
func (q *Queries) UpdateConfig(ctx context.Context, arg UpdateConfigParams) (Config, error) {
row := q.queryRow(ctx, q.updateConfigStmt, updateConfig,
arg.Entrypoints,
arg.Routers,
arg.Services,
arg.Middlewares,
arg.Version,
arg.ProfileID,
)
var i Config
err := row.Scan(
&i.ProfileID,
&i.Entrypoints,
&i.Routers,
&i.Services,
&i.Middlewares,
&i.Version,
)
return i, err
}
const updateCredential = `-- name: UpdateCredential :exec
UPDATE credentials
SET
username = ?,
password = ?
WHERE
id = ?
`
type UpdateCredentialParams struct {
Username string `json:"username"`
Password string `json:"password"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateCredential(ctx context.Context, arg UpdateCredentialParams) error {
_, err := q.exec(ctx, q.updateCredentialStmt, updateCredential, arg.Username, arg.Password, arg.ID)
return err
}
const updateProfile = `-- name: UpdateProfile :one
UPDATE profiles
SET
name = ?,
url = ?,
username = ?,
password = ?,
tls = ?
WHERE
id = ? RETURNING id, name, url, username, password, tls
`
type UpdateProfileParams struct {
Name string `json:"name"`
Url string `json:"url"`
Username *string `json:"username"`
Password *string `json:"password"`
Tls bool `json:"tls"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateProfile(ctx context.Context, arg UpdateProfileParams) (Profile, error) {
row := q.queryRow(ctx, q.updateProfileStmt, updateProfile,
arg.Name,
arg.Url,
arg.Username,
arg.Password,
arg.Tls,
arg.ID,
)
var i Profile
err := row.Scan(
&i.ID,
&i.Name,
&i.Url,
&i.Username,
&i.Password,
&i.Tls,
)
return i, err
}
const updateProvider = `-- name: UpdateProvider :one
UPDATE providers
SET
name = ?,
type = ?,
external_ip = ?,
api_key = ?,
api_url = ?
WHERE
id = ? RETURNING id, name, type, external_ip, api_key, api_url
`
type UpdateProviderParams struct {
Name string `json:"name"`
Type string `json:"type"`
ExternalIp string `json:"external_ip"`
ApiKey string `json:"api_key"`
ApiUrl *string `json:"api_url"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateProvider(ctx context.Context, arg UpdateProviderParams) (Provider, error) {
row := q.queryRow(ctx, q.updateProviderStmt, updateProvider,
arg.Name,
arg.Type,
arg.ExternalIp,
arg.ApiKey,
arg.ApiUrl,
arg.ID,
)
var i Provider
err := row.Scan(
&i.ID,
&i.Name,
&i.Type,
&i.ExternalIp,
&i.ApiKey,
&i.ApiUrl,
)
return i, err
}
const validateAuth = `-- name: ValidateAuth :one
SELECT
id,
username
FROM
credentials
WHERE
username = ?
AND password = ?
`
type ValidateAuthParams struct {
Username string `json:"username"`
Password string `json:"password"`
}
type ValidateAuthRow struct {
ID int64 `json:"id"`
Username string `json:"username"`
}
func (q *Queries) ValidateAuth(ctx context.Context, arg ValidateAuthParams) (ValidateAuthRow, error) {
row := q.queryRow(ctx, q.validateAuthStmt, validateAuth, arg.Username, arg.Password)
var i ValidateAuthRow
err := row.Scan(&i.ID, &i.Username)
return i, err
}

43
internal/db/schema.sql Normal file
View File

@@ -0,0 +1,43 @@
-- db/schema.sql
CREATE TABLE profiles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(100) NOT NULL UNIQUE,
url TEXT NOT NULL,
username VARCHAR(100),
password TEXT,
tls BOOLEAN NOT NULL
);
CREATE TABLE config (
profile_id INTEGER NOT NULL,
entrypoints JSONB,
routers JSONB,
services JSONB,
middlewares JSONB,
version TEXT,
FOREIGN KEY (profile_id) REFERENCES profiles (id) ON DELETE CASCADE
);
CREATE TABLE providers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(100) NOT NULL UNIQUE,
type VARCHAR(50) NOT NULL,
external_ip TEXT NOT NULL,
api_key TEXT NOT NULL,
api_url TEXT
);
CREATE TABLE credentials (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username VARCHAR(100) NOT NULL UNIQUE,
password TEXT NOT NULL
);
-- Trigger to create an empty config when inserting a profile
CREATE TRIGGER add_config AFTER INSERT ON profiles FOR EACH ROW BEGIN
INSERT INTO
config (profile_id)
VALUES
(NEW.id);
END;

52
main.go
View File

@@ -2,8 +2,6 @@ package main
import (
"context"
"flag"
"fmt"
"log/slog"
"net/http"
"os"
@@ -13,6 +11,8 @@ import (
"time"
"github.com/MizuchiLabs/mantrae/internal/api"
"github.com/MizuchiLabs/mantrae/internal/config"
"github.com/MizuchiLabs/mantrae/internal/db"
"github.com/MizuchiLabs/mantrae/pkg/traefik"
"github.com/MizuchiLabs/mantrae/pkg/util"
"github.com/lmittmann/tint"
@@ -22,60 +22,30 @@ import (
func init() {
logger := slog.New(tint.NewHandler(os.Stdout, nil))
slog.SetDefault(logger)
if err := util.GenerateCreds(); err != nil {
slog.Error("Failed to generate creds", "error", err)
}
var profiles traefik.Profiles
if err := profiles.Load(); err != nil {
slog.Error("Failed to get traefik config", "error", err)
}
go traefik.GetTraefikConfig()
}
func main() {
version := flag.Bool("version", false, "Print version and exit")
port := flag.Int("port", 3000, "Port to listen on")
url := flag.String(
"url",
"",
"Specify the URL of the Traefik instance (e.g. http://localhost:8080)",
)
username := flag.String(
"username",
"",
"Specify the username for the Traefik instance",
)
password := flag.String(
"password",
"",
"Specify the password for the Traefik instance",
)
flag.Parse()
if *version {
fmt.Println(util.Version)
os.Exit(0)
db, err := db.InitDB()
if err != nil {
slog.Error("Failed to initialize database", "error", err)
return
}
defer db.Close() // Close the database connection when the program exits
if *url != "" {
var profiles traefik.Profiles
if err := profiles.SetDefaultProfile(*url, *username, *password); err != nil {
slog.Error("Failed to add default profile", "error", err)
return
}
}
flags := config.ParseFlags() // Parse command-line flags
util.SetDefaultAdminUser() // Set default admin user
// Start the background sync processes
go traefik.Sync()
// go dns.Sync()
srv := &http.Server{
Addr: ":" + strconv.Itoa(*port),
Addr: ":" + strconv.Itoa(flags.Port),
Handler: api.Routes(),
ReadHeaderTimeout: 5 * time.Second,
}
slog.Info("Listening on port", "port", *port)
slog.Info("Listening on port", "port", flags.Port)
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
slog.Error("ListenAndServe", "error", err)

View File

@@ -1,11 +1,13 @@
package dns
import (
"context"
"fmt"
"log/slog"
"regexp"
"time"
"github.com/MizuchiLabs/mantrae/internal/db"
"github.com/MizuchiLabs/mantrae/pkg/traefik"
)
@@ -53,10 +55,28 @@ func UpdateDNS() {
return
}
profiles, err := db.Query.ListProfiles(context.Background())
if err != nil {
slog.Error("Failed to get profiles", "error", err)
return
}
// Get all local
domains := make(map[string]string)
for _, profile := range traefik.ProfileData.Profiles {
for _, router := range profile.Dynamic.Routers {
for _, profile := range profiles {
config, err := db.Query.GetConfigByProfileID(context.Background(), profile.ID)
if err != nil {
slog.Error("Failed to get config", "error", err)
return
}
data, err := traefik.DecodeConfig(config)
if err != nil {
slog.Error("Failed to decode config", "error", err)
return
}
for _, router := range data.Routers {
if router.Provider == "http" {
domain, err := extractDomainFromRule(router.Rule)
if err != nil {

View File

@@ -1,6 +1,7 @@
package traefik
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
@@ -10,6 +11,7 @@ import (
"net/http"
"time"
"github.com/MizuchiLabs/mantrae/internal/db"
"github.com/traefik/genconf/dynamic"
)
@@ -107,8 +109,8 @@ func (r UDPRouter) ToRouter() *Router {
}
}
func getRouters[T Routerable](p Profile, endpoint string) map[string]Router {
body, err := p.fetch(endpoint)
func getRouters[T Routerable](profile db.Profile, endpoint string) map[string]Router {
body, err := fetch(profile, endpoint)
if err != nil {
slog.Error("Failed to get routers", "error", err)
return nil
@@ -126,6 +128,9 @@ func getRouters[T Routerable](p Profile, endpoint string) map[string]Router {
routers := make(map[string]Router, len(routerables))
for _, r := range routerables {
newRouter := r.ToRouter()
if newRouter.Name == "" {
continue
}
routers[newRouter.Name] = *newRouter
}
return routers
@@ -202,8 +207,8 @@ func (s UDPService) ToService() *Service {
}
}
func getServices[T Serviceable](p Profile, endpoint string) map[string]Service {
body, err := p.fetch(endpoint)
func getServices[T Serviceable](profile db.Profile, endpoint string) map[string]Service {
body, err := fetch(profile, endpoint)
if err != nil {
slog.Error("Failed to get services", "error", err)
return nil
@@ -219,6 +224,9 @@ func getServices[T Serviceable](p Profile, endpoint string) map[string]Service {
services := make(map[string]Service, len(serviceables))
for _, s := range serviceables {
newService := s.ToService()
if newService.Name == "" {
continue
}
services[newService.Name] = *newService
}
@@ -305,8 +313,8 @@ func (m TCPMiddleware) ToMiddleware() *Middleware {
}
}
func getMiddlewares[T Middlewareable](p Profile, endpoint string) map[string]Middleware {
body, err := p.fetch(endpoint)
func getMiddlewares[T Middlewareable](profile db.Profile, endpoint string) map[string]Middleware {
body, err := fetch(profile, endpoint)
if err != nil {
slog.Error("Failed to get middlewares", "error", err)
return nil
@@ -322,6 +330,9 @@ func getMiddlewares[T Middlewareable](p Profile, endpoint string) map[string]Mid
middlewares := make(map[string]Middleware, len(middlewareables))
for _, m := range middlewareables {
newMiddleware := m.ToMiddleware()
if newMiddleware.Name == "" {
continue
}
middlewares[newMiddleware.Name] = *newMiddleware
}
@@ -329,65 +340,76 @@ func getMiddlewares[T Middlewareable](p Profile, endpoint string) map[string]Mid
}
func GetTraefikConfig() {
for i, profile := range ProfileData.Profiles {
if profile.URL == "" {
profiles, err := db.Query.ListProfiles(context.Background())
if err != nil {
slog.Error("Failed to get profiles", "error", err)
return
}
for _, profile := range profiles {
if profile.Url == "" {
continue
}
d := Dynamic{
Entrypoints: make([]Entrypoint, 0),
Routers: make(map[string]Router),
Services: make(map[string]Service),
Middlewares: make(map[string]Middleware),
config, err := db.Query.GetConfigByProfileID(context.Background(), profile.ID)
if err != nil {
slog.Error("Failed to get config", "error", err)
return
}
// Retrieve routers
d.Routers = merge(
data, err := DecodeConfig(config)
if err != nil {
slog.Error("Failed to decode config", "error", err)
return
}
// Fetch routers
data.Routers = merge(
getRouters[HTTPRouter](profile, HTTPRouterAPI),
getRouters[TCPRouter](profile, TCPRouterAPI),
getRouters[UDPRouter](profile, UDPRouterAPI),
filterByLocalProvider(
profile.Dynamic.Routers,
data.Routers,
func(r Router) string { return r.Provider },
),
)
// Retrieve services
d.Services = merge(
// Fetch services
data.Services = merge(
getServices[HTTPService](profile, HTTPServiceAPI),
getServices[TCPService](profile, TCPServiceAPI),
getServices[UDPService](profile, UDPServiceAPI),
filterByLocalProvider(
profile.Dynamic.Services,
data.Services,
func(s Service) string { return s.Provider },
),
)
// Fetch middlewares
d.Middlewares = merge(
data.Middlewares = merge(
getMiddlewares[HTTPMiddleware](profile, HTTPMiddlewaresAPI),
getMiddlewares[TCPMiddleware](profile, TCPMiddlewaresAPI),
filterByLocalProvider(
profile.Dynamic.Middlewares,
data.Middlewares,
func(m Middleware) string { return m.Provider },
),
)
// Retrieve entrypoints
entrypoints, err := profile.fetch(EntrypointsAPI)
entrypoints, err := fetch(profile, 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 {
if err = json.NewDecoder(entrypoints).Decode(&data.Entrypoints); err != nil {
slog.Error("Failed to decode entrypoints", "error", err)
return
}
// Fetch version
version, err := profile.fetch(VersionAPI)
version, err := fetch(profile, VersionAPI)
if err != nil {
slog.Error("Failed to get version", "error", err)
return
@@ -402,14 +424,11 @@ func GetTraefikConfig() {
slog.Error("Failed to decode version", "error", err)
return
}
d.Version = v.Version
profile.Dynamic = d
ProfileData.Profiles[i] = profile
}
if err := ProfileData.Save(); err != nil {
slog.Error("Failed to save profiles", "error", err)
data.Version = v.Version
if err := UpdateConfig(config.ProfileID, data); err != nil {
slog.Error("Failed to update config", "error", err)
return
}
}
}
@@ -418,6 +437,7 @@ func Sync() {
ticker := time.NewTicker(time.Second * 60)
defer ticker.Stop()
GetTraefikConfig()
for range ticker.C {
GetTraefikConfig()
}
@@ -444,32 +464,31 @@ func merge[T any](maps ...map[string]T) map[string]T {
return merged
}
func (p Profile) fetch(endpoint string) (io.ReadCloser, error) {
if p.URL == "" || endpoint == "" {
func fetch(profile db.Profile, endpoint string) (io.ReadCloser, error) {
if profile.Url == "" {
return nil, fmt.Errorf("invalid URL or endpoint")
}
apiURL := p.URL + endpoint
client := http.Client{Timeout: time.Second * 10}
if !p.TLS {
if !profile.Tls {
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
}
req, err := http.NewRequest("GET", apiURL, nil)
req, err := http.NewRequest("GET", profile.Url+endpoint, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
if p.Username != "" && p.Password != "" {
req.SetBasicAuth(p.Username, p.Password)
if *profile.Username != "" && *profile.Password != "" {
req.SetBasicAuth(*profile.Username, *profile.Password)
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to fetch %s: %w", apiURL, err)
return nil, fmt.Errorf("failed to fetch %s: %w", profile.Url+endpoint, err)
}
if resp.StatusCode != http.StatusOK {

91
pkg/traefik/convert.go Normal file
View File

@@ -0,0 +1,91 @@
package traefik
import (
"context"
"encoding/json"
"github.com/MizuchiLabs/mantrae/internal/db"
)
func DecodeConfig(config db.Config) (*Dynamic, error) {
data := &Dynamic{
ProfileID: config.ProfileID,
Entrypoints: make([]Entrypoint, 0),
Routers: make(map[string]Router),
Services: make(map[string]Service),
Middlewares: make(map[string]Middleware),
Version: "",
}
if config.Entrypoints != nil {
if err := json.Unmarshal(config.Entrypoints.([]byte), &data.Entrypoints); err != nil {
return nil, err
}
}
if config.Routers != nil {
if err := json.Unmarshal(config.Routers.([]byte), &data.Routers); err != nil {
return nil, err
}
}
if config.Services != nil {
if err := json.Unmarshal(config.Services.([]byte), &data.Services); err != nil {
return nil, err
}
}
if config.Middlewares != nil {
if err := json.Unmarshal(config.Middlewares.([]byte), &data.Middlewares); err != nil {
return nil, err
}
}
if config.Version != nil {
data.Version = *config.Version
}
return data, nil
}
func UpdateConfig(profileID int64, data *Dynamic) error {
for _, r := range data.Routers {
if err := r.Verify(); err != nil {
return err
}
}
for _, s := range data.Services {
if err := s.Verify(); err != nil {
return err
}
}
for _, m := range data.Middlewares {
if err := m.Verify(); err != nil {
return err
}
}
entrypoints, err := json.Marshal(data.Entrypoints)
if err != nil {
return err
}
routers, err := json.Marshal(data.Routers)
if err != nil {
return err
}
services, err := json.Marshal(data.Services)
if err != nil {
return err
}
middlewares, err := json.Marshal(data.Middlewares)
if err != nil {
return err
}
if _, err := db.Query.UpdateConfig(context.Background(), db.UpdateConfigParams{
ProfileID: profileID,
Entrypoints: entrypoints,
Routers: routers,
Services: services,
Middlewares: middlewares,
Version: &data.Version,
}); err != nil {
return err
}
return nil
}

View File

@@ -3,26 +3,11 @@
package traefik
import (
"sync"
"github.com/traefik/genconf/dynamic"
)
type Profiles struct {
Profiles map[string]Profile `json:"profiles,omitempty"`
mu sync.RWMutex
}
type Profile struct {
Name string `json:"name"`
URL string `json:"url"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
TLS bool `json:"tls,omitempty"`
Dynamic Dynamic `json:"dynamic,omitempty"`
}
type Dynamic struct {
ProfileID int64 `json:"profile_id,omitempty"`
Entrypoints []Entrypoint `json:"entrypoints,omitempty"`
Routers map[string]Router `json:"routers,omitempty"`
Services map[string]Service `json:"services,omitempty"`

View File

@@ -1,149 +0,0 @@
package traefik
import (
"encoding/json"
"fmt"
"io"
"log"
"log/slog"
"os"
"path/filepath"
"strings"
)
// Global profiles variable, only loaded once
var ProfileData = Profiles{
Profiles: make(map[string]Profile),
}
func init() {
if err := ProfileData.Load(); err != nil {
log.Fatalf("Failed to load profiles: %v", err)
}
}
func (p *Profiles) Load() error {
p.mu.RLock()
cwd, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
path := filepath.Join(cwd, "profiles.json")
if _, err = os.Stat(path); os.IsNotExist(err) {
p.Profiles = make(map[string]Profile)
p.Profiles["default"] = Profile{Name: "default"}
p.mu.RUnlock()
if err = p.Save(); err != nil {
slog.Error("Failed to save profiles", "error", err)
}
return nil
}
file, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("failed to read profiles file: %w", err)
}
if err := json.Unmarshal(file, &p.Profiles); err != nil {
return fmt.Errorf("failed to unmarshal profiles: %w", err)
}
p.mu.RUnlock()
return nil
}
func (p *Profiles) Save() error {
p.mu.Lock()
defer p.mu.Unlock()
cwd, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
path := filepath.Join(cwd, "profiles.json")
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(p.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(), path); err != nil {
return fmt.Errorf("failed to move temp file: %w", err)
}
return nil
}
func (p *Profiles) SetDefaultProfile(url, username, password string) error {
if err := p.Load(); err != nil {
return fmt.Errorf("failed to load profiles: %w", err)
}
if len(p.Profiles) == 0 {
p.Profiles = make(map[string]Profile)
}
p.Profiles["default"] = Profile{
Name: "default",
URL: url,
Username: username,
Password: password,
TLS: false,
}
return p.Save()
}
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

@@ -12,21 +12,6 @@ import (
"github.com/MizuchiLabs/mantrae/pkg/util"
)
func (p *Profile) Verify() error {
if p.Name == "" {
return fmt.Errorf("profile name cannot be empty")
}
if p.URL != "" {
if !isValidURL(p.URL) {
return fmt.Errorf("invalid url")
}
} else {
return fmt.Errorf("url cannot be empty")
}
return nil
}
func (r *Router) Verify() error {
if r.Name == "" {
return fmt.Errorf("name cannot be empty")
@@ -50,6 +35,10 @@ func (s *Service) Verify() error {
if s.ServiceType == "" {
return fmt.Errorf("service type cannot be empty")
}
if len(s.LoadBalancer.Servers) == 0 || len(s.TCPLoadBalancer.Servers) == 0 ||
len(s.UDPLoadBalancer.Servers) == 0 {
return fmt.Errorf("servers cannot be empty")
}
s.Provider = "http"
s.Name = validateName(s.Name, s.Provider)

View File

@@ -2,82 +2,47 @@
package util
import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/json"
"log/slog"
"os"
"path/filepath"
"github.com/MizuchiLabs/mantrae/internal/db"
)
type Credentials struct {
Username string `json:"username"`
Password string `json:"password"`
Secret []byte `json:"secret"`
}
func randomPassword(length int) []byte {
func randomPassword(length int) string {
bytes := make([]byte, length)
if _, err := rand.Read(bytes); err != nil {
return nil
return ""
}
return bytes
return base64.RawURLEncoding.EncodeToString(bytes)[:length]
}
func GenerateCreds() error {
cwd, err := os.Getwd()
func SetDefaultAdminUser() {
// check if default admin user exists
creds, err := db.Query.GetCredentialByUsername(context.Background(), "admin")
if err != nil {
return err
}
credsPath := filepath.Join(cwd, "creds.json")
if _, err = os.Stat(credsPath); os.IsNotExist(err) {
username := "admin"
password := base64.StdEncoding.EncodeToString(randomPassword(32))
jsonCreds, err := json.MarshalIndent(Credentials{
Username: username,
password := randomPassword(32)
if err := db.Query.CreateCredential(context.Background(), db.CreateCredentialParams{
Username: "admin",
Password: password,
Secret: randomPassword(64),
}, "", " ")
if err != nil {
return err
}); err != nil {
slog.Error("Failed to create default admin user", "error", err)
}
if err := os.WriteFile(credsPath, jsonCreds, 0600); err != nil {
return err
}
slog.Info("Generated new credentials", "username", username, "password", password)
slog.Info("Generated default admin user", "username", "admin", "password", password)
return
}
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 {
// Validate credentials
if creds.Username != "admin" || creds.Password == "" {
password := randomPassword(32)
slog.Info("Invalid credentials, regenerating...")
if err := os.Remove(credsPath); err != nil {
return err
}
if err := GenerateCreds(); err != nil {
return err
if err := db.Query.UpdateCredential(context.Background(), db.UpdateCredentialParams{
Username: "admin",
Password: password,
}); err != nil {
slog.Error("Failed to update default admin user", "error", err)
}
slog.Info("Generated default admin user", "username", "admin", "password", password)
}
return nil
}

13
sqlc.yml Normal file
View File

@@ -0,0 +1,13 @@
version: "2"
sql:
- engine: "sqlite"
queries: "internal/db/query.sql"
schema: "internal/db/schema.sql"
gen:
go:
package: "db"
out: "internal/db"
emit_json_tags: true
emit_prepared_queries: true
emit_interface: true
emit_pointers_for_null_types: true

View File

@@ -1,43 +1,42 @@
import { goto } from '$app/navigation';
import { toast } from 'svelte-sonner';
import type { Profile } from './types/dynamic';
import type { Middleware } from './types/middlewares';
import type { Config, Profile } from './types/dynamic';
import { newMiddleware, type Middleware } from './types/middlewares';
import { derived, get, writable, type Writable } from 'svelte/store';
import { newRouter, newService, type Router, type Service } from './types/config';
import type { Provider } from './types/provider';
export const loggedIn = writable(false);
export const profile: Writable<Profile> = writable();
export const profiles: Writable<Record<string, Profile>> = writable();
export const provider: Writable<Record<string, Provider>> = writable();
export const config: Writable<Config> = writable();
export const profiles: Writable<Profile[]> = writable();
export const provider: Writable<Provider[]> = writable();
export const API_URL = import.meta.env.PROD ? '/api' : 'http://localhost:3000/api';
export const routers = derived(profile, ($profile) =>
Object.values($profile?.dynamic?.routers ?? [])
);
export const services = derived(profile, ($profile) =>
Object.values($profile?.dynamic?.services ?? [])
);
export const middlewares = derived(profile, ($profile) =>
Object.values($profile?.dynamic?.middlewares ?? [])
);
export const version = derived(profile, ($profile) => $profile?.dynamic?.version ?? '');
export const entrypoints = derived(profile, ($profile) => $profile?.dynamic?.entrypoints ?? []);
export const routers = derived(config, ($config) => Object.values($config?.routers ?? []));
export const services = derived(config, ($config) => Object.values($config?.services ?? []));
export const middlewares = derived(config, ($config) => Object.values($config?.middlewares ?? []));
export const version = derived(config, ($config) => $config?.version ?? '');
export const entrypoints = derived(config, ($config) => $config?.entrypoints ?? []);
async function handleRequest(endpoint: string, method: string, body?: any): Promise<any> {
async function handleRequest(
endpoint: string,
method: string,
body?: any
): Promise<Response | undefined> {
if (!get(loggedIn)) return;
const token = localStorage.getItem('token');
try {
const response = await fetch(`${API_URL}${endpoint}`, {
method: method,
body: body ? JSON.stringify(body) : undefined,
headers: { Authorization: `Bearer ${token}` }
});
return await response.json();
} catch (e: any) {
const response = await fetch(`${API_URL}${endpoint}`, {
method: method,
body: body ? JSON.stringify(body) : undefined,
headers: { Authorization: `Bearer ${token}` }
});
if (response.status === 200) {
return response;
} else {
toast.error('Request failed', {
description: e.message,
description: await response.text(),
duration: 3000
});
}
@@ -45,20 +44,20 @@ async function handleRequest(endpoint: string, method: string, body?: any): Prom
// Login ----------------------------------------------------------------------
export async function login(username: string, password: string) {
try {
const response = await fetch(`${API_URL}/login`, {
method: 'POST',
body: JSON.stringify({ username, password })
});
const response = await fetch(`${API_URL}/login`, {
method: 'POST',
body: JSON.stringify({ username, password })
});
if (response.status === 200) {
const { token } = await response.json();
localStorage.setItem('token', token);
loggedIn.set(true);
await getProfiles();
goto('/');
toast.success('Login successful');
} catch (e: any) {
} else {
toast.error('Login failed', {
description: e.message,
description: await response.text(),
duration: 3000
});
return;
@@ -72,134 +71,198 @@ export async function logout() {
// Profiles -------------------------------------------------------------------
export async function getProfiles() {
const response = await handleRequest('/profiles', 'GET');
profiles.set(response);
// Get saved profile
const savedProfile = localStorage.getItem('profile');
if (savedProfile !== null) {
getProfile(savedProfile);
}
if (!get(profile) && Object.keys(response).length > 0) {
getProfile(Object.keys(response)[0]);
}
}
export async function getProfile(name: string) {
const response = await handleRequest('/profile/' + name, 'GET');
profile.set(response);
localStorage.setItem('profile', response.name);
}
export async function createProfile(profile: Profile): Promise<void> {
const response = await handleRequest('/profiles', 'POST', profile);
const response = await handleRequest('/profile', 'GET');
if (response) {
profiles.set(response);
toast.success(`Profile ${profile.name} created`);
}
}
let data = await response.json();
profiles.set(data);
export async function updateProfile(name: string, p: Profile): Promise<void> {
if (!get(profile)) return;
const response = await handleRequest(`/profiles/${get(profile).name}`, 'PUT', p);
if (response) {
profile.set(response);
toast.success(`Profile ${name} updated`);
if (get(profile).name === name) {
localStorage.setItem('profile', response.name);
// Get saved profile
const profileID = parseInt(localStorage.getItem('profile') ?? '');
if (profileID) {
getProfile(profileID);
return;
}
if (data === undefined) return;
if (!get(profile) && data.length > 0) {
getProfile(data[0].id);
}
}
}
export async function deleteProfile(name: string): Promise<void> {
const response = await handleRequest(`/profiles/${name}`, 'DELETE');
export async function getProfile(id: number) {
const respProfile = await handleRequest(`/profile/${id}`, 'GET');
if (respProfile) {
let data = await respProfile.json();
profile.set(data);
localStorage.setItem('profile', data.id.toString());
} else {
localStorage.removeItem('profile');
return;
}
const respConfig = await handleRequest(`/config/${id}`, 'GET');
if (respConfig) {
let data = await respConfig.json();
config.set(data);
}
}
export async function createProfile(p: Profile): Promise<void> {
const response = await handleRequest('/profile', 'POST', p);
if (response) {
profiles.set(response);
toast.success(`Profile ${name} deleted`);
if (get(profile).name === name) {
let data = await response.json();
profiles.update((items) => [...(items ?? []), data]);
toast.success(`Profile ${data.name} created`);
const profileID = parseInt(localStorage.getItem('profile') ?? '');
if (!profileID) {
localStorage.setItem('profile', data.id.toString());
profile.set(data);
}
}
}
export async function updateProfile(p: Profile): Promise<void> {
const response = await handleRequest(`/profile`, 'PUT', p);
if (response) {
let data = await response.json();
profile.set(data);
profiles.update((items) => items.map((i) => (i.id === p.id ? data : i)));
toast.success(`Profile ${data.name} updated`);
if (get(profile).id === data.id) {
localStorage.setItem('profile', data.id.toString());
}
}
}
export async function deleteProfile(p: Profile): Promise<void> {
const response = await handleRequest(`/profile/${p.id}`, 'DELETE', p);
if (response) {
profiles.update((items) => items.filter((i) => i.id !== p.id));
toast.success(`Profile deleted`);
if (get(profile).id === p.id) {
profile.set({} as Profile);
localStorage.removeItem('profile');
}
}
}
// Providers ------------------------------------------------------------------
// Provider -------------------------------------------------------------------
export async function getProviders() {
const response = await handleRequest('/providers', 'GET');
provider.set(response);
}
export async function updateProvider(oldName: string, p: Provider): Promise<void> {
const response = await handleRequest(`/providers/${oldName}`, 'PUT', p);
provider.set(response);
toast.success(`Provider ${p.name} updated`);
}
export async function deleteProvider(name: string): Promise<void> {
const response = await handleRequest(`/providers/${name}`, 'DELETE');
provider.set(response);
toast.success(`Provider ${name} deleted`);
}
// Routers --------------------------------------------------------------------
export async function updateRouter(
oldName: string,
router: Router,
service: Service
): Promise<void> {
if (!get(profile)) return;
const resRouter = await handleRequest(`/routers/${get(profile).name}/${oldName}`, 'PUT', router);
if (resRouter) {
const resService = await handleRequest(
`/services/${get(profile).name}/${oldName}`,
'PUT',
service
);
if (resService) {
profile.set(resService);
toast.success(`Router ${router.name} updated`);
}
const response = await handleRequest('/provider', 'GET');
if (response) {
let data = await response.json();
provider.set(data);
}
}
export async function deleteRouter(name: string): Promise<void> {
if (!get(profile)) return;
await handleRequest(`/routers/${get(profile).name}/${name}`, 'DELETE');
const response = await handleRequest(`/services/${get(profile).name}/${name}`, 'DELETE');
profile.set(response);
toast.success(`Router ${name} deleted`);
export async function getProvider(id: number): Promise<Provider> {
const response = await handleRequest(`/provider/${id}`, 'GET');
if (response) {
let data = await response.json();
return data;
}
return {} as Provider;
}
// Middlewares ----------------------------------------------------------------
export async function updateMiddleware(
middleware: Middleware,
oldMiddleware: string
): Promise<void> {
if (!get(profile)) return;
const response = await handleRequest(
`/middlewares/${get(profile).name}/${oldMiddleware}`,
'PUT',
middleware
);
profile.set(response);
toast.success(`Middleware ${middleware.name} updated`);
export async function createProvider(p: Provider): Promise<void> {
const response = await handleRequest('/provider', 'POST', p);
if (response) {
let data = await response.json();
provider.update((items) => [...(items ?? []), data]);
toast.success(`Provider ${data.name} created`);
}
}
export async function deleteMiddleware(name: string): Promise<void> {
if (!get(profile)) return;
const response = await handleRequest(`/middlewares/${get(profile).name}/${name}`, 'DELETE');
profile.set(response);
toast.success(`Middleware ${name} deleted`);
export async function updateProvider(p: Provider): Promise<void> {
const response = await handleRequest(`/provider`, 'PUT', p);
if (response) {
let data = await response.json();
provider.update((items) => items.map((i) => (i.id === p.id ? data : i)));
toast.success(`Provider ${data.name} updated`);
}
}
export async function deleteProvider(id: number): Promise<void> {
const response = await handleRequest(`/provider/${id}`, 'DELETE');
if (response) {
provider.update((items) => items.filter((i) => i.id !== id));
toast.success(`Provider deleted`);
}
}
// Config ---------------------------------------------------------------------
export async function updateConfig(c: Config): Promise<void> {
const response = await handleRequest(`/config/${get(profile).id}`, 'PUT', c);
if (response) {
let data = await response.json();
config.set(data);
toast.success(`Config updated`);
}
}
// Helper functions -----------------------------------------------------------
export function getRouter(routerName: string): Router {
const router = get(profile)?.dynamic?.routers?.[routerName];
const router = get(config)?.routers?.[routerName];
return router ?? newRouter();
}
export function getService(serviceName: string): Service {
const service = get(profile)?.dynamic?.services?.[serviceName];
const service = get(config)?.services?.[serviceName];
return service ?? newService();
}
export function getMiddleware(middlewareName: string): Middleware {
const middleware = get(config)?.middlewares?.[middlewareName];
return middleware ?? newMiddleware();
}
function nameCheck(name: string): string {
return name.split('@')[0].toLowerCase() + '@http';
}
// Create or update a router
export async function upsertRouter(name: string, router: Router, service: Service): Promise<void> {
let data = get(config);
if (!data.routers) data.routers = {};
if (!data.services) data.services = {};
if (router.name !== name) {
delete data.routers[name];
delete data.services[name];
}
router.name = nameCheck(router.name);
service.name = router.name;
data.routers[router.name] = router;
data.services[router.name] = service;
await updateConfig(data);
}
// Create or update a middleware
export async function upsertMiddleware(name: string, middleware: Middleware): Promise<void> {
let data = get(config);
if (!data.middlewares) data.middlewares = {};
if (middleware.name !== name) {
delete data.middlewares[name];
}
middleware.name = nameCheck(middleware.name);
data.middlewares[middleware.name] = middleware;
await updateConfig(data);
}
// Delete a router by name
export async function deleteRouter(name: string): Promise<void> {
let data = get(config);
if (!data.routers || !data.services) return;
delete data.routers[name];
delete data.services[name];
await updateConfig(data);
}
// Delete a middleware by name
export async function deleteMiddleware(name: string): Promise<void> {
let data = get(config);
if (!data.middlewares) return;
delete data.middlewares[name];
await updateConfig(data);
}

View File

@@ -7,7 +7,7 @@
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 { updateMiddleware, middlewares } from '$lib/api';
import { upsertMiddleware, middlewares } from '$lib/api';
import { newMiddleware } from '$lib/types/middlewares';
import { LoadMiddlewareForm } from '../utils/middlewareModules';
import { onMount, SvelteComponent } from 'svelte';
@@ -15,13 +15,12 @@
let middleware = newMiddleware();
let isHTTP = middleware.middlewareType === 'http';
const create = () => {
const create = async () => {
if (middleware.type === '' || middleware.name === '' || isNameTaken) return;
middleware.name = middleware.name + '@' + middleware.provider;
if (isHTTP) middleware.middlewareType = 'http';
else middleware.middlewareType = 'tcp';
updateMiddleware(middleware, middleware.name);
await upsertMiddleware(middleware.name, middleware);
middleware = newMiddleware();
middlewareType = HTTPMiddlewareTypes[0];
};

View File

@@ -4,24 +4,21 @@
import { Button } from '$lib/components/ui/button/index.js';
import { Input } from '$lib/components/ui/input/index.js';
import { Label } from '$lib/components/ui/label/index.js';
import { profiles, createProfile } from '$lib/api';
import { createProfile } from '$lib/api';
import { newProfile } from '$lib/types/dynamic';
let profile = newProfile();
const create = () => {
if (profile.name === '' || isNameTaken) return;
const create = async () => {
if (profile.name === '') return;
// Strip trailing slashes
if (profile.url.endsWith('/')) {
profile.url = profile.url.slice(0, -1);
}
createProfile(profile);
await createProfile(profile);
profile = newProfile();
};
let isNameTaken = false;
$: isNameTaken = Object.keys($profiles).some((p) => p === profile.name);
const onKeydown = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
create();
@@ -50,9 +47,7 @@
name="name"
type="text"
bind:value={profile.name}
class={isNameTaken
? 'col-span-3 border-red-400 focus-visible:ring-0 focus-visible:ring-offset-0'
: 'col-span-3 focus-visible:ring-0 focus-visible:ring-offset-0'}
class="col-span-3 focus-visible:ring-0 focus-visible:ring-offset-0"
placeholder="Your profile name"
required
/>

View File

@@ -7,7 +7,7 @@
import { Button } from '$lib/components/ui/button';
import { newProvider, type Provider } from '$lib/types/provider';
import type { Selected } from 'bits-ui';
import { updateProvider } from '$lib/api';
import { createProvider } from '$lib/api';
let provider: Provider = newProvider();
const providerTypes: Selected<string>[] = [
@@ -15,15 +15,15 @@
{ label: 'PowerDNS', value: 'powerdns' }
];
const create = () => {
const create = async () => {
if (
provider.name === '' ||
provider.type === '' ||
provider.key === '' ||
provider.externalIP === ''
provider.api_key === '' ||
provider.external_ip === ''
)
return;
updateProvider(provider.name, provider);
await createProvider(provider);
provider = newProvider();
providerType = providerTypes[0];
};
@@ -84,7 +84,7 @@
type="text"
placeholder="The public IP address of the traefik instance"
class="col-span-3 focus-visible:ring-0 focus-visible:ring-offset-0"
bind:value={provider.externalIP}
bind:value={provider.external_ip}
required
/>
</div>
@@ -96,7 +96,7 @@
type="text"
placeholder="http://127.0.0.1:8081"
class="col-span-3 focus-visible:ring-0 focus-visible:ring-offset-0"
bind:value={provider.url}
bind:value={provider.api_url}
required
/>
</div>
@@ -107,7 +107,7 @@
name="key"
type="password"
class="col-span-3 focus-visible:ring-0 focus-visible:ring-offset-0"
bind:value={provider.key}
bind:value={provider.api_key}
placeholder="API Key of the provider"
required
/>

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 type { Selected } from 'bits-ui';
import { routers, entrypoints, middlewares, updateRouter } from '$lib/api';
import { routers, entrypoints, middlewares, upsertRouter } from '$lib/api';
import { newRouter, newService, type Router } from '$lib/types/config';
import RuleEditor from '../utils/ruleEditor.svelte';
import Service from '../forms/service.svelte';
@@ -17,8 +17,7 @@
const create = async () => {
if (router.name === '' || isNameTaken) return;
service.name = router.name.split('@')[0] + '@' + router.provider;
await updateRouter(router.name, router, service);
await upsertRouter(router.name, router, service);
router = newRouter();
service = newService();

View File

@@ -5,27 +5,26 @@
import { Input } from '$lib/components/ui/input/index.js';
import { Label } from '$lib/components/ui/label/index.js';
import { Badge } from '$lib/components/ui/badge/index.js';
import { deleteMiddleware, updateMiddleware, middlewares } from '$lib/api';
import { deleteMiddleware, upsertMiddleware, middlewares } from '$lib/api';
import type { Middleware } from '$lib/types/middlewares';
import { LoadMiddlewareForm } from '../utils/middlewareModules';
import { onMount, type SvelteComponent } from 'svelte';
export let middleware: Middleware;
let name = middleware.name.split('@')[0];
let originalName = middleware.name;
let middlewareCompare = $middlewares.filter((m) => m.name !== middleware.name);
let open = false;
const update = () => {
const update = async () => {
if (middleware.name === '' || isNameTaken) return;
let oldName = middleware.name;
middleware.name = name + '@' + middleware.provider;
updateMiddleware(middleware, oldName);
await upsertMiddleware(originalName, middleware);
originalName = middleware.name;
open = false;
};
// Check if middleware name is taken unless self
let isNameTaken = false;
$: isNameTaken = middlewareCompare.some((m) => m.name === name + '@' + middleware.provider);
$: isNameTaken = middlewareCompare.some((m) => m.name === middleware.name);
const onKeydown = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
@@ -70,7 +69,7 @@
id="name"
name="name"
type="text"
bind:value={name}
bind:value={middleware.name}
on:keydown={onKeydown}
class={isNameTaken
? 'col-span-3 border-red-400 focus-visible:ring-0 focus-visible:ring-offset-0'

View File

@@ -4,22 +4,20 @@
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 { deleteProfile, profiles, updateProfile } from '$lib/api';
import { deleteProfile, updateProfile } from '$lib/api';
import type { Profile } from '$lib/types/dynamic';
export let name: string;
export let profile: Profile;
let open = false;
let profileCompare = Object.keys($profiles).filter((p) => p !== name);
const update = () => {
const update = async () => {
// Strip trailing slashes
if ($profiles[name].url.endsWith('/')) {
$profiles[name].url = $profiles[name].url.slice(0, -1);
if (profile.url.endsWith('/')) {
profile.url = profile.url.slice(0, -1);
}
updateProfile(name, $profiles[name]);
await updateProfile(profile);
open = false;
};
let isNameTaken = false;
$: isNameTaken = profileCompare.some((p) => p === $profiles[name].name);
const onKeydown = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
update();
@@ -45,7 +43,7 @@
type="text"
class="col-span-3"
placeholder="Your profile name"
bind:value={$profiles[name].name}
bind:value={profile.name}
required
/>
</div>
@@ -55,7 +53,7 @@
name="url"
type="text"
class="col-span-3"
bind:value={$profiles[name].url}
bind:value={profile.url}
placeholder="URL of your client"
required
/>
@@ -66,7 +64,7 @@
name="username"
type="text"
class="col-span-3"
bind:value={$profiles[name].username}
bind:value={profile.username}
placeholder="Username of your client"
required
/>
@@ -77,18 +75,18 @@
name="password"
type="password"
class="col-span-3"
bind:value={$profiles[name].password}
bind:value={profile.password}
placeholder="Password of your client"
required
/>
</div>
<div class="flex items-center justify-end gap-4">
<Label for="tls" class="text-right">Verify Certificate?</Label>
<Switch name="tls" bind:checked={$profiles[name].tls} required />
<Switch name="tls" bind:checked={profile.tls} required />
</div>
</div>
<Dialog.Close class="flex w-full flex-row gap-2">
<Button type="submit" class="w-full bg-red-400" on:click={() => deleteProfile(name)}>
<Button type="submit" class="w-full bg-red-400" on:click={() => deleteProfile(profile)}>
Delete
</Button>
<Button type="submit" class="w-full" on:click={() => update()}>Save</Button>

View File

@@ -2,31 +2,29 @@
import * as Dialog from '$lib/components/ui/dialog/index.js';
import * as Card from '$lib/components/ui/card/index.js';
import { Button } from '$lib/components/ui/button/index.js';
import { Badge } from '$lib/components/ui/badge/index.js';
import { Input } from '$lib/components/ui/input/index.js';
import { Label } from '$lib/components/ui/label/index.js';
import type { Provider } from '$lib/types/provider';
import { updateProvider, provider } from '$lib/api';
import { updateProvider } from '$lib/api';
export let p: Provider;
let open = false;
let oldName = p.name;
let providerCompare = Object.keys($provider).filter((prov) => prov !== p.name);
const update = async () => {
if (p.name === '' || p.type === '' || p.key === '' || p.externalIP === '') return;
if (p.name === '' || p.type === '' || p.api_key === '' || p.external_ip === '') return;
// check if url starts with http:// or https://
if (p.type === 'powerdns' && !p.url?.startsWith('http://') && !p.url?.startsWith('https://')) {
p.url = 'http://' + p.url;
if (
p.type === 'powerdns' &&
!p.api_url?.startsWith('http://') &&
!p.api_url?.startsWith('https://')
) {
p.api_url = 'http://' + p.api_url;
}
updateProvider(oldName, p);
oldName = p.name;
await updateProvider(p);
open = false;
};
// Check if provider name is taken
let isNameTaken = false;
$: isNameTaken = providerCompare.some((prov) => prov === p.name);
const onKeydown = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
update();
@@ -43,7 +41,14 @@
<Dialog.Content class="no-scrollbar max-h-screen overflow-y-auto sm:max-w-[500px]">
<Card.Root class="mt-4">
<Card.Header>
<Card.Title>DNS Provider</Card.Title>
<Card.Title class="flex items-center justify-between gap-2">
<span>DNS Provider</span>
<div>
<Badge variant="secondary" class="bg-blue-400">
{p.type}
</Badge>
</div>
</Card.Title>
<Card.Description>Update your DNS provider.</Card.Description>
</Card.Header>
<Card.Content>
@@ -54,9 +59,7 @@
name="name"
type="text"
bind:value={p.name}
class={isNameTaken
? 'col-span-3 border-red-400 focus-visible:ring-0 focus-visible:ring-offset-0'
: 'col-span-3 focus-visible:ring-0 focus-visible:ring-offset-0'}
class="col-span-3 focus-visible:ring-0 focus-visible:ring-offset-0"
placeholder="Your profile name"
required
/>
@@ -68,7 +71,7 @@
type="text"
placeholder="The public IP address of the traefik instance"
class="col-span-3 focus-visible:ring-0 focus-visible:ring-offset-0"
bind:value={p.externalIP}
bind:value={p.external_ip}
required
/>
</div>
@@ -80,7 +83,7 @@
type="text"
placeholder="http://127.0.0.1:8081"
class="col-span-3 focus-visible:ring-0 focus-visible:ring-offset-0"
bind:value={p.url}
bind:value={p.api_url}
required
/>
</div>
@@ -91,7 +94,7 @@
name="key"
type="password"
class="col-span-3 focus-visible:ring-0 focus-visible:ring-offset-0"
bind:value={p.key}
bind:value={p.api_key}
placeholder="API Key of the provider"
required
/>

View File

@@ -7,22 +7,29 @@
import { Badge } from '$lib/components/ui/badge/index.js';
import { Input } from '$lib/components/ui/input/index.js';
import { Label } from '$lib/components/ui/label/index.js';
import { routers, entrypoints, middlewares, updateRouter, getService } from '$lib/api';
import { newService, type Router } from '$lib/types/config';
import {
routers,
entrypoints,
middlewares,
getService,
upsertRouter,
deleteRouter
} from '$lib/api';
import { type Router } from '$lib/types/config';
import RuleEditor from '../utils/ruleEditor.svelte';
import type { Selected } from 'bits-ui';
import Service from '../forms/service.svelte';
export let router: Router;
let service = getService(router.name) ?? newService();
let originalName = router.name;
let service = getService(router.name);
let routerCompare = $routers.filter((r) => r.name !== router.name);
let open = false;
const update = () => {
const update = async () => {
if (router.name === '' || isNameTaken) return;
// Extra check in case router name changed
service.name = router.name.split('@')[0] + '@' + router.provider;
updateRouter(router.service, router, service);
await upsertRouter(originalName, router, service);
originalName = router.name;
open = false;
};
@@ -166,8 +173,9 @@
<Service bind:service />
</Tabs.Content>
</Tabs.Root>
<Dialog.Close class="w-full">
<Button class="w-full" on:click={() => update()}>Save</Button>
<Dialog.Close class="grid grid-cols-2 items-center justify-between gap-2">
<Button class="bg-red-400" on:click={() => deleteRouter(router.name)}>Delete</Button>
<Button type="submit" on:click={() => update()}>Save</Button>
</Dialog.Close>
</Dialog.Content>
</Dialog.Root>

View File

@@ -8,8 +8,8 @@
import { profiles, profile, getProfile } from '$lib/api';
let open = false;
function handleProfileClick(name: string) {
getProfile(name);
function handleProfileClick(id: number) {
getProfile(id);
open = false;
}
</script>
@@ -32,12 +32,12 @@
<Command.Input placeholder="Search profile..." />
<Command.Empty>No profile found.</Command.Empty>
<Command.Group>
{#each Object.keys($profiles) ?? [] as name}
{#each $profiles ?? [] as profile}
<Command.Item class="flex w-full flex-row items-center justify-between">
<span class="w-full py-2" on:click={() => handleProfileClick(name)} aria-hidden
>{name}</span
<span class="w-full py-2" on:click={() => handleProfileClick(profile.id)} aria-hidden
>{profile.name}</span
>
<UpdateProfile {name} />
<UpdateProfile {profile} />
</Command.Item>
{/each}
<Command.Item>

View File

@@ -12,12 +12,12 @@
},
{
name: 'Middlewares',
path: '/middlewares',
path: '/middlewares/',
icon: 'fa6-solid:layer-group'
},
{
name: 'DNS',
path: '/dns',
path: '/dns/',
icon: 'fa6-solid:earth-americas'
}
];

View File

@@ -3,12 +3,21 @@ import type { Middleware } from './middlewares';
import type { CertAndStores, Options, Store } from './tls';
export interface Profile {
id: number;
name: string;
url: string;
username: string;
password: string;
tls: boolean;
dynamic?: Dynamic;
}
export interface Config {
profile_id: number;
entrypoints?: Entrypoint[];
routers?: Record<string, Router>;
services?: Record<string, Service>;
middlewares?: Record<string, Middleware>;
version?: string;
}
export interface Dynamic {
@@ -37,11 +46,11 @@ export interface TLSConfiguration {
export const newProfile = (): Profile => {
return {
id: 0,
name: '',
url: '',
username: '',
password: '',
tls: false,
dynamic: {}
tls: false
};
};

View File

@@ -1,17 +1,19 @@
export interface Provider {
id: number;
name: string;
type: string;
externalIP: string;
key?: string;
url?: string;
external_ip: string;
api_key?: string;
api_url?: string;
}
export function newProvider(): Provider {
return {
id: 0,
name: '',
type: 'cloudflare',
externalIP: '',
key: '',
url: ''
external_ip: '',
api_key: '',
api_url: ''
};
}

View File

@@ -28,7 +28,7 @@
>
<div class="mb-6 flex flex-row items-center justify-between">
<Profile />
<Button variant="default" href={`${API_URL}/${$profile?.name}`}>
<Button variant="default" href={`${API_URL}/${$profile?.id}`}>
Download Config
<iconify-icon icon="fa6-solid:download" class="ml-2" />
</Button>

View File

@@ -10,8 +10,8 @@
middlewares,
routers,
services,
updateRouter,
getService
getService,
upsertRouter
} from '$lib/api';
import CreateRouter from '$lib/components/modals/createRouter.svelte';
import UpdateRouter from '$lib/components/modals/updateRouter.svelte';
@@ -47,7 +47,7 @@
? router.provider?.toLowerCase() === part.split(':')[1]
: part.startsWith('@type:')
? router.routerType.toLowerCase() === part.split(':')[1]
: router.service.toLowerCase().includes(part)
: router.name.toLowerCase().includes(part)
);
});
@@ -86,13 +86,13 @@
if (item === undefined) return;
router.entrypoints = item.map((i) => i.value) as string[];
let service = getService(router.name);
updateRouter(router.name, router, service);
upsertRouter(router.name, router, service);
};
const toggleMiddleware = (router: Router, item: Selected<unknown>[] | undefined) => {
if (item === undefined) return;
router.middlewares = item.map((i) => i.value) as string[];
let service = getService(router.name);
updateRouter(router.name, router, service);
upsertRouter(router.name, router, service);
};
const getSelectedEntrypoints = (router: Router): Selected<unknown>[] => {
let list = router?.entrypoints?.map((entrypoint) => {

View File

@@ -4,29 +4,38 @@
import { Button } from '$lib/components/ui/button';
import CreateProvider from '$lib/components/modals/createProvider.svelte';
import UpdateProvider from '$lib/components/modals/updateProvider.svelte';
import { deleteProvider, provider } from '$lib/api';
import { deleteProvider, getProviders, provider } from '$lib/api';
import { onMount } from 'svelte';
onMount(() => {
if ($provider === undefined) {
getProviders();
}
});
</script>
<CreateProvider />
<div class="flex flex-row items-center gap-2">
{#each Object.values($provider ?? []) as p}
<Card.Root class="w-[400px]">
<Card.Header>
<Card.Title class="flex items-center justify-between gap-2">
<span>{p.name}</span>
<Badge variant="secondary" class="bg-blue-400">
{p.type}
</Badge>
</Card.Title>
</Card.Header>
<Card.Content class="space-y-2"></Card.Content>
<Card.Footer class="grid grid-cols-2 items-center gap-2">
<Button variant="ghost" class="w-full bg-red-400" on:click={() => deleteProvider(p.name)}
>Delete</Button
>
<UpdateProvider {p} />
</Card.Footer>
</Card.Root>
{/each}
{#if $provider}
{#each $provider as p}
<Card.Root class="w-[400px]">
<Card.Header>
<Card.Title class="flex items-center justify-between gap-2">
<span>{p.name}</span>
<Badge variant="secondary" class="bg-blue-400">
{p.type}
</Badge>
</Card.Title>
</Card.Header>
<Card.Content class="space-y-2"></Card.Content>
<Card.Footer class="grid grid-cols-2 items-center gap-2">
<Button variant="ghost" class="w-full bg-red-400" on:click={() => deleteProvider(p.id)}
>Delete</Button
>
<UpdateProvider {p} />
</Card.Footer>
</Card.Root>
{/each}
{/if}
</div>