mirror of
https://github.com/MizuchiLabs/mantrae.git
synced 2026-01-06 06:19:57 -06:00
huge refactor, switching to sqlite
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -24,5 +24,8 @@ mantrae
|
||||
# Go workspace file
|
||||
go.work
|
||||
*.json
|
||||
|
||||
*.db
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
.envrc
|
||||
dist/
|
||||
|
||||
23
README.md
23
README.md
@@ -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.
|
||||
|
||||
@@ -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
1
go.mod
@@ -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
2
go.sum
@@ -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=
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
78
internal/config/flags.go
Normal 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
368
internal/db/db.go
Normal 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
43
internal/db/init.go
Normal 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
38
internal/db/models.go
Normal 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
43
internal/db/querier.go
Normal 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
239
internal/db/query.sql
Normal 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
744
internal/db/query.sql.go
Normal 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
43
internal/db/schema.sql
Normal 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
52
main.go
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
91
pkg/traefik/convert.go
Normal 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
|
||||
}
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
13
sqlc.yml
Normal 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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
};
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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: ''
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user