feat(wireguard): selektiver .conf-Import — Auswahl einzelner Interfaces per Modal

- GET /wireguard/importable listet alle .conf-Dateien in /etc/wireguard/
  mit already_in-Flag (bereits in DB)
- POST /wireguard/import nimmt optionale Names-Liste; ohne Namen → alles
- edgeguard-ctl wg-import [iface…] importiert nur die genannten Interfaces
- UI: Checkbox-Modal mit "Alle neuen auswählen" + Einzelauswahl;
  bereits importierte Interfaces disabled + grüner Tag

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-24 22:09:37 +02:00
parent ac068bc9dd
commit 8d7a43bc8c
10 changed files with 230 additions and 13 deletions

View File

@@ -47,6 +47,68 @@ func NewImporter(ifaces *InterfacesRepo, peers *PeersRepo, box *secrets.Box) *Im
return &Importer{Ifaces: ifaces, Peers: peers, Box: box}
}
// ImportableEntry describes one .conf file that can be imported.
type ImportableEntry struct {
Name string `json:"name"` // interface name without .conf
AlreadyIn bool `json:"already_in"` // already present in the DB
}
// ListImportable scans dir for *.conf files and reports which are new
// (not yet in the DB) and which are already imported.
func (im *Importer) ListImportable(ctx context.Context, dir string) ([]ImportableEntry, error) {
entries, err := os.ReadDir(dir)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil, nil
}
return nil, err
}
existing, err := im.Ifaces.List(ctx)
if err != nil {
return nil, err
}
inDB := make(map[string]bool, len(existing))
for _, x := range existing {
inDB[x.Name] = true
}
var out []ImportableEntry
for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".conf") {
continue
}
name := strings.TrimSuffix(e.Name(), ".conf")
if !validIfaceName(name) {
continue
}
out = append(out, ImportableEntry{Name: name, AlreadyIn: inDB[name]})
}
return out, nil
}
// ImportSelected imports only the named interfaces from dir.
// An empty names slice is treated as "import all" (same as ImportDir).
func (im *Importer) ImportSelected(ctx context.Context, dir string, names []string) (*ImportResult, error) {
if len(names) == 0 {
return im.ImportDir(ctx, dir)
}
res := &ImportResult{}
want := make(map[string]bool, len(names))
for _, n := range names {
want[n] = true
}
for name := range want {
if !validIfaceName(name) {
res.Skipped = append(res.Skipped, name+" (invalid name)")
continue
}
path := filepath.Join(dir, name+".conf")
if err := im.importFile(ctx, name, path, res); err != nil {
res.Skipped = append(res.Skipped, name+": "+err.Error())
}
}
return res, nil
}
func (im *Importer) ImportDir(ctx context.Context, dir string) (*ImportResult, error) {
res := &ImportResult{}
entries, err := os.ReadDir(dir)