").appendTo(i),n=i.uniqueId().attr("id");return this._addClass(s,"ui-tooltip-content"),this._addClass(i,"ui-tooltip","ui-widget ui-widget-content"),i.appendTo(this._appendTo(e)),this.tooltips[n]={element:e,tooltip:i}},_find:function(t){var e=t.data("ui-tooltip-id");return e?this.tooltips[e]:null},_removeTooltip:function(t){t.remove(),delete this.tooltips[t.attr("id")]},_appendTo:function(t){var e=t.closest(".ui-front, dialog");return e.length||(e=this.document[0].body),e},_destroy:function(){var e=this;t.each(this.tooltips,function(i,s){var n=t.Event("blur"),o=s.element;n.target=n.currentTarget=o[0],e.close(n,!0),t("#"+i).remove(),o.data("ui-tooltip-title")&&(o.attr("title")||o.attr("title",o.data("ui-tooltip-title")),o.removeData("ui-tooltip-title"))}),this.liveRegion.remove()}}),t.uiBackCompat!==!1&&t.widget("ui.tooltip",t.ui.tooltip,{options:{tooltipClass:null},_tooltip:function(){var t=this._superApply(arguments);return this.options.tooltipClass&&t.tooltip.addClass(this.options.tooltipClass),t}}),t.ui.tooltip});
\ No newline at end of file
diff --git a/assets/tpl/admin_create_clients.html b/assets/tpl/admin_create_clients.html
deleted file mode 100644
index 3d59b41..0000000
--- a/assets/tpl/admin_create_clients.html
+++ /dev/null
@@ -1,73 +0,0 @@
-
-
-
-
-
-
{{ .Static.WebsiteTitle }} - Admin
-
-
-
-
-
-
-
-
-
-
- {{template "prt_nav.html" .}}
-
-
Create new clients
-
Enter valid user email addresses to quickly create new accounts.
- {{template "prt_flashes.html" .}}
-
-
- {{template "prt_footer.html" .}}
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/assets/tpl/admin_edit_client.html b/assets/tpl/admin_edit_client.html
deleted file mode 100644
index d7d5cbf..0000000
--- a/assets/tpl/admin_edit_client.html
+++ /dev/null
@@ -1,221 +0,0 @@
-
-
-
-
-
-
{{ .Static.WebsiteTitle }} - Admin
-
-
-
-
-
-
-
- {{template "prt_nav.html" .}}
-
- {{template "prt_flashes.html" .}}
-
-
- {{if eq .Device.Type "server"}}
- {{if .Peer.IsNew}}
-
Create a new client
- {{else}}
-
Edit client: {{.Peer.Identifier}}
- {{end}}
-
-
- {{end}}
-
-
- {{if eq .Device.Type "client"}}
- {{if .Peer.IsNew}}
-
Create a new remote endpoint
- {{else}}
-
Edit remote endpoint: {{.Peer.Identifier}}
- {{end}}
-
-
- {{end}}
-
- {{template "prt_footer.html" .}}
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/assets/tpl/admin_edit_interface.html b/assets/tpl/admin_edit_interface.html
deleted file mode 100644
index 737f006..0000000
--- a/assets/tpl/admin_edit_interface.html
+++ /dev/null
@@ -1,263 +0,0 @@
-
-
-
-
-
-
{{ .Static.WebsiteTitle }} - Admin
-
-
-
-
-
-
-
- {{template "prt_nav.html" .}}
-
-
Edit interface {{.Device.DeviceName}}
- {{template "prt_flashes.html" .}}
-
-
-
-
-
- {{template "prt_footer.html" .}}
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/assets/tpl/admin_edit_user.html b/assets/tpl/admin_edit_user.html
deleted file mode 100644
index 1218ca5..0000000
--- a/assets/tpl/admin_edit_user.html
+++ /dev/null
@@ -1,95 +0,0 @@
-
-
-
-
-
-
{{ .Static.WebsiteTitle }} - Users
-
-
-
-
-
-
-
- {{template "prt_nav.html" .}}
-
- {{if eq .User.CreatedAt .Epoch}}
-
Create a new user
- {{else}}
-
Edit user {{.User.Email}}
- {{end}}
-
- {{template "prt_flashes.html" .}}
-
-
-
- {{template "prt_footer.html" .}}
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/assets/tpl/admin_index.html b/assets/tpl/admin_index.html
deleted file mode 100644
index f4e5544..0000000
--- a/assets/tpl/admin_index.html
+++ /dev/null
@@ -1,278 +0,0 @@
-
-
-
-
-
-
{{ .Static.WebsiteTitle }} - Admin
-
-
-
-
-
-
-
- {{template "prt_nav.html" .}}
-
-
WireGuard VPN Administration
- {{template "prt_flashes.html" .}}
-
-
-
-
- {{if eq $.Device.Type "server"}}
-
-
-
-
- Public Key:
- {{.Device.PublicKey}}
-
-
- Public Endpoint:
- {{.Device.DefaultEndpoint}}
-
-
- Listening Port:
- {{.Device.ListenPort}}
-
-
- Enabled Peers:
- {{len .Device.Interface.Peers}}
-
-
- Total Peers:
- {{.TotalPeers}}
-
-
-
-
-
-
-
-
- IP Address:
- {{.Device.IPsStr}}
-
-
- Default allowed IP's:
- {{.Device.DefaultAllowedIPsStr}}
-
-
- Default DNS servers:
- {{.Device.DNSStr}}
-
-
- Default MTU:
- {{.Device.Mtu}}
-
-
- Default Keepalive Interval:
- {{.Device.DefaultPersistentKeepalive}}
-
-
-
-
- {{end}}
- {{if eq $.Device.Type "client"}}
-
-
-
-
- Public Key:
- {{.Device.PublicKey}}
-
-
- Enabled Endpoints:
- {{len .Device.Interface.Peers}}
-
-
- Total Endpoints:
- {{.TotalPeers}}
-
-
-
-
-
-
-
-
- IP Address:
- {{.Device.IPsStr}}
-
-
- DNS servers:
- {{.Device.DNSStr}}
-
-
- Default MTU:
- {{.Device.Mtu}}
-
-
-
-
- {{end}}
-
-
-
-
-
-
- {{if eq $.Device.Type "server"}}
-
Current VPN Peers
- {{end}}
- {{if eq $.Device.Type "client"}}
- Current VPN Endpoints
- {{end}}
-
-
-
- {{if eq $.Device.Type "server"}}
-
- {{end}}
-
-
-
-
-
-
-
-
- Identifier
- Public Key
- {{if eq $.Device.Type "server"}}
- E-Mail
- {{end}}
- {{if eq $.Device.Type "server"}}
- IP's
- {{end}}
- {{if eq $.Device.Type "client"}}
- Endpoint
- {{end}}
- Handshake
-
-
-
-
- {{range $i, $p :=.Peers}}
- {{$peerUser:=(userForEmail $.Users $p.Email)}}
-
-
-
-
-
-
- {{$p.Identifier}}{{if $p.WillExpire}} {{end}}
- {{$p.PublicKey}}
- {{if eq $.Device.Type "server"}}
- {{$p.Email}}
- {{end}}
- {{if eq $.Device.Type "server"}}
- {{$p.IPsStr}}
- {{end}}
- {{if eq $.Device.Type "client"}}
- {{$p.Endpoint}}
- {{end}}
- {{$p.LastHandshake}}
-
- {{if eq $.Session.IsAdmin true}}
-
- {{end}}
-
-
-
-
-
-
-
-
-
-
-
User details
- {{if not $peerUser}}
-
No user information available...
- {{else}}
-
- Firstname: {{$peerUser.Firstname}}
- Lastname: {{$peerUser.Lastname}}
- Phone: {{$peerUser.Phone}}
- Mail: {{$peerUser.Email}}
-
- {{end}}
-
Connection / Traffic
- {{if not $p.Peer}}
-
No Traffic data available...
- {{else}}
-
{{if $p.DeactivatedAt}}-{{else}} {{$p.Peer.Endpoint}}{{end}}
-
{{if $p.DeactivatedAt}}-{{else}} {{formatBytes $p.Peer.ReceiveBytes}} / {{formatBytes $p.Peer.TransmitBytes}}{{end}}
- {{end}}
-
- {{if eq $.Device.Type "server"}}
-
- {{end}}
-
-
-
-
- {{if eq $.Device.Type "server"}}
-
- {{end}}
-
-
- {{if $p.DeactivatedAt}}
-
Peer is disabled!
- {{end}}
- {{if $p.WillExpire}}
-
Peer will expire on {{ formatDate $p.ExpiresAt}}
- {{end}}
- {{if eq $.Device.Type "server"}}
-
- {{end}}
-
-
-
-
-
- {{end}}
-
-
-
Currently listed peers: {{len .Peers}}
-
-
- {{template "prt_footer.html" .}}
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/assets/tpl/admin_user_index.html b/assets/tpl/admin_user_index.html
deleted file mode 100644
index 87dd814..0000000
--- a/assets/tpl/admin_user_index.html
+++ /dev/null
@@ -1,69 +0,0 @@
-
-
-
-
-
-
{{ .Static.WebsiteTitle }} - Users
-
-
-
-
-
-
-
- {{template "prt_nav.html" .}}
-
-
WireGuard VPN Users
- {{template "prt_flashes.html" .}}
-
-
-
-
-
- E-Mail
- Lastname
- Firstname
- Source
- Is Admin
-
-
-
-
- {{range $i, $u :=.Users}}
-
- {{$u.Email}}
- {{$u.Lastname}}
- {{$u.Firstname}}
- {{$u.Source}}
- {{if $u.IsAdmin}}True{{else}}False{{end}}
-
- {{if eq $.Session.IsAdmin true}}
- {{if eq $u.Source "db"}}
-
- {{end}}
- {{end}}
-
-
- {{end}}
-
-
-
Currently listed users: {{len .Users}}
-
-
- {{template "prt_footer.html" .}}
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/assets/tpl/error.html b/assets/tpl/error.html
deleted file mode 100644
index b2cda50..0000000
--- a/assets/tpl/error.html
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
-
{{ .Static.WebsiteTitle }} - Error
-
-
-
-
-
-
-
- {{template "prt_nav.html" .}}
-
- {{template "prt_footer.html" .}}
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/assets/tpl/index.html b/assets/tpl/index.html
deleted file mode 100644
index 10d93c5..0000000
--- a/assets/tpl/index.html
+++ /dev/null
@@ -1,89 +0,0 @@
-
-
-
-
-
-
-
{{ .Static.WebsiteTitle }}
-
-
-
-
-
-
-
- {{template "prt_nav.html" .}}
-
-
- {{template "prt_flashes.html" .}}
-
WireGuard® is an extremely simple yet fast and modern VPN that utilizes state-of-the-art cryptography. It aims to be faster, simpler, leaner, and more useful than IPsec, while avoiding the massive headache. It intends to be considerably more performant than OpenVPN.
-
More Information
-
-
-
-
-
-
Installation
-
Installation instructions for client software can be found on the official WireGuard website.
-
Open Instructions
-
-
-
-
-
-
-
-
About
-
WireGuard® is an extremely simple yet fast and modern VPN that utilizes state-of-the-art cryptography.
-
More details
-
-
-
-
-
-
-
-
WireGuard Portal
-
WireGuard Portal is a simple, web based configuration portal for WireGuard.
-
More details
-
-
-
-
-
-
-
VPN Profiles
-
You can access and download your personal VPN configurations via your Userprofile.
-
-
To find all your configured profiles click on the button below.
-
- Open My Profile
-
-
-
- {{with eq $.Session.LoggedIn true}}{{with eq $.Session.IsAdmin true}}
-
-
Administration Area
-
In the administration area you can manage WireGuard peers and the server interface as well as users that are allowed to log in to the WireGuard Portal.
-
-
To find all your configured profiles click on the button below.
-
- Open WireGuard Administration
- Open User Administration
-
-
- {{end}}{{end}}
-
-
- {{template "prt_footer.html" .}}
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/assets/tpl/login.html b/assets/tpl/login.html
deleted file mode 100644
index 402be7d..0000000
--- a/assets/tpl/login.html
+++ /dev/null
@@ -1,66 +0,0 @@
-
-
-
-
-
-
-
{{ .static.WebsiteTitle }} - Login
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{template "prt_flashes.html" .}}
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/assets/tpl/prt_flashes.html b/assets/tpl/prt_flashes.html
deleted file mode 100644
index f21c787..0000000
--- a/assets/tpl/prt_flashes.html
+++ /dev/null
@@ -1,5 +0,0 @@
-{{range $flash := $.Alerts}}
-
- {{$flash.Message}}
-
-{{end}}
\ No newline at end of file
diff --git a/assets/tpl/prt_footer.html b/assets/tpl/prt_footer.html
deleted file mode 100644
index f454568..0000000
--- a/assets/tpl/prt_footer.html
+++ /dev/null
@@ -1,5 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/tpl/prt_nav.html b/assets/tpl/prt_nav.html
deleted file mode 100644
index e997a44..0000000
--- a/assets/tpl/prt_nav.html
+++ /dev/null
@@ -1,61 +0,0 @@
-
-
-
-
-
-
-
-
-
- {{with eq $.Session.LoggedIn true}}{{with eq $.Session.IsAdmin true}}
- {{with eq $.Route "/admin/"}}
-
- {{end}}
- {{with eq $.Route "/admin/users/"}}
-
- {{end}}
- {{end}}{{end}}
-
- {{with eq $.Session.LoggedIn true}}{{with eq $.Session.IsAdmin true}}
- {{with startsWith $.Route "/admin/"}}
-
- {{end}}
- {{end}}{{end}}
- {{if eq $.Session.LoggedIn true}}
-
- {{else}}
-
Login
- {{end}}
-
-
-{{if not $.Device.IsValid}}
-
-
Warning: WireGuard Interface {{$.Device.DeviceName}} is not fully configured! Configurations may be incomplete and non functional!
-
-{{end}}
\ No newline at end of file
diff --git a/assets/tpl/user_create_client.html b/assets/tpl/user_create_client.html
deleted file mode 100644
index 5e6540c..0000000
--- a/assets/tpl/user_create_client.html
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
-
-
-
{{ .Static.WebsiteTitle }} - Admin
-
-
-
-
-
-
-
- {{template "prt_nav.html" .}}
-
- {{template "prt_flashes.html" .}}
-
-
-
Create a new client
-
-
-
- {{template "prt_footer.html" .}}
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/assets/tpl/user_edit_client.html b/assets/tpl/user_edit_client.html
deleted file mode 100644
index 9d73174..0000000
--- a/assets/tpl/user_edit_client.html
+++ /dev/null
@@ -1,54 +0,0 @@
-
-
-
-
-
-
{{ .Static.WebsiteTitle }} - Admin
-
-
-
-
-
-
-
- {{template "prt_nav.html" .}}
-
- {{template "prt_flashes.html" .}}
-
-
-
Edit client: {{.Peer.Identifier}}
-
-
-
- {{template "prt_footer.html" .}}
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/assets/tpl/user_index.html b/assets/tpl/user_index.html
deleted file mode 100644
index 1968aba..0000000
--- a/assets/tpl/user_index.html
+++ /dev/null
@@ -1,137 +0,0 @@
-
-
-
-
-
-
{{ .Static.WebsiteTitle }} - Profile
-
-
-
-
-
-
-
- {{template "prt_nav.html" .}}
-
-
WireGuard VPN User-Portal
-
-
-
-
Your VPN Profiles
-
-
- {{if eq $.UserManagePeers true}}
-
- {{end}}
-
-
-
-
-
-
-
- Identifier
- Public Key
- E-Mail
- IP's
- Interface
- Handshake
- {{if eq $.UserManagePeers true}}
-
- {{end}}
-
-
-
- {{range $i, $p :=.Peers}}
- {{$peerUser:=(userForEmail $.Users $p.Email)}}
-
-
-
-
-
-
- {{$p.Identifier}}{{if $p.WillExpire}} {{end}}
- {{$p.PublicKey}}
- {{$p.Email}}
- {{$p.IPsStr}}
- {{$p.DeviceName}}
- {{$p.LastHandshake}}
- {{if eq $.UserManagePeers true}}
-
-
-
- {{end}}
-
-
-
-
-
-
-
-
-
-
User details
- {{if not $peerUser}}
-
No user information available...
- {{else}}
-
- Firstname: {{$peerUser.Firstname}}
- Lastname: {{$peerUser.Lastname}}
- Phone: {{$peerUser.Phone}}
- Mail: {{$peerUser.Email}}
-
- {{end}}
-
Traffic
- {{if not $p.Peer}}
-
No Traffic data available...
- {{else}}
-
{{if $p.DeactivatedAt}}-{{else}} {{formatBytes $p.Peer.ReceiveBytes}} / {{formatBytes $p.Peer.TransmitBytes}}{{end}}
- {{end}}
-
-
-
-
-
-
-
-
- {{if $p.DeactivatedAt}}
-
Peer is disabled!
- {{end}}
- {{if $p.WillExpire}}
-
Profile expires on {{ formatDate $p.ExpiresAt}}
- {{end}}
-
-
-
-
-
-
- {{end}}
-
-
-
Currently listed peers: {{len .Peers}}
-
-
- {{template "prt_footer.html" .}}
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/cmd/api_build_tool/main.go b/cmd/api_build_tool/main.go
new file mode 100644
index 0000000..4b0caad
--- /dev/null
+++ b/cmd/api_build_tool/main.go
@@ -0,0 +1,70 @@
+package main
+
+import (
+ "fmt"
+ "log"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/sirupsen/logrus"
+ "github.com/swaggo/swag"
+ "github.com/swaggo/swag/gen"
+)
+
+// this replaces the call to: swag init --propertyStrategy pascalcase --parseDependency --parseInternal --generalInfo base.go
+func main() {
+ wd, err := os.Getwd() // should be the project root
+ if err != nil {
+ panic(err)
+ }
+
+ apiBasePath := filepath.Join(wd, "/internal/app/api")
+ apis := []string{"v0"}
+
+ hasError := false
+ for _, apiVersion := range apis {
+ apiPath := filepath.Join(apiBasePath, apiVersion, "handlers")
+
+ apiVersion = strings.TrimLeft(apiVersion, "api-")
+ log.Println("")
+ log.Println("Generate swagger docs for API", apiVersion)
+ log.Println("Api path:", apiPath)
+
+ err := generateApi(apiBasePath, apiPath, apiVersion)
+ if err != nil {
+ hasError = true
+ logrus.Errorf("failed to generate API docs for %s: %v", apiVersion, err)
+ }
+
+ log.Println("Generated swagger docs for API", apiVersion)
+ }
+
+ if hasError {
+ os.Exit(1)
+ }
+}
+
+func generateApi(basePath, apiPath, version string) error {
+ err := gen.New().Build(&gen.Config{
+ SearchDir: apiPath,
+ Excludes: "",
+ MainAPIFile: "base.go",
+ PropNamingStrategy: swag.PascalCase,
+ OutputDir: filepath.Join(basePath, "core/assets/doc"),
+ OutputTypes: []string{"json", "yaml"},
+ ParseVendor: false,
+ ParseDependency: true,
+ MarkdownFilesDir: "",
+ ParseInternal: true,
+ GeneratedTime: false,
+ CodeExampleFilesDir: "",
+ ParseDepth: 3,
+ InstanceName: version,
+ })
+ if err != nil {
+ return fmt.Errorf("swag failed: %w", err)
+ }
+
+ return nil
+}
diff --git a/cmd/hc/main.go b/cmd/hc/main.go
deleted file mode 100644
index ece44d1..0000000
--- a/cmd/hc/main.go
+++ /dev/null
@@ -1,35 +0,0 @@
-// source taken from https://git.prolicht.digital/golib/healthcheck/-/blob/master/cmd/hc/main.go
-
-package main
-
-import (
- "net/http"
- "os"
- "time"
-)
-
-// main checks the given URL, if the response is not 200, it will return with exit code 1
-// on success, exit code 0 will be returned
-func main() {
- os.Exit(checkWebEndpointFromArgs())
-}
-
-func checkWebEndpointFromArgs() int {
- if len(os.Args) < 2 {
- return 1
- }
- if status := checkWebEndpoint(os.Args[1]); !status {
- return 1
- }
- return 0
-}
-
-func checkWebEndpoint(url string) bool {
- client := &http.Client{
- Timeout: time.Second * 2,
- }
- if resp, err := client.Get(url); err != nil || resp.StatusCode < 200 || resp.StatusCode > 299 {
- return false
- }
- return true
-}
diff --git a/cmd/wg-portal/main.go b/cmd/wg-portal/main.go
index 430d92d..f56a64c 100644
--- a/cmd/wg-portal/main.go
+++ b/cmd/wg-portal/main.go
@@ -2,103 +2,144 @@ package main
import (
"context"
- "io"
+ "github.com/h44z/wg-portal/internal/app/api/core"
+ handlersV0 "github.com/h44z/wg-portal/internal/app/api/v0/handlers"
+ "github.com/h44z/wg-portal/internal/app/audit"
+ "github.com/h44z/wg-portal/internal/app/auth"
+ "github.com/h44z/wg-portal/internal/app/configfile"
+ "github.com/h44z/wg-portal/internal/app/mail"
+ "github.com/h44z/wg-portal/internal/app/route"
+ "github.com/h44z/wg-portal/internal/app/users"
+ "github.com/h44z/wg-portal/internal/app/wireguard"
"os"
- "os/signal"
- "runtime"
+ "strings"
"syscall"
"time"
- "git.prolicht.digital/golib/healthcheck"
- "github.com/h44z/wg-portal/internal/server"
+ "github.com/h44z/wg-portal/internal"
+ "github.com/h44z/wg-portal/internal/adapters"
+ "github.com/h44z/wg-portal/internal/app"
+ "github.com/h44z/wg-portal/internal/config"
"github.com/sirupsen/logrus"
+ evbus "github.com/vardius/message-bus"
)
+// main entry point for WireGuard Portal
func main() {
- _ = setupLogger(logrus.StandardLogger())
+ ctx := internal.SignalAwareContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
- c := make(chan os.Signal, 1)
- signal.Notify(c, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
+ logrus.Infof("Starting WireGuard Portal V2...")
+ logrus.Infof("WireGuard Portal version: %s", internal.Version)
- logrus.Infof("sysinfo: os=%s, arch=%s", runtime.GOOS, runtime.GOARCH)
- logrus.Infof("starting WireGuard Portal Server [%s]...", server.Version)
+ cfg, err := config.GetConfig()
+ internal.AssertNoError(err)
+ setupLogging(cfg)
- // Context for clean shutdown
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
+ cfg.LogStartupValues()
- // start health check service on port 11223
- healthcheck.New(healthcheck.ListenOn("127.0.0.1:11223")).StartWithContext(ctx)
+ rawDb, err := adapters.NewDatabase(cfg.Database)
+ internal.AssertNoError(err)
- service := server.Server{}
- if err := service.Setup(ctx); err != nil {
- logrus.Fatalf("setup failed: %v", err)
+ database, err := adapters.NewSqlRepository(rawDb)
+ internal.AssertNoError(err)
+
+ wireGuard := adapters.NewWireGuardRepository()
+
+ wgQuick := adapters.NewWgQuickRepo()
+
+ mailer := adapters.NewSmtpMailRepo(cfg.Mail)
+
+ cfgFileSystem, err := adapters.NewFileSystemRepository(cfg.Advanced.ConfigStoragePath)
+ internal.AssertNoError(err)
+
+ shouldExit, err := app.HandleProgramArgs(cfg, rawDb)
+ switch {
+ case shouldExit && err == nil:
+ return
+ case shouldExit && err != nil:
+ logrus.Errorf("Failed to process program args: %v", err)
+ os.Exit(1)
+ case !shouldExit:
+ internal.AssertNoError(err)
}
- // Attach signal handlers to context
- go func() {
- osCall := <-c
- logrus.Tracef("received system call: %v", osCall)
- cancel() // cancel the context
- }()
+ queueSize := 100
+ eventBus := evbus.New(queueSize)
- // Start main process in background
- go service.Run()
+ userManager, err := users.NewUserManager(cfg, eventBus, database, database)
+ internal.AssertNoError(err)
- <-ctx.Done() // Wait until the context gets canceled
+ authenticator, err := auth.NewAuthenticator(&cfg.Auth, eventBus, userManager)
+ internal.AssertNoError(err)
- // Give goroutines some time to stop gracefully
- logrus.Info("stopping WireGuard Portal Server...")
- time.Sleep(2 * time.Second)
+ wireGuardManager, err := wireguard.NewWireGuardManager(cfg, eventBus, wireGuard, wgQuick, database)
+ internal.AssertNoError(err)
- logrus.Infof("stopped WireGuard Portal Server...")
- logrus.Exit(0)
+ statisticsCollector, err := wireguard.NewStatisticsCollector(cfg, database, wireGuard)
+ internal.AssertNoError(err)
+
+ cfgFileManager, err := configfile.NewConfigFileManager(cfg, eventBus, database, database, cfgFileSystem)
+ internal.AssertNoError(err)
+
+ mailManager, err := mail.NewMailManager(cfg, mailer, cfgFileManager, database, database)
+ internal.AssertNoError(err)
+
+ auditRecorder, err := audit.NewAuditRecorder(cfg, eventBus, database)
+ internal.AssertNoError(err)
+ auditRecorder.StartBackgroundJobs(ctx)
+
+ routeManager, err := route.NewRouteManager(cfg, eventBus, database)
+ internal.AssertNoError(err)
+ routeManager.StartBackgroundJobs(ctx)
+
+ backend, err := app.New(cfg, eventBus, authenticator, userManager, wireGuardManager,
+ statisticsCollector, cfgFileManager, mailManager)
+ internal.AssertNoError(err)
+ err = backend.Startup(ctx)
+ internal.AssertNoError(err)
+
+ apiFrontend := handlersV0.NewRestApi(cfg, backend)
+
+ webSrv, err := core.NewServer(cfg, apiFrontend)
+ internal.AssertNoError(err)
+
+ go webSrv.Run(ctx, cfg.Web.ListeningAddress)
+
+ // wait until context gets cancelled
+ <-ctx.Done()
+
+ logrus.Infof("Stopping WireGuard Portal")
+
+ time.Sleep(5 * time.Second) // wait for (most) goroutines to finish gracefully
+
+ logrus.Infof("Stopped WireGuard Portal")
}
-func setupLogger(logger *logrus.Logger) error {
- // Check environment variables for logrus settings
- level, ok := os.LookupEnv("LOG_LEVEL")
- if !ok {
- level = "debug" // Default logrus level
- }
-
- useJSON, ok := os.LookupEnv("LOG_JSON")
- if !ok {
- useJSON = "false" // Default use human readable logging
- }
-
- useColor, ok := os.LookupEnv("LOG_COLOR")
- if !ok {
- useColor = "true"
- }
-
- switch level {
- case "off":
- logger.SetOutput(io.Discard)
- case "info":
- logger.SetLevel(logrus.InfoLevel)
- case "debug":
- logger.SetLevel(logrus.DebugLevel)
+func setupLogging(cfg *config.Config) {
+ switch strings.ToLower(cfg.Advanced.LogLevel) {
case "trace":
- logger.SetLevel(logrus.TraceLevel)
+ logrus.SetLevel(logrus.TraceLevel)
+ case "debug":
+ logrus.SetLevel(logrus.DebugLevel)
+ case "info", "information":
+ logrus.SetLevel(logrus.InfoLevel)
+ case "warn", "warning":
+ logrus.SetLevel(logrus.WarnLevel)
+ case "error":
+ logrus.SetLevel(logrus.ErrorLevel)
+ default:
+ logrus.SetLevel(logrus.WarnLevel)
}
- var formatter logrus.Formatter
- if useJSON == "false" {
- f := new(logrus.TextFormatter)
- f.TimestampFormat = "2006-01-02 15:04:05"
- f.FullTimestamp = true
- if useColor == "true" {
- f.ForceColors = true
- }
- formatter = f
- } else {
- f := new(logrus.JSONFormatter)
- f.TimestampFormat = "2006-01-02 15:04:05"
- formatter = f
+ switch {
+ case cfg.Advanced.LogJson:
+ logrus.SetFormatter(&logrus.JSONFormatter{
+ PrettyPrint: cfg.Advanced.LogPretty,
+ })
+ case cfg.Advanced.LogPretty:
+ logrus.SetFormatter(&logrus.TextFormatter{
+ ForceColors: true,
+ DisableColors: false,
+ })
}
-
- logger.SetFormatter(formatter)
-
- return nil
}
diff --git a/config.yml.sample b/config.yml.sample
new file mode 100644
index 0000000..1303274
--- /dev/null
+++ b/config.yml.sample
@@ -0,0 +1,47 @@
+advanced:
+ log_level: trace
+
+core:
+ admin_user: test@test.de
+ admin_password: secret
+
+web:
+ external_url: http://localhost:8888
+ request_logging: true
+
+auth:
+ callback_url_prefix: http://localhost:8888/api/v0
+ ldap:
+ - id: ldap1
+ provider_name: company ldap
+ display_name: Login withLDAP
+ url: ldap://ldap.yourcompany.local:389
+ bind_user: ldap_wireguard@yourcompany.local
+ bind_pass: super_Secret_PASSWORD
+ base_dn: DC=YOURCOMPANY,DC=LOCAL
+ login_filter: (&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2))
+ admin_group: CN=WireGuardAdmins,OU=it,DC=YOURCOMPANY,DC=LOCAL
+ synchronize: false
+ sync_filter: (&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*))
+ registration_enabled: true
+ oidc:
+ - id: oidc1
+ provider_name: google
+ display_name: Login withGoogle
+ base_url: https://accounts.google.com
+ client_id: the-client-id-1234.apps.googleusercontent.com
+ client_secret: A_CLIENT_SECRET
+ extra_scopes:
+ - https://www.googleapis.com/auth/userinfo.email
+ - https://www.googleapis.com/auth/userinfo.profile
+ registration_enabled: true
+ - id: oidc2
+ provider_name: google2
+ display_name: Login withGoogle2
+ base_url: https://accounts.google.com
+ client_id: another-client-id-1234.apps.googleusercontent.com
+ client_secret: A_CLIENT_SECRET
+ extra_scopes:
+ - https://www.googleapis.com/auth/userinfo.email
+ - https://www.googleapis.com/auth/userinfo.profile
+ registration_enabled: true
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index 25e1ffa..8b640a6 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -2,7 +2,7 @@
version: '3.6'
services:
wg-portal:
- image: h44z/wg-portal:1.0.17
+ image: h44z/wg-portal:2.0.0-alpha1
container_name: wg-portal
restart: unless-stopped
logging:
@@ -16,4 +16,4 @@ services:
- /etc/wireguard:/etc/wireguard
- ./data:/app/data
environment:
- - EXTERNAL_URL=http://localhost:8123
+ - EXTERNAL_URL=http://localhost:8888
diff --git a/efs.go b/efs.go
deleted file mode 100644
index cb7469b..0000000
--- a/efs.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package wg_portal
-
-import "embed"
-
-//go:embed assets/tpl/*
-var Templates embed.FS
-
-//go:embed assets/css/*
-//go:embed assets/fonts/*
-//go:embed assets/img/*
-//go:embed assets/js/*
-var Statics embed.FS
diff --git a/frontend/.env.development b/frontend/.env.development
new file mode 100644
index 0000000..3abdd03
--- /dev/null
+++ b/frontend/.env.development
@@ -0,0 +1 @@
+VITE_SOME_EXAMPLE_VAR=http://localhost:5000 (can be used internally like: import.meta.env.VITE_SOME_EXAMPLE_VAR)
\ No newline at end of file
diff --git a/frontend/.env.production b/frontend/.env.production
new file mode 100644
index 0000000..4a9f415
--- /dev/null
+++ b/frontend/.env.production
@@ -0,0 +1 @@
+VITE_API_BASE_URL=https://wgportal.server.com
\ No newline at end of file
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 0000000..6821a23
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,28 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+.DS_Store
+dist
+dist-ssr
+coverage
+*.local
+
+/cypress/videos/
+/cypress/screenshots/
+
+# Editor directories and files
+.vscode/extensions.json
+!.vscode/extensions.json
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/frontend/.vscode/extensions.json b/frontend/.vscode/extensions.json
new file mode 100644
index 0000000..806eacd
--- /dev/null
+++ b/frontend/.vscode/extensions.json
@@ -0,0 +1,3 @@
+{
+ "recommendations": ["johnsoncodehk.volar", "johnsoncodehk.vscode-typescript-vue-plugin"]
+}
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 0000000..0d7f9af
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,29 @@
+# frontend
+
+This template should help get you started developing with Vue 3 in Vite.
+
+## Recommended IDE Setup
+
+[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.vscode-typescript-vue-plugin).
+
+## Customize configuration
+
+See [Vite Configuration Reference](https://vitejs.dev/config/).
+
+## Project Setup
+
+```sh
+npm install
+```
+
+### Compile and Hot-Reload for Development
+
+```sh
+npm run dev
+```
+
+### Compile and Minify for Production
+
+```sh
+npm run build
+```
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..b667f8d
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
WireGuard Portal
+
+
+
+
+
+
+ We're sorry but this site doesn't work properly without JavaScript enabled. Please enable it to continue.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
new file mode 100644
index 0000000..55e18b2
--- /dev/null
+++ b/frontend/package-lock.json
@@ -0,0 +1,1684 @@
+{
+ "name": "frontend",
+ "version": "0.0.0",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "frontend",
+ "version": "0.0.0",
+ "dependencies": {
+ "@fortawesome/fontawesome-free": "^6.4.0",
+ "@kyvg/vue3-notification": "^2.9.1",
+ "@popperjs/core": "^2.11.8",
+ "bootstrap": "^5.3.0",
+ "bootswatch": "^5.3.0",
+ "flag-icons": "^6.7.0",
+ "is-cidr": "^5.0.3",
+ "is-ip": "^5.0.0",
+ "pinia": "^2.1.4",
+ "prismjs": "^1.29.0",
+ "vue": "^3.3.4",
+ "vue-i18n": "^9.2.2",
+ "vue-prism-component": "github:h44z/vue-prism-component",
+ "vue-router": "^4.2.2",
+ "vue3-tags-input": "^1.0.12"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "^4.2.3",
+ "vite": "^4.3.9"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz",
+ "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==",
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz",
+ "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz",
+ "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz",
+ "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz",
+ "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz",
+ "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz",
+ "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz",
+ "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz",
+ "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz",
+ "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz",
+ "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz",
+ "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz",
+ "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz",
+ "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz",
+ "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz",
+ "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz",
+ "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz",
+ "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz",
+ "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz",
+ "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz",
+ "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz",
+ "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz",
+ "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@fortawesome/fontawesome-free": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.0.tgz",
+ "integrity": "sha512-0NyytTlPJwB/BF5LtRV8rrABDbe3TdTXqNB3PdZ+UUUZAEIrdOJdmABqKjt4AXwIoJNaRVVZEXxpNrqvE1GAYQ==",
+ "hasInstallScript": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@intlify/core-base": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.2.2.tgz",
+ "integrity": "sha512-JjUpQtNfn+joMbrXvpR4hTF8iJQ2sEFzzK3KIESOx+f+uwIjgw20igOyaIdhfsVVBCds8ZM64MoeNSx+PHQMkA==",
+ "dependencies": {
+ "@intlify/devtools-if": "9.2.2",
+ "@intlify/message-compiler": "9.2.2",
+ "@intlify/shared": "9.2.2",
+ "@intlify/vue-devtools": "9.2.2"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/@intlify/devtools-if": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/@intlify/devtools-if/-/devtools-if-9.2.2.tgz",
+ "integrity": "sha512-4ttr/FNO29w+kBbU7HZ/U0Lzuh2cRDhP8UlWOtV9ERcjHzuyXVZmjyleESK6eVP60tGC9QtQW9yZE+JeRhDHkg==",
+ "dependencies": {
+ "@intlify/shared": "9.2.2"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/@intlify/message-compiler": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.2.2.tgz",
+ "integrity": "sha512-IUrQW7byAKN2fMBe8z6sK6riG1pue95e5jfokn8hA5Q3Bqy4MBJ5lJAofUsawQJYHeoPJ7svMDyBaVJ4d0GTtA==",
+ "dependencies": {
+ "@intlify/shared": "9.2.2",
+ "source-map": "0.6.1"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/@intlify/shared": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.2.2.tgz",
+ "integrity": "sha512-wRwTpsslgZS5HNyM7uDQYZtxnbI12aGiBZURX3BTR9RFIKKRWpllTsgzHWvj3HKm3Y2Sh5LPC1r0PDCKEhVn9Q==",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/@intlify/vue-devtools": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/@intlify/vue-devtools/-/vue-devtools-9.2.2.tgz",
+ "integrity": "sha512-+dUyqyCHWHb/UcvY1MlIpO87munedm3Gn6E9WWYdWrMuYLcoIoOEVDWSS8xSwtlPU+kA+MEQTP6Q1iI/ocusJg==",
+ "dependencies": {
+ "@intlify/core-base": "9.2.2",
+ "@intlify/shared": "9.2.2"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.4.15",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
+ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
+ },
+ "node_modules/@kyvg/vue3-notification": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@kyvg/vue3-notification/-/vue3-notification-2.9.1.tgz",
+ "integrity": "sha512-FsY8g25tQetr3etnarxHtCeNFKssH8sheFu13LyL2JJmOOel437QqKV5n4RBDDDTIo55iKgIVYXeojliXYdEhw==",
+ "peerDependencies": {
+ "vue": "^3.0.0"
+ }
+ },
+ "node_modules/@popperjs/core": {
+ "version": "2.11.8",
+ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
+ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
+ }
+ },
+ "node_modules/@vitejs/plugin-vue": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.2.3.tgz",
+ "integrity": "sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw==",
+ "dev": true,
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.0.0",
+ "vue": "^3.2.25"
+ }
+ },
+ "node_modules/@vue/compiler-core": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.4.tgz",
+ "integrity": "sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==",
+ "dependencies": {
+ "@babel/parser": "^7.21.3",
+ "@vue/shared": "3.3.4",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.0.2"
+ }
+ },
+ "node_modules/@vue/compiler-dom": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz",
+ "integrity": "sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==",
+ "dependencies": {
+ "@vue/compiler-core": "3.3.4",
+ "@vue/shared": "3.3.4"
+ }
+ },
+ "node_modules/@vue/compiler-sfc": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz",
+ "integrity": "sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==",
+ "dependencies": {
+ "@babel/parser": "^7.20.15",
+ "@vue/compiler-core": "3.3.4",
+ "@vue/compiler-dom": "3.3.4",
+ "@vue/compiler-ssr": "3.3.4",
+ "@vue/reactivity-transform": "3.3.4",
+ "@vue/shared": "3.3.4",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.0",
+ "postcss": "^8.1.10",
+ "source-map-js": "^1.0.2"
+ }
+ },
+ "node_modules/@vue/compiler-ssr": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz",
+ "integrity": "sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==",
+ "dependencies": {
+ "@vue/compiler-dom": "3.3.4",
+ "@vue/shared": "3.3.4"
+ }
+ },
+ "node_modules/@vue/devtools-api": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.5.0.tgz",
+ "integrity": "sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q=="
+ },
+ "node_modules/@vue/reactivity": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.4.tgz",
+ "integrity": "sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==",
+ "dependencies": {
+ "@vue/shared": "3.3.4"
+ }
+ },
+ "node_modules/@vue/reactivity-transform": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz",
+ "integrity": "sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==",
+ "dependencies": {
+ "@babel/parser": "^7.20.15",
+ "@vue/compiler-core": "3.3.4",
+ "@vue/shared": "3.3.4",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.0"
+ }
+ },
+ "node_modules/@vue/runtime-core": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.4.tgz",
+ "integrity": "sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==",
+ "dependencies": {
+ "@vue/reactivity": "3.3.4",
+ "@vue/shared": "3.3.4"
+ }
+ },
+ "node_modules/@vue/runtime-dom": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.4.tgz",
+ "integrity": "sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==",
+ "dependencies": {
+ "@vue/runtime-core": "3.3.4",
+ "@vue/shared": "3.3.4",
+ "csstype": "^3.1.1"
+ }
+ },
+ "node_modules/@vue/server-renderer": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.4.tgz",
+ "integrity": "sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==",
+ "dependencies": {
+ "@vue/compiler-ssr": "3.3.4",
+ "@vue/shared": "3.3.4"
+ },
+ "peerDependencies": {
+ "vue": "3.3.4"
+ }
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz",
+ "integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ=="
+ },
+ "node_modules/bootstrap": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.0.tgz",
+ "integrity": "sha512-UnBV3E3v4STVNQdms6jSGO2CvOkjUMdDAVR2V5N4uCMdaIkaQjbcEAMqRimDHIs4uqBYzDAKCQwCB+97tJgHQw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/twbs"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/bootstrap"
+ }
+ ],
+ "peerDependencies": {
+ "@popperjs/core": "^2.11.7"
+ }
+ },
+ "node_modules/bootswatch": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/bootswatch/-/bootswatch-5.3.0.tgz",
+ "integrity": "sha512-ga2hHognDrh5h3+CaBBug6ktx3MTlnDzH57s+Mvjt9ZcNxqwpK+m3sE3YIUSr8zf2iG05elOb1mnqqcdbce2ow=="
+ },
+ "node_modules/cidr-regex": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/cidr-regex/-/cidr-regex-4.0.3.tgz",
+ "integrity": "sha512-HOwDIy/rhKeMf6uOzxtv7FAbrz8zPjmVKfSpM+U7/bNBXC5rtOyr758jxcptiSx6ZZn5LOhPJT5WWxPAGDV8dw==",
+ "dependencies": {
+ "ip-regex": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/click-outside-vue3": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/click-outside-vue3/-/click-outside-vue3-4.0.1.tgz",
+ "integrity": "sha512-sbplNecrup5oGqA3o4bo8XmvHRT6q9fvw21Z67aDbTqB9M6LF7CuYLTlLvNtOgKU6W3zst5H5zJuEh4auqA34g==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/clone-regexp": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-3.0.0.tgz",
+ "integrity": "sha512-ujdnoq2Kxb8s3ItNBtnYeXdm07FcU0u8ARAT1lQ2YdMwQC+cdiXX8KoqMVuglztILivceTtp4ivqGSmEmhBUJw==",
+ "dependencies": {
+ "is-regexp": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/convert-hrtime": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/convert-hrtime/-/convert-hrtime-5.0.0.tgz",
+ "integrity": "sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
+ "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
+ },
+ "node_modules/esbuild": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz",
+ "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/android-arm": "0.17.19",
+ "@esbuild/android-arm64": "0.17.19",
+ "@esbuild/android-x64": "0.17.19",
+ "@esbuild/darwin-arm64": "0.17.19",
+ "@esbuild/darwin-x64": "0.17.19",
+ "@esbuild/freebsd-arm64": "0.17.19",
+ "@esbuild/freebsd-x64": "0.17.19",
+ "@esbuild/linux-arm": "0.17.19",
+ "@esbuild/linux-arm64": "0.17.19",
+ "@esbuild/linux-ia32": "0.17.19",
+ "@esbuild/linux-loong64": "0.17.19",
+ "@esbuild/linux-mips64el": "0.17.19",
+ "@esbuild/linux-ppc64": "0.17.19",
+ "@esbuild/linux-riscv64": "0.17.19",
+ "@esbuild/linux-s390x": "0.17.19",
+ "@esbuild/linux-x64": "0.17.19",
+ "@esbuild/netbsd-x64": "0.17.19",
+ "@esbuild/openbsd-x64": "0.17.19",
+ "@esbuild/sunos-x64": "0.17.19",
+ "@esbuild/win32-arm64": "0.17.19",
+ "@esbuild/win32-ia32": "0.17.19",
+ "@esbuild/win32-x64": "0.17.19"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
+ },
+ "node_modules/flag-icons": {
+ "version": "6.7.0",
+ "resolved": "https://registry.npmjs.org/flag-icons/-/flag-icons-6.7.0.tgz",
+ "integrity": "sha512-+KXrrrXN2jiETFxisFl+3f83Bq7tj5nuIWnbv9fX59k05lvldEXRCOffybb5hAIjMWt4nmG0E8OfKt7Flm99Eg=="
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-timeout": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/function-timeout/-/function-timeout-0.1.1.tgz",
+ "integrity": "sha512-0NVVC0TaP7dSTvn1yMiy6d6Q8gifzbvQafO46RtLG/kHJUBNd+pVRGOBoK44wNBvtSPUJRfdVvkFdD3p0xvyZg==",
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ip-regex": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz",
+ "integrity": "sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-cidr": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/is-cidr/-/is-cidr-5.0.3.tgz",
+ "integrity": "sha512-lKkM0tmz07dAxNsr8Ii9MGreExa9ZR34N9j8mTG5op824kcwBqinZPowNjcVWWc7j+jR8XAMMItOmBkniN0jOA==",
+ "dependencies": {
+ "cidr-regex": "4.0.3"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/is-ip": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-5.0.0.tgz",
+ "integrity": "sha512-uhmKwcdWJ1nTmBdoBxdHilfJs4qdLBIvVHKRels2+UCZmfcfefuQWziadaYLpN7t/bUrJOjJHv+R1di1q7Q1HQ==",
+ "dependencies": {
+ "ip-regex": "^5.0.0",
+ "super-regex": "^0.2.0"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-regexp": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz",
+ "integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.0",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz",
+ "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.4.13"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
+ "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
+ },
+ "node_modules/pinia": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.1.4.tgz",
+ "integrity": "sha512-vYlnDu+Y/FXxv1ABo1vhjC+IbqvzUdiUC3sfDRrRyY2CQSrqqaa+iiHmqtARFxJVqWQMCJfXx1PBvFs9aJVLXQ==",
+ "dependencies": {
+ "@vue/devtools-api": "^6.5.0",
+ "vue-demi": ">=0.14.5"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/posva"
+ },
+ "peerDependencies": {
+ "@vue/composition-api": "^1.4.0",
+ "typescript": ">=4.4.4",
+ "vue": "^2.6.14 || ^3.3.0"
+ },
+ "peerDependenciesMeta": {
+ "@vue/composition-api": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pinia/node_modules/vue-demi": {
+ "version": "0.14.5",
+ "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.5.tgz",
+ "integrity": "sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==",
+ "hasInstallScript": true,
+ "bin": {
+ "vue-demi-fix": "bin/vue-demi-fix.js",
+ "vue-demi-switch": "bin/vue-demi-switch.js"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "@vue/composition-api": "^1.0.0-rc.1",
+ "vue": "^3.0.0-0 || ^2.6.0"
+ },
+ "peerDependenciesMeta": {
+ "@vue/composition-api": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.4.24",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz",
+ "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "nanoid": "^3.3.6",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prismjs": {
+ "version": "1.29.0",
+ "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
+ "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "3.25.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.25.1.tgz",
+ "integrity": "sha512-tywOR+rwIt5m2ZAWSe5AIJcTat8vGlnPFAv15ycCrw33t6iFsXZ6mzHVFh2psSjxQPmI+xgzMZZizUAukBI4aQ==",
+ "dev": true,
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=14.18.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
+ "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/super-regex": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/super-regex/-/super-regex-0.2.0.tgz",
+ "integrity": "sha512-WZzIx3rC1CvbMDloLsVw0lkZVKJWbrkJ0k1ghKFmcnPrW1+jWbgTkTEWVtD9lMdmI4jZEz40+naBxl1dCUhXXw==",
+ "dependencies": {
+ "clone-regexp": "^3.0.0",
+ "function-timeout": "^0.1.0",
+ "time-span": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/time-span": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz",
+ "integrity": "sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==",
+ "dependencies": {
+ "convert-hrtime": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/vite": {
+ "version": "4.3.9",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz",
+ "integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==",
+ "dev": true,
+ "dependencies": {
+ "esbuild": "^0.17.5",
+ "postcss": "^8.4.23",
+ "rollup": "^3.21.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ },
+ "peerDependencies": {
+ "@types/node": ">= 14",
+ "less": "*",
+ "sass": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.4.tgz",
+ "integrity": "sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==",
+ "dependencies": {
+ "@vue/compiler-dom": "3.3.4",
+ "@vue/compiler-sfc": "3.3.4",
+ "@vue/runtime-dom": "3.3.4",
+ "@vue/server-renderer": "3.3.4",
+ "@vue/shared": "3.3.4"
+ }
+ },
+ "node_modules/vue-i18n": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.2.2.tgz",
+ "integrity": "sha512-yswpwtj89rTBhegUAv9Mu37LNznyu3NpyLQmozF3i1hYOhwpG8RjcjIFIIfnu+2MDZJGSZPXaKWvnQA71Yv9TQ==",
+ "dependencies": {
+ "@intlify/core-base": "9.2.2",
+ "@intlify/shared": "9.2.2",
+ "@intlify/vue-devtools": "9.2.2",
+ "@vue/devtools-api": "^6.2.1"
+ },
+ "engines": {
+ "node": ">= 14"
+ },
+ "peerDependencies": {
+ "vue": "^3.0.0"
+ }
+ },
+ "node_modules/vue-prism-component": {
+ "version": "1.0.0",
+ "resolved": "git+ssh://git@github.com/h44z/vue-prism-component.git#ec539f9533a7ecb2a030a27b2b7bdf21c08b8da0",
+ "license": "MIT"
+ },
+ "node_modules/vue-router": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.2.tgz",
+ "integrity": "sha512-cChBPPmAflgBGmy3tBsjeoe3f3VOSG6naKyY5pjtrqLGbNEXdzCigFUHgBvp9e3ysAtFtEx7OLqcSDh/1Cq2TQ==",
+ "dependencies": {
+ "@vue/devtools-api": "^6.5.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/posva"
+ },
+ "peerDependencies": {
+ "vue": "^3.2.0"
+ }
+ },
+ "node_modules/vue3-tags-input": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/vue3-tags-input/-/vue3-tags-input-1.0.12.tgz",
+ "integrity": "sha512-s5rG+1W3M8+be0nd9H1nv/8WLjJOO6pShgVz8ALAqOiz3tDH5QhGrDH6fzD14ZjJNRWSa3bRBSXQwHEXffPQ6g==",
+ "dependencies": {
+ "click-outside-vue3": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "vue": "^3.0.5"
+ }
+ }
+ },
+ "dependencies": {
+ "@babel/parser": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz",
+ "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q=="
+ },
+ "@esbuild/android-arm": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz",
+ "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/android-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz",
+ "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/android-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz",
+ "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/darwin-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz",
+ "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/darwin-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz",
+ "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/freebsd-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz",
+ "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/freebsd-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz",
+ "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-arm": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz",
+ "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz",
+ "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-ia32": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz",
+ "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-loong64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz",
+ "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-mips64el": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz",
+ "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-ppc64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz",
+ "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-riscv64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz",
+ "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-s390x": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz",
+ "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz",
+ "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/netbsd-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz",
+ "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/openbsd-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz",
+ "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/sunos-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz",
+ "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/win32-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz",
+ "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/win32-ia32": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz",
+ "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/win32-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz",
+ "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==",
+ "dev": true,
+ "optional": true
+ },
+ "@fortawesome/fontawesome-free": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.0.tgz",
+ "integrity": "sha512-0NyytTlPJwB/BF5LtRV8rrABDbe3TdTXqNB3PdZ+UUUZAEIrdOJdmABqKjt4AXwIoJNaRVVZEXxpNrqvE1GAYQ=="
+ },
+ "@intlify/core-base": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.2.2.tgz",
+ "integrity": "sha512-JjUpQtNfn+joMbrXvpR4hTF8iJQ2sEFzzK3KIESOx+f+uwIjgw20igOyaIdhfsVVBCds8ZM64MoeNSx+PHQMkA==",
+ "requires": {
+ "@intlify/devtools-if": "9.2.2",
+ "@intlify/message-compiler": "9.2.2",
+ "@intlify/shared": "9.2.2",
+ "@intlify/vue-devtools": "9.2.2"
+ }
+ },
+ "@intlify/devtools-if": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/@intlify/devtools-if/-/devtools-if-9.2.2.tgz",
+ "integrity": "sha512-4ttr/FNO29w+kBbU7HZ/U0Lzuh2cRDhP8UlWOtV9ERcjHzuyXVZmjyleESK6eVP60tGC9QtQW9yZE+JeRhDHkg==",
+ "requires": {
+ "@intlify/shared": "9.2.2"
+ }
+ },
+ "@intlify/message-compiler": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.2.2.tgz",
+ "integrity": "sha512-IUrQW7byAKN2fMBe8z6sK6riG1pue95e5jfokn8hA5Q3Bqy4MBJ5lJAofUsawQJYHeoPJ7svMDyBaVJ4d0GTtA==",
+ "requires": {
+ "@intlify/shared": "9.2.2",
+ "source-map": "0.6.1"
+ }
+ },
+ "@intlify/shared": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.2.2.tgz",
+ "integrity": "sha512-wRwTpsslgZS5HNyM7uDQYZtxnbI12aGiBZURX3BTR9RFIKKRWpllTsgzHWvj3HKm3Y2Sh5LPC1r0PDCKEhVn9Q=="
+ },
+ "@intlify/vue-devtools": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/@intlify/vue-devtools/-/vue-devtools-9.2.2.tgz",
+ "integrity": "sha512-+dUyqyCHWHb/UcvY1MlIpO87munedm3Gn6E9WWYdWrMuYLcoIoOEVDWSS8xSwtlPU+kA+MEQTP6Q1iI/ocusJg==",
+ "requires": {
+ "@intlify/core-base": "9.2.2",
+ "@intlify/shared": "9.2.2"
+ }
+ },
+ "@jridgewell/sourcemap-codec": {
+ "version": "1.4.15",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
+ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
+ },
+ "@kyvg/vue3-notification": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@kyvg/vue3-notification/-/vue3-notification-2.9.1.tgz",
+ "integrity": "sha512-FsY8g25tQetr3etnarxHtCeNFKssH8sheFu13LyL2JJmOOel437QqKV5n4RBDDDTIo55iKgIVYXeojliXYdEhw==",
+ "requires": {}
+ },
+ "@popperjs/core": {
+ "version": "2.11.8",
+ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
+ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="
+ },
+ "@vitejs/plugin-vue": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.2.3.tgz",
+ "integrity": "sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw==",
+ "dev": true,
+ "requires": {}
+ },
+ "@vue/compiler-core": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.4.tgz",
+ "integrity": "sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==",
+ "requires": {
+ "@babel/parser": "^7.21.3",
+ "@vue/shared": "3.3.4",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.0.2"
+ }
+ },
+ "@vue/compiler-dom": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz",
+ "integrity": "sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==",
+ "requires": {
+ "@vue/compiler-core": "3.3.4",
+ "@vue/shared": "3.3.4"
+ }
+ },
+ "@vue/compiler-sfc": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz",
+ "integrity": "sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==",
+ "requires": {
+ "@babel/parser": "^7.20.15",
+ "@vue/compiler-core": "3.3.4",
+ "@vue/compiler-dom": "3.3.4",
+ "@vue/compiler-ssr": "3.3.4",
+ "@vue/reactivity-transform": "3.3.4",
+ "@vue/shared": "3.3.4",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.0",
+ "postcss": "^8.1.10",
+ "source-map-js": "^1.0.2"
+ }
+ },
+ "@vue/compiler-ssr": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz",
+ "integrity": "sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==",
+ "requires": {
+ "@vue/compiler-dom": "3.3.4",
+ "@vue/shared": "3.3.4"
+ }
+ },
+ "@vue/devtools-api": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.5.0.tgz",
+ "integrity": "sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q=="
+ },
+ "@vue/reactivity": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.4.tgz",
+ "integrity": "sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==",
+ "requires": {
+ "@vue/shared": "3.3.4"
+ }
+ },
+ "@vue/reactivity-transform": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz",
+ "integrity": "sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==",
+ "requires": {
+ "@babel/parser": "^7.20.15",
+ "@vue/compiler-core": "3.3.4",
+ "@vue/shared": "3.3.4",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.0"
+ }
+ },
+ "@vue/runtime-core": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.4.tgz",
+ "integrity": "sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==",
+ "requires": {
+ "@vue/reactivity": "3.3.4",
+ "@vue/shared": "3.3.4"
+ }
+ },
+ "@vue/runtime-dom": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.4.tgz",
+ "integrity": "sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==",
+ "requires": {
+ "@vue/runtime-core": "3.3.4",
+ "@vue/shared": "3.3.4",
+ "csstype": "^3.1.1"
+ }
+ },
+ "@vue/server-renderer": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.4.tgz",
+ "integrity": "sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==",
+ "requires": {
+ "@vue/compiler-ssr": "3.3.4",
+ "@vue/shared": "3.3.4"
+ }
+ },
+ "@vue/shared": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz",
+ "integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ=="
+ },
+ "bootstrap": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.0.tgz",
+ "integrity": "sha512-UnBV3E3v4STVNQdms6jSGO2CvOkjUMdDAVR2V5N4uCMdaIkaQjbcEAMqRimDHIs4uqBYzDAKCQwCB+97tJgHQw==",
+ "requires": {}
+ },
+ "bootswatch": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/bootswatch/-/bootswatch-5.3.0.tgz",
+ "integrity": "sha512-ga2hHognDrh5h3+CaBBug6ktx3MTlnDzH57s+Mvjt9ZcNxqwpK+m3sE3YIUSr8zf2iG05elOb1mnqqcdbce2ow=="
+ },
+ "cidr-regex": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/cidr-regex/-/cidr-regex-4.0.3.tgz",
+ "integrity": "sha512-HOwDIy/rhKeMf6uOzxtv7FAbrz8zPjmVKfSpM+U7/bNBXC5rtOyr758jxcptiSx6ZZn5LOhPJT5WWxPAGDV8dw==",
+ "requires": {
+ "ip-regex": "^5.0.0"
+ }
+ },
+ "click-outside-vue3": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/click-outside-vue3/-/click-outside-vue3-4.0.1.tgz",
+ "integrity": "sha512-sbplNecrup5oGqA3o4bo8XmvHRT6q9fvw21Z67aDbTqB9M6LF7CuYLTlLvNtOgKU6W3zst5H5zJuEh4auqA34g=="
+ },
+ "clone-regexp": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-3.0.0.tgz",
+ "integrity": "sha512-ujdnoq2Kxb8s3ItNBtnYeXdm07FcU0u8ARAT1lQ2YdMwQC+cdiXX8KoqMVuglztILivceTtp4ivqGSmEmhBUJw==",
+ "requires": {
+ "is-regexp": "^3.0.0"
+ }
+ },
+ "convert-hrtime": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/convert-hrtime/-/convert-hrtime-5.0.0.tgz",
+ "integrity": "sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg=="
+ },
+ "csstype": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
+ "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
+ },
+ "esbuild": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz",
+ "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==",
+ "dev": true,
+ "requires": {
+ "@esbuild/android-arm": "0.17.19",
+ "@esbuild/android-arm64": "0.17.19",
+ "@esbuild/android-x64": "0.17.19",
+ "@esbuild/darwin-arm64": "0.17.19",
+ "@esbuild/darwin-x64": "0.17.19",
+ "@esbuild/freebsd-arm64": "0.17.19",
+ "@esbuild/freebsd-x64": "0.17.19",
+ "@esbuild/linux-arm": "0.17.19",
+ "@esbuild/linux-arm64": "0.17.19",
+ "@esbuild/linux-ia32": "0.17.19",
+ "@esbuild/linux-loong64": "0.17.19",
+ "@esbuild/linux-mips64el": "0.17.19",
+ "@esbuild/linux-ppc64": "0.17.19",
+ "@esbuild/linux-riscv64": "0.17.19",
+ "@esbuild/linux-s390x": "0.17.19",
+ "@esbuild/linux-x64": "0.17.19",
+ "@esbuild/netbsd-x64": "0.17.19",
+ "@esbuild/openbsd-x64": "0.17.19",
+ "@esbuild/sunos-x64": "0.17.19",
+ "@esbuild/win32-arm64": "0.17.19",
+ "@esbuild/win32-ia32": "0.17.19",
+ "@esbuild/win32-x64": "0.17.19"
+ }
+ },
+ "estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
+ },
+ "flag-icons": {
+ "version": "6.7.0",
+ "resolved": "https://registry.npmjs.org/flag-icons/-/flag-icons-6.7.0.tgz",
+ "integrity": "sha512-+KXrrrXN2jiETFxisFl+3f83Bq7tj5nuIWnbv9fX59k05lvldEXRCOffybb5hAIjMWt4nmG0E8OfKt7Flm99Eg=="
+ },
+ "fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "optional": true
+ },
+ "function-timeout": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/function-timeout/-/function-timeout-0.1.1.tgz",
+ "integrity": "sha512-0NVVC0TaP7dSTvn1yMiy6d6Q8gifzbvQafO46RtLG/kHJUBNd+pVRGOBoK44wNBvtSPUJRfdVvkFdD3p0xvyZg=="
+ },
+ "ip-regex": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz",
+ "integrity": "sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw=="
+ },
+ "is-cidr": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/is-cidr/-/is-cidr-5.0.3.tgz",
+ "integrity": "sha512-lKkM0tmz07dAxNsr8Ii9MGreExa9ZR34N9j8mTG5op824kcwBqinZPowNjcVWWc7j+jR8XAMMItOmBkniN0jOA==",
+ "requires": {
+ "cidr-regex": "4.0.3"
+ }
+ },
+ "is-ip": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-5.0.0.tgz",
+ "integrity": "sha512-uhmKwcdWJ1nTmBdoBxdHilfJs4qdLBIvVHKRels2+UCZmfcfefuQWziadaYLpN7t/bUrJOjJHv+R1di1q7Q1HQ==",
+ "requires": {
+ "ip-regex": "^5.0.0",
+ "super-regex": "^0.2.0"
+ }
+ },
+ "is-regexp": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz",
+ "integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA=="
+ },
+ "magic-string": {
+ "version": "0.30.0",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz",
+ "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==",
+ "requires": {
+ "@jridgewell/sourcemap-codec": "^1.4.13"
+ }
+ },
+ "nanoid": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
+ "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA=="
+ },
+ "picocolors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
+ },
+ "pinia": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.1.4.tgz",
+ "integrity": "sha512-vYlnDu+Y/FXxv1ABo1vhjC+IbqvzUdiUC3sfDRrRyY2CQSrqqaa+iiHmqtARFxJVqWQMCJfXx1PBvFs9aJVLXQ==",
+ "requires": {
+ "@vue/devtools-api": "^6.5.0",
+ "vue-demi": ">=0.14.5"
+ },
+ "dependencies": {
+ "vue-demi": {
+ "version": "0.14.5",
+ "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.5.tgz",
+ "integrity": "sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==",
+ "requires": {}
+ }
+ }
+ },
+ "postcss": {
+ "version": "8.4.24",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz",
+ "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==",
+ "requires": {
+ "nanoid": "^3.3.6",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ }
+ },
+ "prismjs": {
+ "version": "1.29.0",
+ "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
+ "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q=="
+ },
+ "rollup": {
+ "version": "3.25.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.25.1.tgz",
+ "integrity": "sha512-tywOR+rwIt5m2ZAWSe5AIJcTat8vGlnPFAv15ycCrw33t6iFsXZ6mzHVFh2psSjxQPmI+xgzMZZizUAukBI4aQ==",
+ "dev": true,
+ "requires": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+ },
+ "source-map-js": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
+ "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw=="
+ },
+ "super-regex": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/super-regex/-/super-regex-0.2.0.tgz",
+ "integrity": "sha512-WZzIx3rC1CvbMDloLsVw0lkZVKJWbrkJ0k1ghKFmcnPrW1+jWbgTkTEWVtD9lMdmI4jZEz40+naBxl1dCUhXXw==",
+ "requires": {
+ "clone-regexp": "^3.0.0",
+ "function-timeout": "^0.1.0",
+ "time-span": "^5.1.0"
+ }
+ },
+ "time-span": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz",
+ "integrity": "sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==",
+ "requires": {
+ "convert-hrtime": "^5.0.0"
+ }
+ },
+ "vite": {
+ "version": "4.3.9",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz",
+ "integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==",
+ "dev": true,
+ "requires": {
+ "esbuild": "^0.17.5",
+ "fsevents": "~2.3.2",
+ "postcss": "^8.4.23",
+ "rollup": "^3.21.0"
+ }
+ },
+ "vue": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.4.tgz",
+ "integrity": "sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==",
+ "requires": {
+ "@vue/compiler-dom": "3.3.4",
+ "@vue/compiler-sfc": "3.3.4",
+ "@vue/runtime-dom": "3.3.4",
+ "@vue/server-renderer": "3.3.4",
+ "@vue/shared": "3.3.4"
+ }
+ },
+ "vue-i18n": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.2.2.tgz",
+ "integrity": "sha512-yswpwtj89rTBhegUAv9Mu37LNznyu3NpyLQmozF3i1hYOhwpG8RjcjIFIIfnu+2MDZJGSZPXaKWvnQA71Yv9TQ==",
+ "requires": {
+ "@intlify/core-base": "9.2.2",
+ "@intlify/shared": "9.2.2",
+ "@intlify/vue-devtools": "9.2.2",
+ "@vue/devtools-api": "^6.2.1"
+ }
+ },
+ "vue-prism-component": {
+ "version": "git+ssh://git@github.com/h44z/vue-prism-component.git#ec539f9533a7ecb2a030a27b2b7bdf21c08b8da0",
+ "from": "vue-prism-component@github:h44z/vue-prism-component"
+ },
+ "vue-router": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.2.tgz",
+ "integrity": "sha512-cChBPPmAflgBGmy3tBsjeoe3f3VOSG6naKyY5pjtrqLGbNEXdzCigFUHgBvp9e3ysAtFtEx7OLqcSDh/1Cq2TQ==",
+ "requires": {
+ "@vue/devtools-api": "^6.5.0"
+ }
+ },
+ "vue3-tags-input": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/vue3-tags-input/-/vue3-tags-input-1.0.12.tgz",
+ "integrity": "sha512-s5rG+1W3M8+be0nd9H1nv/8WLjJOO6pShgVz8ALAqOiz3tDH5QhGrDH6fzD14ZjJNRWSa3bRBSXQwHEXffPQ6g==",
+ "requires": {
+ "click-outside-vue3": "^4.0.1"
+ }
+ }
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..3c6aab4
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "frontend",
+ "version": "0.0.0",
+ "scripts": {
+ "dev": "vite",
+ "build-dev": "vite build --mode development --base=/app/",
+ "build": "vite build --base=/app/",
+ "preview": "vite preview --port 5050"
+ },
+ "dependencies": {
+ "@fortawesome/fontawesome-free": "^6.4.0",
+ "@kyvg/vue3-notification": "^2.9.1",
+ "@popperjs/core": "^2.11.8",
+ "bootstrap": "^5.3.0",
+ "bootswatch": "^5.3.0",
+ "flag-icons": "^6.7.0",
+ "is-cidr": "^5.0.3",
+ "is-ip": "^5.0.0",
+ "pinia": "^2.1.4",
+ "prismjs": "^1.29.0",
+ "vue": "^3.3.4",
+ "vue-i18n": "^9.2.2",
+ "vue-prism-component": "github:h44z/vue-prism-component",
+ "vue-router": "^4.2.2",
+ "vue3-tags-input": "^1.0.12"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "^4.2.3",
+ "vite": "^4.3.9"
+ }
+}
diff --git a/assets/img/favicon-large.png b/frontend/public/favicon-large.png
similarity index 100%
rename from assets/img/favicon-large.png
rename to frontend/public/favicon-large.png
diff --git a/assets/img/favicon.ico b/frontend/public/favicon.ico
similarity index 100%
rename from assets/img/favicon.ico
rename to frontend/public/favicon.ico
diff --git a/assets/img/favicon.png b/frontend/public/favicon.png
similarity index 100%
rename from assets/img/favicon.png
rename to frontend/public/favicon.png
diff --git a/assets/img/header-logo.png b/frontend/public/img/header-logo.png
similarity index 100%
rename from assets/img/header-logo.png
rename to frontend/public/img/header-logo.png
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
new file mode 100644
index 0000000..dfe1641
--- /dev/null
+++ b/frontend/src/App.vue
@@ -0,0 +1,128 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('menu.home') }}
+
+
+ {{ $t('menu.interfaces') }}
+
+
+ {{ $t('menu.users') }}
+
+
+
+
+
+
+
+ {{ $t('menu.login') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/assets/base.css b/frontend/src/assets/base.css
new file mode 100644
index 0000000..5dcf4b6
--- /dev/null
+++ b/frontend/src/assets/base.css
@@ -0,0 +1,5 @@
+a.disabled {
+ pointer-events: none;
+ cursor: default;
+ color: #888888;
+}
\ No newline at end of file
diff --git a/frontend/src/assets/logo.svg b/frontend/src/assets/logo.svg
new file mode 100644
index 0000000..bc826fe
--- /dev/null
+++ b/frontend/src/assets/logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/src/components/Confirmation.vue b/frontend/src/components/Confirmation.vue
new file mode 100644
index 0000000..d5a5318
--- /dev/null
+++ b/frontend/src/components/Confirmation.vue
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/InterfaceEditModal.vue b/frontend/src/components/InterfaceEditModal.vue
new file mode 100644
index 0000000..0496566
--- /dev/null
+++ b/frontend/src/components/InterfaceEditModal.vue
@@ -0,0 +1,513 @@
+
+
+
+
+
+
+
+
+
+ {{ $t('modals.interface-edit.header-general') }}
+
+ {{ $t('modals.interface-edit.identifier.label') }}
+
+
+
+ {{ $t('modals.interface-edit.mode.label') }}
+
+ {{ $t('modals.interface-edit.mode.server') }}
+ {{ $t('modals.interface-edit.mode.client') }}
+ {{ $t('modals.interface-edit.mode.any') }}
+
+
+
+ {{ $t('modals.interface-edit.display-name.label') }}
+
+
+
+
+ {{ $t('modals.interface-edit.header-crypto') }}
+
+ {{ $t('modals.interface-edit.private-key.label') }}
+
+
+
+ {{ $t('modals.interface-edit.public-key.label') }}
+
+
+
+
+ {{ $t('modals.interface-edit.header-network') }}
+
+ {{ $t('modals.interface-edit.ip.label') }}
+
+
+
+ {{ $t('modals.interface-edit.listen-port.label') }}
+
+
+
+ {{ $t('modals.interface-edit.dns.label') }}
+
+
+
+ {{ $t('modals.interface-edit.dns-search.label') }}
+
+
+
+
+
+ {{ $t('modals.interface-edit.routing-table.label') }}
+
+ {{ $t('modals.interface-edit.routing-table.description') }}
+
+
+
+
+
+
+ {{ $t('modals.interface-edit.header-hooks') }}
+
+ {{ $t('modals.interface-edit.pre-up.label') }}
+
+
+
+ {{ $t('modals.interface-edit.post-up.label') }}
+
+
+
+ {{ $t('modals.interface-edit.pre-down.label') }}
+
+
+
+ {{ $t('modals.interface-edit.post-down.label') }}
+
+
+
+
+ {{ $t('modals.interface-edit.header-state') }}
+
+
+ {{ $t('modals.interface-edit.disabled.label') }}
+
+
+
+ {{ $t('modals.interface-edit.save-config.label') }}
+
+
+
+
+
+ {{ $t('modals.interface-edit.header-network') }}
+
+ {{ $t('modals.interface-edit.defaults.endpoint.label') }}
+
+ {{ $t('modals.interface-edit.defaults.endpoint.description') }}
+
+
+ {{ $t('modals.interface-edit.defaults.networks.label') }}
+
+ {{ $t('modals.interface-edit.defaults.networks.description') }}
+
+
+ {{ $t('modals.interface-edit.defaults.allowed-ip.label') }}
+
+
+
+ {{ $t('modals.interface-edit.dns.label') }}
+
+
+
+ {{ $t('modals.interface-edit.dns-search.label') }}
+
+
+
+
+
+
+ {{ $t('modals.interface-edit.header-peer-hooks') }}
+
+ {{ $t('modals.interface-edit.pre-up.label') }}
+
+
+
+ {{ $t('modals.interface-edit.post-up.label') }}
+
+
+
+ {{ $t('modals.interface-edit.pre-down.label') }}
+
+
+
+ {{ $t('modals.interface-edit.post-down.label') }}
+
+
+
+
+
+ {{ $t('modals.interface-edit.button-apply-defaults') }}
+
+
+
+
+
+
+ {{ $t('general.delete') }}
+
+ {{ $t('general.save') }}
+ {{ $t('general.close') }}
+
+
+
+
+
diff --git a/frontend/src/components/InterfaceViewModal.vue b/frontend/src/components/InterfaceViewModal.vue
new file mode 100644
index 0000000..b71c400
--- /dev/null
+++ b/frontend/src/components/InterfaceViewModal.vue
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+ {{ $t('general.close') }}
+
+
+
diff --git a/frontend/src/components/Modal.vue b/frontend/src/components/Modal.vue
new file mode 100644
index 0000000..7409b57
--- /dev/null
+++ b/frontend/src/components/Modal.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/components/PeerEditModal.vue b/frontend/src/components/PeerEditModal.vue
new file mode 100644
index 0000000..51e47ff
--- /dev/null
+++ b/frontend/src/components/PeerEditModal.vue
@@ -0,0 +1,431 @@
+
+
+
+
+
+
+ {{ $t('modals.peer-edit.header-general') }}
+
+ {{ $t('modals.peer-edit.display-name.label') }}
+
+
+
+ {{ $t('modals.peer-edit.linked-user.label') }}
+
+
+
+
+ {{ $t('modals.peer-edit.header-crypto') }}
+
+ {{ $t('modals.peer-edit.private-key.label') }}
+
+
+
+ {{ $t('modals.peer-edit.public-key.label') }}
+
+
+
+ {{ $t('modals.peer-edit.preshared-key.label') }}
+
+
+
+ {{ $t('modals.peer-edit.endpoint-public-key.label') }}
+
+
+
+
+ {{ $t('modals.peer-edit.header-network') }}
+
+ {{ $t('modals.peer-edit.endpoint.label') }}
+
+
+
+ {{ $t('modals.peer-edit.ip.label') }}
+
+
+
+ {{ $t('modals.peer-edit.allowed-ip.label') }}
+
+
+
+ {{ $t('modals.peer-edit.extra-allowed-ip.label') }}
+
+ {{ $t('modals.peer-edit.extra-allowed-ip.description') }}
+
+
+ {{ $t('modals.peer-edit.dns.label') }}
+
+
+
+ {{ $t('modals.peer-edit.dns-search.label') }}
+
+
+
+
+
+ {{ $t('modals.peer-edit.header-hooks') }}
+
+ {{ $t('modals.peer-edit.pre-up.label') }}
+
+
+
+ {{ $t('modals.peer-edit.post-up.label') }}
+
+
+
+ {{ $t('modals.peer-edit.pre-down.label') }}
+
+
+
+ {{ $t('modals.peer-edit.post-down.label') }}
+
+
+
+
+ {{ $t('modals.peer-edit.header-state') }}
+
+
+
+ {{ $t('modals.peer-edit.expires-at.label') }}
+
+
+
+
+
+
+
+ {{ $t('general.delete') }}
+
+ {{ $t('general.save') }}
+ {{ $t('general.close') }}
+
+
+
+
+
diff --git a/frontend/src/components/PeerMultiCreateModal.vue b/frontend/src/components/PeerMultiCreateModal.vue
new file mode 100644
index 0000000..f8b7bd8
--- /dev/null
+++ b/frontend/src/components/PeerMultiCreateModal.vue
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
+ {{ $t('modals.peer-multi-create.identifiers.label') }}
+
+ {{ $t('modals.peer-multi-create.identifiers.description') }}
+
+
+ {{ $t('modals.peer-multi-create.prefix.label') }}
+
+ {{ $t('modals.peer-multi-create.prefix.description') }}
+
+
+
+
+ {{ $t('general.save') }}
+ {{ $t('general.close') }}
+
+
+
diff --git a/frontend/src/components/PeerViewModal.vue b/frontend/src/components/PeerViewModal.vue
new file mode 100644
index 0000000..d06f24b
--- /dev/null
+++ b/frontend/src/components/PeerViewModal.vue
@@ -0,0 +1,199 @@
+
+
+
+
+
+
+
+
+
+ {{ $t('modals.peer-view.button-download') }}
+ {{ $t('modals.peer-view.button-email') }}
+
+ {{ $t('general.close') }}
+
+
+
+
+
+
+
diff --git a/frontend/src/components/UserEditModal.vue b/frontend/src/components/UserEditModal.vue
new file mode 100644
index 0000000..cf2ac0c
--- /dev/null
+++ b/frontend/src/components/UserEditModal.vue
@@ -0,0 +1,174 @@
+
+
+
+
+
+
+ {{ $t('modals.user-edit.header-general') }}
+
+ {{ $t('modals.user-edit.identifier.label') }}
+
+
+
+ {{ $t('modals.user-edit.source.label') }}
+
+
+
+ {{ $t('modals.user-edit.password.label') }}
+
+ {{ $t('modals.user-edit.password.description') }}
+
+
+
+ {{ $t('modals.user-edit.header-personal') }}
+
+ {{ $t('modals.user-edit.email.label') }}
+
+
+
+
+
+
+ {{ $t('modals.user-edit.header-notes') }}
+
+ {{ $t('modals.user-edit.notes.label') }}
+
+
+
+
+ {{ $t('modals.user-edit.header-state') }}
+
+
+ {{ $t('modals.user-edit.disabled.label') }}
+
+
+
+ {{ $t('modals.user-edit.locked.label') }}
+
+
+
+ {{ $t('modals.user-edit.admin.label') }}
+
+
+
+
+
+
+ {{ $t('general.delete') }}
+
+ {{ $t('general.save') }}
+ {{ $t('general.close') }}
+
+
+
diff --git a/frontend/src/components/UserViewModal.vue b/frontend/src/components/UserViewModal.vue
new file mode 100644
index 0000000..c2edbb7
--- /dev/null
+++ b/frontend/src/components/UserViewModal.vue
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+
+
+
+
+ {{ $t('modals.user-view.headline-info') }}
+
+
+
+ {{ $t('modals.user-view.email') }}:
+ {{selectedUser.Email}}
+
+
+ {{ $t('modals.user-view.firstname') }}:
+ {{selectedUser.Firstname}}
+
+
+ {{ $t('modals.user-view.lastname') }}:
+ {{selectedUser.Lastname}}
+
+
+ {{ $t('modals.user-view.phone') }}:
+ {{selectedUser.Phone}}
+
+
+ {{ $t('modals.user-view.department') }}:
+ {{selectedUser.Department}}
+
+
+ {{ $t('modals.user-view.disabled') }}:
+ {{selectedUser.DisabledReason}}
+
+
+ {{ $t('modals.user-view.locked') }}:
+ {{selectedUser.LockedReason}}
+
+
+
+
+
+ {{ $t('modals.user-view.headline-notes') }}
+
+
+ {{selectedUser.Notes}}
+
+
+
+
+
+
+
+ {{ $t('modals.user-view.no-peers') }}
+
+
+
+
+
+ {{ $t('modals.user-view.peers.name') }}
+ {{ $t('modals.user-view.peers.interface') }}
+ {{ $t('modals.user-view.peers.ip') }}
+
+
+
+
+
+ {{peer.DisplayName}}
+ {{peer.InterfaceIdentifier}}
+
+ {{ ip }}
+
+
+
+
+
+
+
+
+ {{ $t('general.close') }}
+
+
+
diff --git a/frontend/src/components/icons/IconCommunity.vue b/frontend/src/components/icons/IconCommunity.vue
new file mode 100644
index 0000000..2dc8b05
--- /dev/null
+++ b/frontend/src/components/icons/IconCommunity.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/frontend/src/components/icons/IconDocumentation.vue b/frontend/src/components/icons/IconDocumentation.vue
new file mode 100644
index 0000000..6d4791c
--- /dev/null
+++ b/frontend/src/components/icons/IconDocumentation.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/frontend/src/components/icons/IconEcosystem.vue b/frontend/src/components/icons/IconEcosystem.vue
new file mode 100644
index 0000000..c3a4f07
--- /dev/null
+++ b/frontend/src/components/icons/IconEcosystem.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/frontend/src/components/icons/IconSupport.vue b/frontend/src/components/icons/IconSupport.vue
new file mode 100644
index 0000000..7452834
--- /dev/null
+++ b/frontend/src/components/icons/IconSupport.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/frontend/src/components/icons/IconTooling.vue b/frontend/src/components/icons/IconTooling.vue
new file mode 100644
index 0000000..660598d
--- /dev/null
+++ b/frontend/src/components/icons/IconTooling.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
+
diff --git a/frontend/src/helpers/encoding.js b/frontend/src/helpers/encoding.js
new file mode 100644
index 0000000..391c9e8
--- /dev/null
+++ b/frontend/src/helpers/encoding.js
@@ -0,0 +1,7 @@
+export function base64_url_encode(input) {
+ let output = btoa(input)
+ output = output.replace('+', '.')
+ output = output.replace('/', '_')
+ output = output.replace('=', '-')
+ return output
+}
\ No newline at end of file
diff --git a/frontend/src/helpers/fetch-wrapper.js b/frontend/src/helpers/fetch-wrapper.js
new file mode 100644
index 0000000..8ac930a
--- /dev/null
+++ b/frontend/src/helpers/fetch-wrapper.js
@@ -0,0 +1,95 @@
+import { authStore } from '@/stores/auth';
+import { securityStore } from '@/stores/security';
+
+export const fetchWrapper = {
+ url: apiUrl(),
+ get: request('GET'),
+ post: request('POST'),
+ put: request('PUT'),
+ delete: request('DELETE')
+};
+
+export const apiWrapper = {
+ url: apiUrl(),
+ get: apiRequest('GET'),
+ post: apiRequest('POST'),
+ put: apiRequest('PUT'),
+ delete: apiRequest('DELETE')
+};
+
+// request can be used to query arbitrary URLs
+function request(method) {
+ return (url, body = undefined) => {
+ const requestOptions = {
+ method,
+ headers: getHeaders(url)
+ };
+ if (body) {
+ requestOptions.headers['Content-Type'] = 'application/json';
+ requestOptions.body = JSON.stringify(body);
+ }
+ return fetch(url, requestOptions).then(handleResponse);
+ }
+}
+
+// apiRequest uses WGPORTAL_BACKEND_BASE_URL as base URL
+function apiRequest(method) {
+ return (path, body = undefined) => {
+ const url = WGPORTAL_BACKEND_BASE_URL + path
+ const requestOptions = {
+ method,
+ headers: getHeaders(method, url)
+ };
+ if (body) {
+ requestOptions.headers['Content-Type'] = 'application/json';
+ requestOptions.body = JSON.stringify(body);
+ }
+ return fetch(url, requestOptions).then(handleResponse);
+ }
+}
+
+// apiUrl uses WGPORTAL_BACKEND_BASE_URL as base URL
+function apiUrl() {
+ return (path) => {
+ return WGPORTAL_BACKEND_BASE_URL + path
+ }
+}
+
+// helper functions
+
+function getHeaders(method, url) {
+ // return auth header with jwt if user is logged in and request is to the api url
+ const auth = authStore();
+ const sec = securityStore();
+ const isApiUrl = url.startsWith(WGPORTAL_BACKEND_BASE_URL);
+
+ let headers = {};
+ if (isApiUrl && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
+ headers["X-CSRF-TOKEN"] = sec.CsrfToken;
+ }
+ if (isApiUrl && auth.IsAuthenticated) {
+ headers["X-FRONTEND-UID"] = auth.UserIdentifier;
+ }
+
+ return headers;
+}
+
+function handleResponse(response) {
+ return response.text().then(text => {
+ const data = text && JSON.parse(text);
+
+ if (!response.ok) {
+ const auth = authStore();
+ if ([401, 403].includes(response.status) && auth.IsAuthenticated) {
+ console.log("automatic logout initiated...");
+ // auto logout if 401 Unauthorized or 403 Forbidden response returned from api
+ auth.Logout();
+ }
+
+ const error = (data && data.Message) || response.statusText;
+ return Promise.reject(error);
+ }
+
+ return data;
+ });
+}
\ No newline at end of file
diff --git a/frontend/src/helpers/models.js b/frontend/src/helpers/models.js
new file mode 100644
index 0000000..3dfa4d6
--- /dev/null
+++ b/frontend/src/helpers/models.js
@@ -0,0 +1,164 @@
+
+export function freshInterface() {
+ return {
+ Disabled: false,
+ DisplayName: "",
+ Identifier: "",
+ Mode: "server",
+
+ PublicKey: "",
+ PrivateKey: "",
+
+ ListenPort: 51820,
+ Addresses: [],
+ DnsStr: [],
+ DnsSearch: [],
+
+ Mtu: 0,
+ FirewallMark: 0,
+ RoutingTable: "",
+
+ PreUp: "",
+ PostUp: "",
+ PreDown: "",
+ PostDown: "",
+
+ SaveConfig: false,
+
+ // Peer defaults
+
+ PeerDefNetwork: [],
+ PeerDefDns: [],
+ PeerDefDnsSearch: [],
+ PeerDefEndpoint: "",
+ PeerDefAllowedIPs: [],
+ PeerDefMtu: 0,
+ PeerDefPersistentKeepalive: 0,
+ PeerDefFirewallMark: 0,
+ PeerDefRoutingTable: "",
+ PeerDefPreUp: "",
+ PeerDefPostUp: "",
+ PeerDefPreDown: "",
+ PeerDefPostDown: "",
+
+ TotalPeers: 0,
+ EnabledPeers: 0
+ }
+}
+
+export function freshPeer() {
+ return {
+ Identifier: "",
+ DisplayName: "",
+ UserIdentifier: "",
+ InterfaceIdentifier: "",
+ Disabled: false,
+ ExpiresAt: null,
+ Notes: "",
+
+ Endpoint: {
+ Value: "",
+ Overridable: true,
+ },
+ EndpointPublicKey: {
+ Value: "",
+ Overridable: true,
+ },
+ AllowedIPs: {
+ Value: [],
+ Overridable: true,
+ },
+ ExtraAllowedIPs: [],
+ PresharedKey: "",
+ PersistentKeepalive: {
+ Value: 0,
+ Overridable: true,
+ },
+
+ PrivateKey: "",
+ PublicKey: "",
+
+ Mode: "client",
+
+ Addresses: [],
+ CheckAliveAddress: "",
+ Dns: {
+ Value: [],
+ Overridable: true,
+ },
+ DnsSearch: {
+ Value: [],
+ Overridable: true,
+ },
+ Mtu: {
+ Value: 0,
+ Overridable: true,
+ },
+ FirewallMark: {
+ Value: 0,
+ Overridable: true,
+ },
+ RoutingTable: {
+ Value: "",
+ Overridable: true,
+ },
+
+ PreUp: {
+ Value: "",
+ Overridable: true,
+ },
+ PostUp: {
+ Value: "",
+ Overridable: true,
+ },
+ PreDown: {
+ Value: "",
+ Overridable: true,
+ },
+ PostDown: {
+ Value: "",
+ Overridable: true,
+ },
+
+ // Internal value
+ IgnoreGlobalSettings: false
+ }
+}
+
+export function freshUser() {
+ return {
+ Identifier: "",
+
+ Email: "",
+ Source: "db",
+ IsAdmin: false,
+
+ Firstname: "",
+ Lastname: "",
+ Phone: "",
+ Department: "",
+ Notes: "",
+
+ Password: "",
+
+ Disabled: false,
+ DisabledReason: "",
+ Locked: false,
+ LockedReason: "",
+
+ PeerCount: 0
+ }
+}
+
+export function freshStats() {
+ return {
+ IsConnected: false,
+ IsPingable: false,
+ LastHandshake: null,
+ LastPing: null,
+ LastSessionStart: null,
+ BytesTransmitted: 0,
+ BytesReceived: 0,
+ EndpointAddress: ""
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/helpers/validators.js b/frontend/src/helpers/validators.js
new file mode 100644
index 0000000..90766ba
--- /dev/null
+++ b/frontend/src/helpers/validators.js
@@ -0,0 +1,14 @@
+import isCidr from "is-cidr";
+import {isIP} from 'is-ip';
+
+export function validateCIDR(value) {
+ return isCidr(value) !== 0
+}
+
+export function validateIP(value) {
+ return isIP(value)
+}
+
+export function validateDomain(value) {
+ return true
+}
\ No newline at end of file
diff --git a/frontend/src/lang/index.js b/frontend/src/lang/index.js
new file mode 100644
index 0000000..1b6460a
--- /dev/null
+++ b/frontend/src/lang/index.js
@@ -0,0 +1,27 @@
+// src/lang/index.js
+import de from './translations/de.json';
+import en from './translations/en.json';
+import {createI18n} from "vue-i18n";
+
+function getStoredLanguage() {
+ let initialLang = localStorage.getItem('wgLang');
+ if (!initialLang) {
+ initialLang = "en"
+ }
+ return initialLang
+}
+
+// Create i18n instance with options
+const i18n = createI18n({
+ legacy: false,
+ globalInjection: true,
+ allowComposition: true,
+ locale: getStoredLanguage(), // set locale
+ fallbackLocale: "en", // set fallback locale
+ messages: {
+ "de": de,
+ "en": en
+ }
+});
+
+export default i18n
\ No newline at end of file
diff --git a/frontend/src/lang/translations/de.json b/frontend/src/lang/translations/de.json
new file mode 100644
index 0000000..0e9c414
--- /dev/null
+++ b/frontend/src/lang/translations/de.json
@@ -0,0 +1,489 @@
+{
+ "general": {
+ "pagination": {
+ "size": "Anzahl an Elementen",
+ "all": "Alle (langsam)"
+ },
+ "search": {
+ "placeholder": "Suche...",
+ "button": "Suchen"
+ },
+ "select-all": "Alle auswählen",
+ "yes": "Ja",
+ "no": "Nein",
+ "cancel": "Abbrechen",
+ "close": "Schließen",
+ "save": "Speichern",
+ "delete": "Löschen"
+ },
+ "login": {
+ "headline": "Bitte melden Sie sich an",
+ "username": {
+ "label": "Benutzername",
+ "placeholder": "Bitte geben Sie Ihren Benutzernamen ein"
+ },
+ "password": {
+ "label": "Kennwort",
+ "placeholder": "Bitte geben Sie Ihr Passwort ein"
+ },
+ "button": "Anmelden"
+ },
+ "menu": {
+ "home": "Home",
+ "interfaces": "Schnittstellen",
+ "users": "Benutzer",
+ "lang": "Sprache ändern",
+ "profile": "Mein Profil",
+ "login": "Anmelden",
+ "logout": "Abmelden"
+ },
+ "home": {
+ "headline": "WireGuard® VPN Portal",
+ "info-headline": "Mehr Informationen",
+ "abstract": "WireGuard® ist ein extrem einfaches, aber dennoch schnelles und modernes VPN, das modernste Kryptographie nutzt. Es zielt darauf ab, schneller, einfacher, schlanker und nützlicher als IPsec zu sein, während es die massiven Kopfschmerzen vermeidet. Es soll wesentlich leistungsfähiger sein als OpenVPN.",
+ "installation": {
+ "box-header": "WireGuard Installation",
+ "headline": "Installation",
+ "content": "Die Installationsanweisungen für die Client-Software finden Sie auf der offiziellen WireGuard-Website.",
+ "btn": "Anleitung öffnen"
+ },
+ "about-wg": {
+ "box-header": "Über WireGuard",
+ "headline": "Über",
+ "content": "WireGuard® ist ein extrem einfaches, aber schnelles und modernes VPN, das modernste Kryptographie verwendet.",
+ "button": "Details"
+ },
+ "about-portal": {
+ "box-header": "Über WireGuard Portal",
+ "headline": "WireGuard Portal",
+ "content": "WireGuard Portal ist ein einfaches, webbasiertes Konfigurationsportal für WireGuard.",
+ "button": "Details"
+ },
+ "profiles": {
+ "headline": "VPN Profile",
+ "abstract": "Über Ihr Benutzerprofil können Sie auf Ihre persönlichen VPN-Konfigurationen zugreifen und diese herunterladen.",
+ "content": "Um alle Ihre konfigurierten Profile zu finden, klicken Sie auf die Schaltfläche unten.",
+ "button": "Mein Profil öffnen"
+ },
+ "admin": {
+ "headline": "Verwaltungsbereich",
+ "abstract": "Im Administrationsbereich können Sie VPN-Zugänge und die Serverschnittstelle sowie die Benutzer, die sich am VPN-Portal anmelden dürfen, verwalten.",
+ "content": "",
+ "button-admin": "Schnittstellenverwaltung",
+ "button-user": "Benutzerverwaltung"
+ }
+ },
+ "interfaces": {
+ "headline": "Schnittstellenverwaltung",
+ "headline-peers": "Current VPN Peers",
+ "headline-endpoints": "Current Endpoints",
+ "no-interface": {
+ "default-selection": "No Interface available",
+ "headline": "No interfaces found...",
+ "abstract": "Click the plus button above to create a new WireGuard interface."
+ },
+ "no-peer": {
+ "headline": "No peers available",
+ "abstract": "Currently, there are no peers available for the selected WireGuard interface."
+ },
+ "table-heading": {
+ "name": "Name",
+ "user": "User",
+ "ip": "IP's",
+ "endpoint": "Endpoint",
+ "status": "Status"
+ },
+ "interface": {
+ "headline": "Interface status for",
+ "mode": "mode",
+ "key": "Public Key",
+ "endpoint": "Public Endpoint",
+ "port": "Listening Port",
+ "peers": "Enabled Peers",
+ "total-peers": "Total Peers",
+ "endpoints": "Enabled Endpoints",
+ "total-endpoints": "Total Endpoints",
+ "ip": "IP Address",
+ "default-allowed-ip": "Default allowed IPs",
+ "dns": "DNS Servers",
+ "mtu": "MTU",
+ "default-keep-alive": "Default Keepalive Interval",
+ "button-show-config": "Show configuration",
+ "button-download-config": "Download configuration",
+ "button-store-config": "Store configuration for wg-quick",
+ "button-edit": "Edit interface"
+ },
+ "button-add-interface": "Add Interface",
+ "button-add-peer": "Add Peer",
+ "button-add-peers": "Add Multiple Peers",
+ "button-show-peer": "Show Peer",
+ "button-edit-peer": "Edit Peer",
+ "peer-disabled": "Peer is disabled, reason:",
+ "peer-expiring": "Peer is expiring at",
+ "peer-connected": "Connected",
+ "peer-not-connected": "Not Connected",
+ "peer-handshake": "Last handshake:"
+ },
+ "users": {
+ "headline": "Benutzerverwaltung",
+ "table-heading": {
+ "id": "ID",
+ "email": "E-Mail",
+ "firstname": "Firstname",
+ "lastname": "Lastname",
+ "source": "Source",
+ "peers": "Peers",
+ "admin": "Admin"
+ },
+ "no-user": {
+ "headline": "No users available",
+ "abstract": "Currently, there are no users registered with WireGuard Portal."
+ },
+ "button-add-user": "Add User",
+ "button-show-user": "Show User",
+ "button-edit-user": "Edit User",
+ "user-disabled": "User is disabled, reason:",
+ "user-locked": "Account is locked, reason:",
+ "admin": "User has administrator privileges",
+ "no-admin": "User has no administrator privileges"
+ },
+ "profile": {
+ "headline": "Meine VPN-Konfigurationen",
+ "table-heading": {
+ "name": "Name",
+ "ip": "IP's",
+ "stats": "Status",
+ "interface": "Server Interface"
+ },
+ "no-peer": {
+ "headline": "No peers available",
+ "abstract": "Currently, there are no peers associated with your user profile."
+ },
+ "peer-connected": "Connected",
+ "button-add-peer": "Add Peer",
+ "button-show-peer": "Show Peer",
+ "button-edit-peer": "Edit Peer"
+ },
+ "modals": {
+ "user-view": {
+ "headline": "User Account:",
+ "tab-user": "Information",
+ "tab-peers": "Peers",
+ "headline-info": "User Information:",
+ "headline-notes": "Notes:",
+ "email": "E-Mail",
+ "firstname": "Firstname",
+ "lastname": "Lastname",
+ "phone": "Phone number",
+ "department": "Department",
+ "disabled": "Account Disabled",
+ "locked": "Account Locked",
+ "no-peers": "User has no associated peers.",
+ "peers": {
+ "name": "Name",
+ "interface": "Interface",
+ "ip": "IP's"
+ }
+ },
+ "user-edit": {
+ "headline-edit": "Edit user:",
+ "headline-new": "New user",
+ "header-general": "General",
+ "header-personal": "User Information",
+ "header-notes": "Notes",
+ "header-state": "State",
+ "identifier": {
+ "label": "Identifier",
+ "placeholder": "The unique user identifier"
+ },
+ "source": {
+ "label": "Source",
+ "placeholder": "The user source"
+ },
+ "password": {
+ "label": "Password",
+ "placeholder": "A super secret password",
+ "description": "Leave this field blank to keep current password."
+ },
+ "email": {
+ "label": "Email",
+ "placeholder": "The email address"
+ },
+ "phone": {
+ "label": "Phone",
+ "placeholder": "The phone number"
+ },
+ "department": {
+ "label": "Department",
+ "placeholder": "The department"
+ },
+ "firstname": {
+ "label": "Firstname",
+ "placeholder": "Firstname"
+ },
+ "lastname": {
+ "label": "Lastname",
+ "placeholder": "Lastname"
+ },
+ "notes": {
+ "label": "Notes",
+ "placeholder": ""
+ },
+ "disabled": {
+ "label": "Disabled (no WireGuard connection and no login possible)"
+ },
+ "locked": {
+ "label": "Locked (no login possible, WireGuard connections still work)"
+ },
+ "admin": {
+ "label": "Is Admin"
+ }
+ },
+ "interface-view": {
+ "headline": "Config for Interface:"
+ },
+ "interface-edit": {
+ "headline-edit": "Edit Interface:",
+ "headline-new": "New Interface",
+ "tab-interface": "Interface",
+ "tab-peerdef": "Peer Defaults",
+ "header-general": "General",
+ "header-network": "Network",
+ "header-crypto": "Cryptography",
+ "header-hooks": "Interface Hooks",
+ "header-peer-hooks": "Hooks",
+ "header-state": "State",
+ "identifier": {
+ "label": "Identifier",
+ "placeholder": "The unique interface identifier"
+ },
+ "mode": {
+ "label": "Interface Mode",
+ "server": "Server Mode",
+ "client": "Client Mode",
+ "any": "Unknown Mode"
+ },
+ "display-name": {
+ "label": "Display Name",
+ "placeholder": "The descriptive name for the interface"
+ },
+ "private-key": {
+ "label": "Private Key",
+ "placeholder": "The private key"
+ },
+ "public-key": {
+ "label": "Public Key",
+ "placeholder": "The public key"
+ },
+ "ip": {
+ "label": "IP Addresses",
+ "placeholder": "IP Addresses (CIDR format)"
+ },
+ "listen-port": {
+ "label": "Listen Port",
+ "placeholder": "The listening port"
+ },
+ "dns": {
+ "label": "DNS Server",
+ "placeholder": "The DNS servers that should be used"
+ },
+ "dns-search": {
+ "label": "DNS Search Domains",
+ "placeholder": "DNS search prefixes"
+ },
+ "mtu": {
+ "label": "MTU",
+ "placeholder": "The interface MTU (0 = keep default)"
+ },
+ "firewall-mark": {
+ "label": "Firewall Mark",
+ "placeholder": "Firewall mark that is applied to outgoing traffic. (0 = automatic)"
+ },
+ "routing-table": {
+ "label": "Routing Table",
+ "placeholder": "The routing table ID",
+ "description": "Special cases: off = do not manage routes, 0 = automatic"
+ },
+ "pre-up": {
+ "label": "Pre-Up",
+ "placeholder": "One or multiple bash commands separated by ;"
+ },
+ "post-up": {
+ "label": "Post-Up",
+ "placeholder": "One or multiple bash commands separated by ;"
+ },
+ "pre-down": {
+ "label": "Pre-Down",
+ "placeholder": "One or multiple bash commands separated by ;"
+ },
+ "post-down": {
+ "label": "Post-Down",
+ "placeholder": "One or multiple bash commands separated by ;"
+ },
+ "disabled": {
+ "label": "Interface Disabled"
+ },
+ "save-config": {
+ "label": "Automatically save wg-quick config"
+ },
+ "defaults": {
+ "endpoint": {
+ "label": "Endpoint Address",
+ "placeholder": "Endpoint Address",
+ "description": "The endpoint address that peers will connect to."
+ },
+ "networks": {
+ "label": "IP Networks",
+ "placeholder": "Network Addresses",
+ "description": "Peers will get IP addresses from those subnets."
+ },
+ "allowed-ip": {
+ "label": "Allowed IP Addresses",
+ "placeholder": "Default Allowed IP Addresses"
+ },
+ "mtu": {
+ "label": "MTU",
+ "placeholder": "The client MTU (0 = keep default)"
+ },
+ "keep-alive": {
+ "label": "Keep Alive Interval",
+ "placeholder": "Persistent Keepalive (0 = default)"
+ }
+ },
+
+ "button-apply-defaults": "Apply Peer Defaults"
+ },
+ "peer-view": {
+ "headline-peer": "Peer:",
+ "headline-endpoint": "Endpoint:",
+ "section-info": "Peer Information",
+ "section-status": "Current Status",
+ "section-config": "Configuration",
+ "identifier": "Identifier",
+ "ip": "IP Addresses",
+ "user": "Associated User",
+ "notes": "Notes",
+ "expiry-status": "Expires At",
+ "disabled-status": "Disabled At",
+ "traffic": "Traffic",
+ "connection-status": "Connection Stats",
+ "upload": "Uploaded Bytes (from Server to Peer)",
+ "download": "Downloaded Bytes (from Peer to Server)",
+ "pingable": "Is Pingable",
+ "handshake": "Last Handshake",
+ "connected-since": "Connected since",
+ "endpoint": "Endpoint",
+ "button-download": "Download configuration",
+ "button-email": "Send configuration via E-Mail"
+ },
+ "peer-edit": {
+ "headline-edit-peer": "Edit peer:",
+ "headline-edit-endpoint": "Edit endpoint:",
+ "headline-new-peer": "Create peer",
+ "headline-new-endpoint": "Create endpoint",
+ "header-general": "General",
+ "header-network": "Network",
+ "header-crypto": "Cryptography",
+ "header-hooks": "Hooks (Executed on Peer)",
+ "header-state": "State",
+ "display-name": {
+ "label": "Display Name",
+ "placeholder": "The descriptive name for the peer"
+ },
+ "linked-user": {
+ "label": "Linked User",
+ "placeholder": "The user account which owns this peer"
+ },
+ "private-key": {
+ "label": "Private Key",
+ "placeholder": "The private key"
+ },
+ "public-key": {
+ "label": "Public Key",
+ "placeholder": "The public key"
+ },
+ "preshared-key": {
+ "label": "Preshared Key",
+ "placeholder": "Optional pre-shared key"
+ },
+ "endpoint-public-key": {
+ "label": "Endpoint public Key",
+ "placeholder": "The public key of the remote endpoint"
+ },
+ "endpoint": {
+ "label": "Endpoint Address",
+ "placeholder": "The address of the remote endpoint"
+ },
+ "ip": {
+ "label": "IP Addresses",
+ "placeholder": "IP Addresses (CIDR format)"
+ },
+ "allowed-ip": {
+ "label": "Allowed IP Addresses",
+ "placeholder": "Allowed IP Addresses (CIDR format)"
+ },
+ "extra-allowed-ip": {
+ "label": "Extra allowed IP Addresses",
+ "placeholder": "Extra allowed IP's (Server Sided)",
+ "description": "Those IP's will be added on the remote WireGuard interface as allowed IP's."
+ },
+ "dns": {
+ "label": "DNS Server",
+ "placeholder": "The DNS servers that should be used"
+ },
+ "dns-search": {
+ "label": "DNS Search Domains",
+ "placeholder": "DNS search prefixes"
+ },
+ "keep-alive": {
+ "label": "Keep Alive Interval",
+ "placeholder": "Persistent Keepalive (0 = default)"
+ },
+ "mtu": {
+ "label": "MTU",
+ "placeholder": "The client MTU (0 = keep default)"
+ },
+ "pre-up": {
+ "label": "Pre-Up",
+ "placeholder": "One or multiple bash commands separated by ;"
+ },
+ "post-up": {
+ "label": "Post-Up",
+ "placeholder": "One or multiple bash commands separated by ;"
+ },
+ "pre-down": {
+ "label": "Pre-Down",
+ "placeholder": "One or multiple bash commands separated by ;"
+ },
+ "post-down": {
+ "label": "Post-Down",
+ "placeholder": "One or multiple bash commands separated by ;"
+ },
+ "disabled": {
+ "label": "Peer Disabled"
+ },
+ "ignore-global": {
+ "label": "Ignore global settings"
+ },
+ "expires-at": {
+ "label": "Expiry date"
+ }
+ },
+ "peer-multi-create": {
+ "headline-peer": "Create multiple peers",
+ "headline-endpoint": "Create multiple endpoints",
+ "identifiers": {
+ "label": "User Identifiers",
+ "placeholder": "User Identifiers",
+ "description": "A user identifier (the username) for which a peer should be created."
+ },
+ "prefix": {
+ "headline-peer": "Peer:",
+ "headline-endpoint": "Endpoint:",
+ "label": "Display Name Prefix",
+ "placeholder": "The prefix",
+ "description": "A prefix that is added to the peers display name."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/lang/translations/en.json b/frontend/src/lang/translations/en.json
new file mode 100644
index 0000000..fa0739b
--- /dev/null
+++ b/frontend/src/lang/translations/en.json
@@ -0,0 +1,489 @@
+{
+ "general": {
+ "pagination": {
+ "size": "Number of Elements",
+ "all": "All (slow)"
+ },
+ "search": {
+ "placeholder": "Search...",
+ "button": "Search"
+ },
+ "select-all": "Select all",
+ "yes": "Yes",
+ "no": "No",
+ "cancel": "Cancel",
+ "close": "Close",
+ "save": "Save",
+ "delete": "Delete"
+ },
+ "login": {
+ "headline": "Please sign in",
+ "username": {
+ "label": "Username",
+ "placeholder": "Please enter your username"
+ },
+ "password": {
+ "label": "Password",
+ "placeholder": "Please enter your password"
+ },
+ "button": "Sign in"
+ },
+ "menu": {
+ "home": "Home",
+ "interfaces": "Interfaces",
+ "users": "Users",
+ "lang": "Toggle Language",
+ "profile": "My Profile",
+ "login": "Login",
+ "logout": "Logout"
+ },
+ "home": {
+ "headline": "WireGuard® VPN Portal",
+ "info-headline": "More Information",
+ "abstract": "WireGuard® is an extremely simple yet fast and modern VPN that utilizes state-of-the-art cryptography. It aims to be faster, simpler, leaner, and more useful than IPsec, while avoiding the massive headache. It intends to be considerably more performant than OpenVPN.",
+ "installation": {
+ "box-header": "WireGuard Installation",
+ "headline": "Installation",
+ "content": "Installation instructions for client software can be found on the official WireGuard website.",
+ "btn": "Open Instructions"
+ },
+ "about-wg": {
+ "box-header": "About WireGuard",
+ "headline": "About",
+ "content": "WireGuard® is an extremely simple yet fast and modern VPN that utilizes state-of-the-art cryptography.",
+ "button": "More"
+ },
+ "about-portal": {
+ "box-header": "About WireGuard Portal",
+ "headline": "WireGuard Portal",
+ "content": "WireGuard Portal is a simple, web based configuration portal for WireGuard.",
+ "button": "More"
+ },
+ "profiles": {
+ "headline": "VPN Profiles",
+ "abstract": "You can access and download your personal VPN configurations via your Userprofile.",
+ "content": "To find all your configured profiles click on the button below.",
+ "button": "Open my profile"
+ },
+ "admin": {
+ "headline": "Administration Area",
+ "abstract": "In the administration area you can manage WireGuard peers and the server interface as well as users that are allowed to log in to the WireGuard Portal.",
+ "content": "",
+ "button-admin": "Open Server Administration",
+ "button-user": "Open User Administration"
+ }
+ },
+ "interfaces": {
+ "headline": "Interface Administration",
+ "headline-peers": "Current VPN Peers",
+ "headline-endpoints": "Current Endpoints",
+ "no-interface": {
+ "default-selection": "No Interface available",
+ "headline": "No interfaces found...",
+ "abstract": "Click the plus button above to create a new WireGuard interface."
+ },
+ "no-peer": {
+ "headline": "No peers available",
+ "abstract": "Currently, there are no peers available for the selected WireGuard interface."
+ },
+ "table-heading": {
+ "name": "Name",
+ "user": "User",
+ "ip": "IP's",
+ "endpoint": "Endpoint",
+ "status": "Status"
+ },
+ "interface": {
+ "headline": "Interface status for",
+ "mode": "mode",
+ "key": "Public Key",
+ "endpoint": "Public Endpoint",
+ "port": "Listening Port",
+ "peers": "Enabled Peers",
+ "total-peers": "Total Peers",
+ "endpoints": "Enabled Endpoints",
+ "total-endpoints": "Total Endpoints",
+ "ip": "IP Address",
+ "default-allowed-ip": "Default allowed IPs",
+ "dns": "DNS Servers",
+ "mtu": "MTU",
+ "default-keep-alive": "Default Keepalive Interval",
+ "button-show-config": "Show configuration",
+ "button-download-config": "Download configuration",
+ "button-store-config": "Store configuration for wg-quick",
+ "button-edit": "Edit interface"
+ },
+ "button-add-interface": "Add Interface",
+ "button-add-peer": "Add Peer",
+ "button-add-peers": "Add Multiple Peers",
+ "button-show-peer": "Show Peer",
+ "button-edit-peer": "Edit Peer",
+ "peer-disabled": "Peer is disabled, reason:",
+ "peer-expiring": "Peer is expiring at",
+ "peer-connected": "Connected",
+ "peer-not-connected": "Not Connected",
+ "peer-handshake": "Last handshake:"
+ },
+ "users": {
+ "headline": "User Administration",
+ "table-heading": {
+ "id": "ID",
+ "email": "E-Mail",
+ "firstname": "Firstname",
+ "lastname": "Lastname",
+ "source": "Source",
+ "peers": "Peers",
+ "admin": "Admin"
+ },
+ "no-user": {
+ "headline": "No users available",
+ "abstract": "Currently, there are no users registered with WireGuard Portal."
+ },
+ "button-add-user": "Add User",
+ "button-show-user": "Show User",
+ "button-edit-user": "Edit User",
+ "user-disabled": "User is disabled, reason:",
+ "user-locked": "Account is locked, reason:",
+ "admin": "User has administrator privileges",
+ "no-admin": "User has no administrator privileges"
+ },
+ "profile": {
+ "headline": "My VPN Peers",
+ "table-heading": {
+ "name": "Name",
+ "ip": "IP's",
+ "stats": "Status",
+ "interface": "Server Interface"
+ },
+ "no-peer": {
+ "headline": "No peers available",
+ "abstract": "Currently, there are no peers associated with your user profile."
+ },
+ "peer-connected": "Connected",
+ "button-add-peer": "Add Peer",
+ "button-show-peer": "Show Peer",
+ "button-edit-peer": "Edit Peer"
+ },
+ "modals": {
+ "user-view": {
+ "headline": "User Account:",
+ "tab-user": "Information",
+ "tab-peers": "Peers",
+ "headline-info": "User Information:",
+ "headline-notes": "Notes:",
+ "email": "E-Mail",
+ "firstname": "Firstname",
+ "lastname": "Lastname",
+ "phone": "Phone number",
+ "department": "Department",
+ "disabled": "Account Disabled",
+ "locked": "Account Locked",
+ "no-peers": "User has no associated peers.",
+ "peers": {
+ "name": "Name",
+ "interface": "Interface",
+ "ip": "IP's"
+ }
+ },
+ "user-edit": {
+ "headline-edit": "Edit user:",
+ "headline-new": "New user",
+ "header-general": "General",
+ "header-personal": "User Information",
+ "header-notes": "Notes",
+ "header-state": "State",
+ "identifier": {
+ "label": "Identifier",
+ "placeholder": "The unique user identifier"
+ },
+ "source": {
+ "label": "Source",
+ "placeholder": "The user source"
+ },
+ "password": {
+ "label": "Password",
+ "placeholder": "A super secret password",
+ "description": "Leave this field blank to keep current password."
+ },
+ "email": {
+ "label": "Email",
+ "placeholder": "The email address"
+ },
+ "phone": {
+ "label": "Phone",
+ "placeholder": "The phone number"
+ },
+ "department": {
+ "label": "Department",
+ "placeholder": "The department"
+ },
+ "firstname": {
+ "label": "Firstname",
+ "placeholder": "Firstname"
+ },
+ "lastname": {
+ "label": "Lastname",
+ "placeholder": "Lastname"
+ },
+ "notes": {
+ "label": "Notes",
+ "placeholder": ""
+ },
+ "disabled": {
+ "label": "Disabled (no WireGuard connection and no login possible)"
+ },
+ "locked": {
+ "label": "Locked (no login possible, WireGuard connections still work)"
+ },
+ "admin": {
+ "label": "Is Admin"
+ }
+ },
+ "interface-view": {
+ "headline": "Config for Interface:"
+ },
+ "interface-edit": {
+ "headline-edit": "Edit Interface:",
+ "headline-new": "New Interface",
+ "tab-interface": "Interface",
+ "tab-peerdef": "Peer Defaults",
+ "header-general": "General",
+ "header-network": "Network",
+ "header-crypto": "Cryptography",
+ "header-hooks": "Interface Hooks",
+ "header-peer-hooks": "Hooks",
+ "header-state": "State",
+ "identifier": {
+ "label": "Identifier",
+ "placeholder": "The unique interface identifier"
+ },
+ "mode": {
+ "label": "Interface Mode",
+ "server": "Server Mode",
+ "client": "Client Mode",
+ "any": "Unknown Mode"
+ },
+ "display-name": {
+ "label": "Display Name",
+ "placeholder": "The descriptive name for the interface"
+ },
+ "private-key": {
+ "label": "Private Key",
+ "placeholder": "The private key"
+ },
+ "public-key": {
+ "label": "Public Key",
+ "placeholder": "The public key"
+ },
+ "ip": {
+ "label": "IP Addresses",
+ "placeholder": "IP Addresses (CIDR format)"
+ },
+ "listen-port": {
+ "label": "Listen Port",
+ "placeholder": "The listening port"
+ },
+ "dns": {
+ "label": "DNS Server",
+ "placeholder": "The DNS servers that should be used"
+ },
+ "dns-search": {
+ "label": "DNS Search Domains",
+ "placeholder": "DNS search prefixes"
+ },
+ "mtu": {
+ "label": "MTU",
+ "placeholder": "The interface MTU (0 = keep default)"
+ },
+ "firewall-mark": {
+ "label": "Firewall Mark",
+ "placeholder": "Firewall mark that is applied to outgoing traffic. (0 = automatic)"
+ },
+ "routing-table": {
+ "label": "Routing Table",
+ "placeholder": "The routing table ID",
+ "description": "Special cases: off = do not manage routes, 0 = automatic"
+ },
+ "pre-up": {
+ "label": "Pre-Up",
+ "placeholder": "One or multiple bash commands separated by ;"
+ },
+ "post-up": {
+ "label": "Post-Up",
+ "placeholder": "One or multiple bash commands separated by ;"
+ },
+ "pre-down": {
+ "label": "Pre-Down",
+ "placeholder": "One or multiple bash commands separated by ;"
+ },
+ "post-down": {
+ "label": "Post-Down",
+ "placeholder": "One or multiple bash commands separated by ;"
+ },
+ "disabled": {
+ "label": "Interface Disabled"
+ },
+ "save-config": {
+ "label": "Automatically save wg-quick config"
+ },
+ "defaults": {
+ "endpoint": {
+ "label": "Endpoint Address",
+ "placeholder": "Endpoint Address",
+ "description": "The endpoint address that peers will connect to."
+ },
+ "networks": {
+ "label": "IP Networks",
+ "placeholder": "Network Addresses",
+ "description": "Peers will get IP addresses from those subnets."
+ },
+ "allowed-ip": {
+ "label": "Allowed IP Addresses",
+ "placeholder": "Default Allowed IP Addresses"
+ },
+ "mtu": {
+ "label": "MTU",
+ "placeholder": "The client MTU (0 = keep default)"
+ },
+ "keep-alive": {
+ "label": "Keep Alive Interval",
+ "placeholder": "Persistent Keepalive (0 = default)"
+ }
+ },
+
+ "button-apply-defaults": "Apply Peer Defaults"
+ },
+ "peer-view": {
+ "headline-peer": "Peer:",
+ "headline-endpoint": "Endpoint:",
+ "section-info": "Peer Information",
+ "section-status": "Current Status",
+ "section-config": "Configuration",
+ "identifier": "Identifier",
+ "ip": "IP Addresses",
+ "user": "Associated User",
+ "notes": "Notes",
+ "expiry-status": "Expires At",
+ "disabled-status": "Disabled At",
+ "traffic": "Traffic",
+ "connection-status": "Connection Stats",
+ "upload": "Uploaded Bytes (from Server to Peer)",
+ "download": "Downloaded Bytes (from Peer to Server)",
+ "pingable": "Is Pingable",
+ "handshake": "Last Handshake",
+ "connected-since": "Connected since",
+ "endpoint": "Endpoint",
+ "button-download": "Download configuration",
+ "button-email": "Send configuration via E-Mail"
+ },
+ "peer-edit": {
+ "headline-edit-peer": "Edit peer:",
+ "headline-edit-endpoint": "Edit endpoint:",
+ "headline-new-peer": "Create peer",
+ "headline-new-endpoint": "Create endpoint",
+ "header-general": "General",
+ "header-network": "Network",
+ "header-crypto": "Cryptography",
+ "header-hooks": "Hooks (Executed on Peer)",
+ "header-state": "State",
+ "display-name": {
+ "label": "Display Name",
+ "placeholder": "The descriptive name for the peer"
+ },
+ "linked-user": {
+ "label": "Linked User",
+ "placeholder": "The user account which owns this peer"
+ },
+ "private-key": {
+ "label": "Private Key",
+ "placeholder": "The private key"
+ },
+ "public-key": {
+ "label": "Public Key",
+ "placeholder": "The public key"
+ },
+ "preshared-key": {
+ "label": "Preshared Key",
+ "placeholder": "Optional pre-shared key"
+ },
+ "endpoint-public-key": {
+ "label": "Endpoint public Key",
+ "placeholder": "The public key of the remote endpoint"
+ },
+ "endpoint": {
+ "label": "Endpoint Address",
+ "placeholder": "The address of the remote endpoint"
+ },
+ "ip": {
+ "label": "IP Addresses",
+ "placeholder": "IP Addresses (CIDR format)"
+ },
+ "allowed-ip": {
+ "label": "Allowed IP Addresses",
+ "placeholder": "Allowed IP Addresses (CIDR format)"
+ },
+ "extra-allowed-ip": {
+ "label": "Extra allowed IP Addresses",
+ "placeholder": "Extra allowed IP's (Server Sided)",
+ "description": "Those IP's will be added on the remote WireGuard interface as allowed IP's."
+ },
+ "dns": {
+ "label": "DNS Server",
+ "placeholder": "The DNS servers that should be used"
+ },
+ "dns-search": {
+ "label": "DNS Search Domains",
+ "placeholder": "DNS search prefixes"
+ },
+ "keep-alive": {
+ "label": "Keep Alive Interval",
+ "placeholder": "Persistent Keepalive (0 = default)"
+ },
+ "mtu": {
+ "label": "MTU",
+ "placeholder": "The client MTU (0 = keep default)"
+ },
+ "pre-up": {
+ "label": "Pre-Up",
+ "placeholder": "One or multiple bash commands separated by ;"
+ },
+ "post-up": {
+ "label": "Post-Up",
+ "placeholder": "One or multiple bash commands separated by ;"
+ },
+ "pre-down": {
+ "label": "Pre-Down",
+ "placeholder": "One or multiple bash commands separated by ;"
+ },
+ "post-down": {
+ "label": "Post-Down",
+ "placeholder": "One or multiple bash commands separated by ;"
+ },
+ "disabled": {
+ "label": "Peer Disabled"
+ },
+ "ignore-global": {
+ "label": "Ignore global settings"
+ },
+ "expires-at": {
+ "label": "Expiry date"
+ }
+ },
+ "peer-multi-create": {
+ "headline-peer": "Create multiple peers",
+ "headline-endpoint": "Create multiple endpoints",
+ "identifiers": {
+ "label": "User Identifiers",
+ "placeholder": "User Identifiers",
+ "description": "A user identifier (the username) for which a peer should be created."
+ },
+ "prefix": {
+ "headline-peer": "Peer:",
+ "headline-endpoint": "Endpoint:",
+ "label": "Display Name Prefix",
+ "placeholder": "The prefix",
+ "description": "A prefix that is added to the peers display name."
+ }
+ }
+ }
+}
diff --git a/frontend/src/main.js b/frontend/src/main.js
new file mode 100644
index 0000000..300a8fb
--- /dev/null
+++ b/frontend/src/main.js
@@ -0,0 +1,45 @@
+import { createApp } from "vue";
+import { createPinia } from "pinia";
+
+import App from "./App.vue";
+import router from "./router";
+
+import i18n from "./lang";
+
+import Notifications from '@kyvg/vue3-notification'
+
+// Bootstrap (and theme)
+//import "bootstrap/dist/css/bootstrap.min.css"
+import "bootswatch/dist/lux/bootstrap.min.css";
+import "bootstrap";
+import "./assets/base.css";
+
+// Fontawesome
+import "@fortawesome/fontawesome-free/js/all.js"
+
+// Flags
+import "flag-icons/css/flag-icons.min.css"
+
+// Syntax Highlighting
+import 'prismjs'
+import 'prismjs/themes/prism-okaidia.css'
+
+const app = createApp(App);
+
+app.use(i18n)
+app.use(createPinia());
+app.use(router);
+app.use(Notifications);
+
+app.config.globalProperties.$filters = {
+ truncate(value, maxLength, suffix) {
+ suffix = suffix || '...'
+ if (value.length > maxLength) {
+ return value.substring(0, maxLength) + suffix;
+ } else {
+ return value;
+ }
+ }
+}
+
+app.mount("#app");
diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js
new file mode 100644
index 0000000..09ef969
--- /dev/null
+++ b/frontend/src/router/index.js
@@ -0,0 +1,109 @@
+import {createRouter, createWebHashHistory} from 'vue-router'
+import HomeView from '../views/HomeView.vue'
+import LoginView from '../views/LoginView.vue'
+import InterfaceView from '../views/InterfaceView.vue'
+
+import {authStore} from '@/stores/auth'
+import {notify} from "@kyvg/vue3-notification";
+
+const router = createRouter({
+ history: createWebHashHistory(),
+ routes: [
+ {
+ path: '/',
+ name: 'home',
+ component: HomeView
+ },
+ {
+ path: '/login',
+ name: 'login',
+ component: LoginView
+ },
+ {
+ path: '/interface',
+ name: 'interface',
+ component: InterfaceView
+ },
+ {
+ path: '/interfaces',
+ name: 'interfaces',
+ // route level code-splitting
+ // this generates a separate chunk (About.[hash].js) for this route
+ // which is lazy-loaded when the route is visited.
+ component: () => import('../views/InterfaceView.vue')
+ },
+ {
+ path: '/users',
+ name: 'users',
+ // route level code-splitting
+ // this generates a separate chunk (About.[hash].js) for this route
+ // which is lazy-loaded when the route is visited.
+ component: () => import('../views/UserView.vue')
+ },
+ {
+ path: '/profile',
+ name: 'profile',
+ // route level code-splitting
+ // this generates a separate chunk (About.[hash].js) for this route
+ // which is lazy-loaded when the route is visited.
+ component: () => import('../views/ProfileView.vue')
+ }
+ ],
+ linkActiveClass: "active",
+ linkExactActiveClass: "exact-active",
+})
+
+router.beforeEach(async (to) => {
+ const auth = authStore()
+
+ // check if the request was a successful oauth login
+ if ('wgLoginState' in to.query && !auth.IsAuthenticated) {
+ const state = to.query['wgLoginState']
+ const returnUrl = auth.ReturnUrl
+ console.log("Oauth login callback:", state)
+
+ if (state === "success") {
+ try {
+ const uid = await auth.LoadSession()
+ console.log("Oauth login completed for UID:", uid)
+ console.log("Continuing to:", returnUrl)
+
+ notify({
+ title: "Logged in",
+ text: "Authentication suceeded!",
+ type: 'success',
+ })
+
+ auth.ResetReturnUrl()
+ return returnUrl
+ } catch (e) {
+ notify({
+ title: "Login failed!",
+ text: "Oauth session is invalid!",
+ type: 'error',
+ })
+
+ return '/login'
+ }
+ } else {
+ notify({
+ title: "Login failed!",
+ text: "Authentication via Oauth failed!",
+ type: 'error',
+ })
+
+ return '/login'
+ }
+ }
+
+ // redirect to login page if not logged in and trying to access a restricted page
+ const publicPages = ['/', '/login']
+ const authRequired = !publicPages.includes(to.path)
+
+ if (authRequired && !auth.IsAuthenticated) {
+ auth.SetReturnUrl(to.fullPath) // store original destination before starting the auth process
+ return '/login'
+ }
+})
+
+export default router
diff --git a/frontend/src/stores/auth.js b/frontend/src/stores/auth.js
new file mode 100644
index 0000000..58b3759
--- /dev/null
+++ b/frontend/src/stores/auth.js
@@ -0,0 +1,125 @@
+import { defineStore } from 'pinia'
+
+import { notify } from "@kyvg/vue3-notification";
+import { apiWrapper } from '@/helpers/fetch-wrapper'
+import router from '../router'
+
+export const authStore = defineStore({
+ id: 'auth',
+ state: () => ({
+ // initialize state from local storage to enable user to stay logged in
+ user: JSON.parse(localStorage.getItem('user')),
+ providers: [],
+ returnUrl: localStorage.getItem('returnUrl')
+ }),
+ getters: {
+ UserIdentifier: (state) => state.user?.Identifier || 'unknown',
+ User: (state) => state.user,
+ LoginProviders: (state) => state.providers,
+ IsAuthenticated: (state) => state.user != null,
+ IsAdmin: (state) => state.user?.IsAdmin || false,
+ ReturnUrl: (state) => state.returnUrl || '/',
+ },
+ actions: {
+ SetReturnUrl(link) {
+ this.returnUrl = link
+ localStorage.setItem('returnUrl', link)
+ },
+ ResetReturnUrl() {
+ this.returnUrl = null
+ localStorage.removeItem('returnUrl')
+ },
+ // LoadProviders always returns a fulfilled promise, even if the request failed.
+ async LoadProviders() {
+ apiWrapper.get(`/auth/providers`)
+ .then(providers => this.providers = providers)
+ .catch(error => {
+ this.providers = []
+ console.log("Failed to load auth providers: ", error)
+ notify({
+ title: "Backend Connection Failure",
+ text: "Failed to load external authentication providers!",
+ })
+ })
+ },
+
+ // LoadSession returns promise that might have been rejected if the session was not authenticated.
+ async LoadSession() {
+ return apiWrapper.get(`/auth/session`)
+ .then(session => {
+ if (session.LoggedIn === true) {
+ this.ResetReturnUrl()
+ this.setUserInfo(session)
+ return session.UserIdentifier
+ } else {
+ this.setUserInfo(null)
+ return Promise.reject(new Error('session not authenticated'))
+ }
+ })
+ .catch(err => {
+ this.setUserInfo(null)
+ return Promise.reject(err)
+ })
+ },
+ // Login returns promise that might have been rejected if the login attempt was not successful.
+ async Login(username, password) {
+ return apiWrapper.post(`/auth/login`, { username, password })
+ .then(user => {
+ this.ResetReturnUrl()
+ this.setUserInfo(user)
+ return user.Identifier
+ })
+ .catch(err => {
+ console.log("Login failed:", err)
+ this.setUserInfo(null)
+ return Promise.reject(new Error("login failed"))
+ })
+ },
+ async Logout() {
+ this.setUserInfo(null)
+ this.ResetReturnUrl() // just to be sure^^
+
+ try {
+ await apiWrapper.post(`/auth/logout`)
+ } catch (e) {
+ console.log("Logout request failed:", e)
+ }
+
+ notify({
+ title: "Logged Out",
+ text: "Logout successful!",
+ type: "warn",
+ })
+
+
+ await router.push('/login')
+ },
+ // -- internal setters
+ setUserInfo(userInfo) {
+ // store user details and jwt in local storage to keep user logged in between page refreshes
+ if (userInfo) {
+ if ('UserIdentifier' in userInfo) { // session object
+ this.user = {
+ Identifier: userInfo['UserIdentifier'],
+ Firstname: userInfo['UserFirstname'],
+ Lastname: userInfo['UserLastname'],
+ Email: userInfo['UserEmail'],
+ IsAdmin: userInfo['IsAdmin']
+ }
+ } else { // user object
+ this.user = {
+ Identifier: userInfo['Identifier'],
+ Firstname: userInfo['Firstname'],
+ Lastname: userInfo['Lastname'],
+ Email: userInfo['Email'],
+ IsAdmin: userInfo['IsAdmin']
+ }
+ }
+ localStorage.setItem('user', JSON.stringify(this.user))
+ } else {
+ this.user = null
+ localStorage.removeItem('user')
+ }
+ },
+ }
+});
\ No newline at end of file
diff --git a/frontend/src/stores/interfaces.js b/frontend/src/stores/interfaces.js
new file mode 100644
index 0000000..58917bf
--- /dev/null
+++ b/frontend/src/stores/interfaces.js
@@ -0,0 +1,152 @@
+import { defineStore } from 'pinia'
+
+import {apiWrapper} from '@/helpers/fetch-wrapper'
+import {notify} from "@kyvg/vue3-notification";
+import { freshInterface } from '@/helpers/models';
+import { base64_url_encode } from '@/helpers/encoding';
+
+const baseUrl = `/interface`
+
+export const interfaceStore = defineStore({
+ id: 'interfaces',
+ state: () => ({
+ interfaces: [],
+ prepared: freshInterface(),
+ configuration: "",
+ selected: "",
+ fetching: false,
+ }),
+ getters: {
+ Count: (state) => state.interfaces.length,
+ Prepared: (state) => {console.log("STATE:", state.prepared); return state.prepared},
+ All: (state) => state.interfaces,
+ Find: (state) => {
+ return (id) => state.interfaces.find((p) => p.Identifier === id)
+ },
+ GetSelected: (state) => state.interfaces.find((i) => i.Identifier === state.selected) || state.interfaces[0],
+ isFetching: (state) => state.fetching,
+ },
+ actions: {
+ setInterfaces(interfaces) {
+ this.interfaces = interfaces
+ if (this.interfaces.length > 0) {
+ this.selected = this.interfaces[0].Identifier
+ } else {
+ this.selected = ""
+ }
+ this.fetching = false
+ },
+ async LoadInterfaces() {
+ this.fetching = true
+ return apiWrapper.get(`${baseUrl}/all`)
+ .then(this.setInterfaces)
+ .catch(error => {
+ this.setInterfaces([])
+ console.log("Failed to load interfaces: ", error)
+ notify({
+ title: "Backend Connection Failure",
+ text: "Failed to load interfaces!",
+ })
+ })
+ },
+ setPreparedInterface(iface) {
+ this.prepared = iface;
+ },
+ setInterfaceConfig(ifaceConfig) {
+ this.configuration = ifaceConfig;
+ },
+ async PrepareInterface() {
+ return apiWrapper.get(`${baseUrl}/prepare`)
+ .then(this.setPreparedInterface)
+ .catch(error => {
+ this.prepared = freshInterface()
+ console.log("Failed to load prepared interface: ", error)
+ notify({
+ title: "Backend Connection Failure",
+ text: "Failed to load prepared interface!",
+ })
+ })
+ },
+ async LoadInterfaceConfig(id) {
+ return apiWrapper.get(`${baseUrl}/config/${base64_url_encode(id)}`)
+ .then(this.setInterfaceConfig)
+ .catch(error => {
+ this.configuration = ""
+ console.log("Failed to load interface configuration: ", error)
+ notify({
+ title: "Backend Connection Failure",
+ text: "Failed to load interface configuration!",
+ })
+ })
+ },
+ async DeleteInterface(id) {
+ this.fetching = true
+ return apiWrapper.delete(`${baseUrl}/${base64_url_encode(id)}`)
+ .then(() => {
+ this.interfaces = this.interfaces.filter(i => i.Identifier !== id)
+ if (this.interfaces.length > 0) {
+ this.selected = this.interfaces[0].Identifier
+ } else {
+ this.selected = ""
+ }
+ this.fetching = false
+ })
+ .catch(error => {
+ this.fetching = false
+ console.log(error)
+ throw new Error(error)
+ })
+ },
+ async UpdateInterface(id, formData) {
+ this.fetching = true
+ return apiWrapper.put(`${baseUrl}/${base64_url_encode(id)}`, formData)
+ .then(iface => {
+ let idx = this.interfaces.findIndex((i) => i.Identifier === id)
+ this.interfaces[idx] = iface
+ this.fetching = false
+ })
+ .catch(error => {
+ this.fetching = false
+ console.log(error)
+ throw new Error(error)
+ })
+ },
+ async CreateInterface(formData) {
+ this.fetching = true
+ return apiWrapper.post(`${baseUrl}/new`, formData)
+ .then(iface => {
+ this.interfaces.push(iface)
+ this.fetching = false
+ })
+ .catch(error => {
+ this.fetching = false
+ console.log(error)
+ throw new Error(error)
+ })
+ },
+ async ApplyPeerDefaults(id, formData) {
+ this.fetching = true
+ return apiWrapper.post(`${baseUrl}/${base64_url_encode(id)}/apply-peer-defaults`, formData)
+ .then(() => {
+ this.fetching = false
+ })
+ .catch(error => {
+ this.fetching = false
+ console.log(error)
+ throw new Error(error)
+ })
+ },
+ async SaveConfiguration(id) {
+ this.fetching = true
+ return apiWrapper.post(`${baseUrl}/${base64_url_encode(id)}/save-config`)
+ .then(() => {
+ this.fetching = false
+ })
+ .catch(error => {
+ this.fetching = false
+ console.log(error)
+ throw new Error(error)
+ })
+ }
+ }
+})
diff --git a/frontend/src/stores/peers.js b/frontend/src/stores/peers.js
new file mode 100644
index 0000000..df68116
--- /dev/null
+++ b/frontend/src/stores/peers.js
@@ -0,0 +1,258 @@
+import { defineStore } from 'pinia'
+import {apiWrapper} from "@/helpers/fetch-wrapper";
+import {notify} from "@kyvg/vue3-notification";
+import {interfaceStore} from "./interfaces";
+import {freshPeer, freshStats} from '@/helpers/models';
+import { base64_url_encode } from '@/helpers/encoding';
+
+const baseUrl = `/peer`
+
+export const peerStore = defineStore({
+ id: 'peers',
+ state: () => ({
+ peers: [],
+ stats: {},
+ statsEnabled: false,
+ peer: freshPeer(),
+ prepared: freshPeer(),
+ configuration: "",
+ filter: "",
+ pageSize: 10,
+ pageOffset: 0,
+ pages: [],
+ fetching: false,
+ }),
+ getters: {
+ Find: (state) => {
+ return (id) => state.peers.find((p) => p.Identifier === id)
+ },
+
+ Count: (state) => state.peers.length,
+ Prepared: (state) => {console.log("STATE:", state.prepared); return state.prepared},
+ FilteredCount: (state) => state.Filtered.length,
+ All: (state) => state.peers,
+ Filtered: (state) => {
+ if (!state.filter) {
+ return state.peers
+ }
+ return state.peers.filter((p) => {
+ return p.DisplayName.includes(state.filter) || p.Identifier.includes(state.filter)
+ })
+ },
+ FilteredAndPaged: (state) => {
+ return state.Filtered.slice(state.pageOffset, state.pageOffset + state.pageSize)
+ },
+ ConfigQrUrl: (state) => {
+ return (id) => state.peers.find((p) => p.Identifier === id) ? apiWrapper.url(`${baseUrl}/config-qr/${base64_url_encode(id)}`) : ''
+ },
+ isFetching: (state) => state.fetching,
+ hasNextPage: (state) => state.pageOffset < (state.FilteredCount - state.pageSize),
+ hasPrevPage: (state) => state.pageOffset > 0,
+ currentPage: (state) => (state.pageOffset / state.pageSize)+1,
+ Statistics: (state) => {
+ return (id) => state.statsEnabled && (id in state.stats) ? state.stats[id] : freshStats()
+ },
+ hasStatistics: (state) => state.statsEnabled,
+
+ },
+ actions: {
+ afterPageSizeChange() {
+ // reset pageOffset to avoid problems with new page sizes
+ this.pageOffset = 0
+ this.calculatePages()
+ },
+ calculatePages() {
+ let pageCounter = 1;
+ this.pages = []
+ for (let i = 0; i < this.FilteredCount; i+=this.pageSize) {
+ this.pages.push(pageCounter++)
+ }
+ },
+ gotoPage(page) {
+ this.pageOffset = (page-1) * this.pageSize
+
+ this.calculatePages()
+ },
+ nextPage() {
+ this.pageOffset += this.pageSize
+
+ this.calculatePages()
+ },
+ previousPage() {
+ this.pageOffset -= this.pageSize
+
+ this.calculatePages()
+ },
+ setPeers(peers) {
+ this.peers = peers
+ this.calculatePages()
+ this.fetching = false
+ },
+ setPeer(peer) {
+ this.peer = peer
+ this.fetching = false
+ },
+ setPreparedPeer(peer) {
+ this.prepared = peer;
+ },
+ setPeerConfig(config) {
+ this.configuration = config;
+ },
+ setStats(statsResponse) {
+ if (!statsResponse) {
+ this.stats = {}
+ this.statsEnabled = false
+ }
+ this.stats = statsResponse.Stats
+ this.statsEnabled = statsResponse.Enabled
+ },
+ async PreparePeer(interfaceId) {
+ return apiWrapper.get(`${baseUrl}/iface/${base64_url_encode(interfaceId)}/prepare`)
+ .then(this.setPreparedPeer)
+ .catch(error => {
+ this.prepared = freshPeer()
+ console.log("Failed to load prepared peer: ", error)
+ notify({
+ title: "Backend Connection Failure",
+ text: "Failed to load prepared peer!",
+ })
+ })
+ },
+ async MailPeerConfig(linkOnly, ids) {
+ return apiWrapper.post(`${baseUrl}/config-mail`, {
+ Identifiers: ids,
+ LinkOnly: linkOnly
+ })
+ .then(() => {
+ notify({
+ title: "Peer Configuration sent",
+ text: "Email sent to linked user!",
+ })
+ })
+ .catch(error => {
+ console.log("Failed to send peer configuration: ", error)
+ throw new Error(error)
+ })
+ },
+ async LoadPeerConfig(id) {
+ return apiWrapper.get(`${baseUrl}/config/${base64_url_encode(id)}`)
+ .then(this.setPeerConfig)
+ .catch(error => {
+ this.configuration = ""
+ console.log("Failed to load peer configuration: ", error)
+ notify({
+ title: "Backend Connection Failure",
+ text: "Failed to load peer configuration!",
+ })
+ })
+ },
+ async LoadPeer(id) {
+ this.fetching = true
+ return apiWrapper.get(`${baseUrl}/${base64_url_encode(id)}`)
+ .then(this.setPeer)
+ .catch(error => {
+ this.setPeers([])
+ console.log("Failed to load peer: ", error)
+ notify({
+ title: "Backend Connection Failure",
+ text: "Failed to load peer!",
+ })
+ })
+ },
+ async LoadStats(interfaceId) {
+ // if no interfaceId is given, use the currently selected interface
+ if (!interfaceId) {
+ interfaceId = interfaceStore().GetSelected.Identifier
+ if (!interfaceId) {
+ return // no interface, nothing to load
+ }
+ }
+ this.fetching = true
+
+ return apiWrapper.get(`${baseUrl}/iface/${base64_url_encode(interfaceId)}/stats`)
+ .then(this.setStats)
+ .catch(error => {
+ this.setStats(undefined)
+ console.log("Failed to load peer stats: ", error)
+ notify({
+ title: "Backend Connection Failure",
+ text: "Failed to load peer stats!",
+ })
+ })
+ },
+ async DeletePeer(id) {
+ this.fetching = true
+ return apiWrapper.delete(`${baseUrl}/${base64_url_encode(id)}`)
+ .then(() => {
+ this.peers = this.peers.filter(p => p.Identifier !== id)
+ this.fetching = false
+ })
+ .catch(error => {
+ this.fetching = false
+ console.log(error)
+ throw new Error(error)
+ })
+ },
+ async UpdatePeer(id, formData) {
+ this.fetching = true
+ return apiWrapper.put(`${baseUrl}/${base64_url_encode(id)}`, formData)
+ .then(peer => {
+ let idx = this.peers.findIndex((p) => p.Identifier === id)
+ this.peers[idx] = peer
+ this.fetching = false
+ })
+ .catch(error => {
+ this.fetching = false
+ console.log(error)
+ throw new Error(error)
+ })
+ },
+ async CreatePeer(interfaceId, formData) {
+ this.fetching = true
+ return apiWrapper.post(`${baseUrl}/iface/${base64_url_encode(interfaceId)}/new`, formData)
+ .then(peer => {
+ this.peers.push(peer)
+ this.fetching = false
+ })
+ .catch(error => {
+ this.fetching = false
+ console.log(error)
+ throw new Error(error)
+ })
+ },
+ async CreateMultiplePeers(interfaceId, formData) {
+ this.fetching = true
+ return apiWrapper.post(`${baseUrl}/iface/${base64_url_encode(interfaceId)}/multiplenew`, formData)
+ .then(peers => {
+ this.peers.push(...peers)
+ this.fetching = false
+ })
+ .catch(error => {
+ this.fetching = false
+ console.log(error)
+ throw new Error(error)
+ })
+ },
+ async LoadPeers(interfaceId) {
+ // if no interfaceId is given, use the currently selected interface
+ if (!interfaceId) {
+ interfaceId = interfaceStore().GetSelected.Identifier
+ if (!interfaceId) {
+ return // no interface, nothing to load
+ }
+ }
+ this.fetching = true
+
+ return apiWrapper.get(`${baseUrl}/iface/${base64_url_encode(interfaceId)}/all`)
+ .then(this.setPeers)
+ .catch(error => {
+ this.setPeers([])
+ console.log("Failed to load peers: ", error)
+ notify({
+ title: "Backend Connection Failure",
+ text: "Failed to load peers!",
+ })
+ })
+ }
+ }
+})
diff --git a/frontend/src/stores/profile.js b/frontend/src/stores/profile.js
new file mode 100644
index 0000000..f795d22
--- /dev/null
+++ b/frontend/src/stores/profile.js
@@ -0,0 +1,137 @@
+import { defineStore } from 'pinia'
+import {apiWrapper} from "@/helpers/fetch-wrapper";
+import {notify} from "@kyvg/vue3-notification";
+import {authStore} from "@/stores/auth";
+import { base64_url_encode } from '@/helpers/encoding';
+import {freshStats} from "@/helpers/models";
+
+const baseUrl = `/user`
+
+export const profileStore = defineStore({
+ id: 'profile',
+ state: () => ({
+ peers: [],
+ stats: {},
+ statsEnabled: false,
+ user: {},
+ filter: "",
+ pageSize: 10,
+ pageOffset: 0,
+ pages: [],
+ fetching: false,
+ }),
+ getters: {
+ FindPeers: (state) => {
+ return (id) => state.peers.find((p) => p.Identifier === id)
+ },
+ CountPeers: (state) => state.peers.length,
+ FilteredPeerCount: (state) => state.FilteredPeers.length,
+ Peers: (state) => state.peers,
+ FilteredPeers: (state) => {
+ if (!state.filter) {
+ return state.peers
+ }
+ return state.peers.filter((p) => {
+ return p.DisplayName.includes(state.filter) || p.Identifier.includes(state.filter)
+ })
+ },
+ FilteredAndPagedPeers: (state) => {
+ return state.FilteredPeers.slice(state.pageOffset, state.pageOffset + state.pageSize)
+ },
+ isFetching: (state) => state.fetching,
+ hasNextPage: (state) => state.pageOffset < (state.FilteredPeerCount - state.pageSize),
+ hasPrevPage: (state) => state.pageOffset > 0,
+ currentPage: (state) => (state.pageOffset / state.pageSize)+1,
+ Statistics: (state) => {
+ return (id) => state.statsEnabled && (id in state.stats) ? state.stats[id] : freshStats()
+ },
+ hasStatistics: (state) => state.statsEnabled,
+ },
+ actions: {
+ afterPageSizeChange() {
+ // reset pageOffset to avoid problems with new page sizes
+ this.pageOffset = 0
+ this.calculatePages()
+ },
+ calculatePages() {
+ let pageCounter = 1;
+ this.pages = []
+ for (let i = 0; i < this.FilteredPeerCount; i+=this.pageSize) {
+ this.pages.push(pageCounter++)
+ }
+ },
+ gotoPage(page) {
+ this.pageOffset = (page-1) * this.pageSize
+
+ this.calculatePages()
+ },
+ nextPage() {
+ this.pageOffset += this.pageSize
+
+ this.calculatePages()
+ },
+ previousPage() {
+ this.pageOffset -= this.pageSize
+
+ this.calculatePages()
+ },
+ setPeers(peers) {
+ this.peers = peers
+ this.fetching = false
+ },
+ setUser(user) {
+ this.user = user
+ this.fetching = false
+ },
+ setStats(statsResponse) {
+ if (!statsResponse) {
+ this.stats = {}
+ this.statsEnabled = false
+ }
+ this.stats = statsResponse.Stats
+ this.statsEnabled = statsResponse.Enabled
+ },
+ async LoadPeers() {
+ this.fetching = true
+ let currentUser = authStore().user.Identifier
+ return apiWrapper.get(`${baseUrl}/${base64_url_encode(currentUser)}/peers`)
+ .then(this.setPeers)
+ .catch(error => {
+ this.setPeers([])
+ console.log("Failed to load user peers for ", currentUser, ": ", error)
+ notify({
+ title: "Backend Connection Failure",
+ text: "Failed to load user peers!",
+ })
+ })
+ },
+ async LoadStats() {
+ this.fetching = true
+ let currentUser = authStore().user.Identifier
+ return apiWrapper.get(`${baseUrl}/${base64_url_encode(currentUser)}/stats`)
+ .then(this.setStats)
+ .catch(error => {
+ this.setStats(undefined)
+ console.log("Failed to load peer stats: ", error)
+ notify({
+ title: "Backend Connection Failure",
+ text: "Failed to load peer stats!",
+ })
+ })
+ },
+ async LoadUser() {
+ this.fetching = true
+ let currentUser = authStore().user.Identifier
+ return apiWrapper.get(`${baseUrl}/${base64_url_encode(currentUser)}`)
+ .then(this.setUser)
+ .catch(error => {
+ this.setUser({})
+ console.log("Failed to load user for ", currentUser, ": ", error)
+ notify({
+ title: "Backend Connection Failure",
+ text: "Failed to load user!",
+ })
+ })
+ },
+ }
+})
diff --git a/frontend/src/stores/security.js b/frontend/src/stores/security.js
new file mode 100644
index 0000000..1d81a38
--- /dev/null
+++ b/frontend/src/stores/security.js
@@ -0,0 +1,32 @@
+import { defineStore } from 'pinia'
+
+import { notify } from "@kyvg/vue3-notification";
+import { apiWrapper } from '@/helpers/fetch-wrapper'
+
+export const securityStore = defineStore({
+ id: 'security',
+ state: () => ({
+ csrfToken: "",
+ }),
+ getters: {
+ CsrfToken: (state) => state.csrfToken,
+ },
+ actions: {
+ SetCsrfToken(token) {
+ this.csrfToken = token
+ },
+ // LoadSecurityProperties always returns a fulfilled promise, even if the request failed.
+ async LoadSecurityProperties() {
+ await apiWrapper.get(`/csrf`)
+ .then(token => this.SetCsrfToken(token))
+ .catch(error => {
+ this.SetCsrfToken("");
+ console.log("Failed to load csrf token: ", error);
+ notify({
+ title: "Backend Connection Failure",
+ text: "Failed to load csrf token!",
+ });
+ })
+ }
+ }
+});
\ No newline at end of file
diff --git a/frontend/src/stores/settings.js b/frontend/src/stores/settings.js
new file mode 100644
index 0000000..6a404d8
--- /dev/null
+++ b/frontend/src/stores/settings.js
@@ -0,0 +1,36 @@
+import { defineStore } from 'pinia'
+
+import { notify } from "@kyvg/vue3-notification";
+import { apiWrapper } from '@/helpers/fetch-wrapper'
+
+const baseUrl = `/config`
+
+export const settingsStore = defineStore({
+ id: 'settings',
+ state: () => ({
+ settings: {},
+ }),
+ getters: {
+ Setting: (state) => {
+ return (key) => (key in state.settings) ? state.settings[key] : undefined
+ }
+ },
+ actions: {
+ setSettings(settings) {
+ this.settings = settings
+ },
+ // LoadSecurityProperties always returns a fulfilled promise, even if the request failed.
+ async LoadSettings() {
+ await apiWrapper.get(`${baseUrl}/settings`)
+ .then(data => this.setSettings(data))
+ .catch(error => {
+ this.setSettings({});
+ console.log("Failed to load settings: ", error);
+ notify({
+ title: "Backend Connection Failure",
+ text: "Failed to load settings!",
+ });
+ })
+ }
+ }
+});
\ No newline at end of file
diff --git a/frontend/src/stores/users.js b/frontend/src/stores/users.js
new file mode 100644
index 0000000..feeff18
--- /dev/null
+++ b/frontend/src/stores/users.js
@@ -0,0 +1,147 @@
+import { defineStore } from 'pinia'
+import {apiWrapper} from "@/helpers/fetch-wrapper";
+import {notify} from "@kyvg/vue3-notification";
+import { base64_url_encode } from '@/helpers/encoding';
+
+const baseUrl = `/user`
+
+export const userStore = defineStore({
+ id: 'users',
+ state: () => ({
+ userPeers: [],
+ users: [],
+ filter: "",
+ pageSize: 10,
+ pageOffset: 0,
+ pages: [],
+ fetching: false,
+ }),
+ getters: {
+ Find: (state) => {
+ return (id) => state.users.find((p) => p.Identifier === id)
+ },
+ Count: (state) => state.users.length,
+ FilteredCount: (state) => state.Filtered.length,
+ All: (state) => state.users,
+ Peers: (state) => state.userPeers,
+ Filtered: (state) => {
+ if (!state.filter) {
+ return state.users
+ }
+ return state.users.filter((u) => {
+ return u.Firstname.includes(state.filter) || u.Lastname.includes(state.filter) || u.Email.includes(state.filter) || u.Identifier.includes(state.filter)
+ })
+ },
+ FilteredAndPaged: (state) => {
+ return state.Filtered.slice(state.pageOffset, state.pageOffset + state.pageSize)
+ },
+ isFetching: (state) => state.fetching,
+ hasNextPage: (state) => state.pageOffset < (state.FilteredCount - state.pageSize),
+ hasPrevPage: (state) => state.pageOffset > 0,
+ currentPage: (state) => (state.pageOffset / state.pageSize)+1,
+ },
+ actions: {
+ afterPageSizeChange() {
+ // reset pageOffset to avoid problems with new page sizes
+ this.pageOffset = 0
+ this.calculatePages()
+ },
+ calculatePages() {
+ let pageCounter = 1;
+ this.pages = []
+ for (let i = 0; i < this.FilteredCount; i+=this.pageSize) {
+ this.pages.push(pageCounter++)
+ }
+ },
+ gotoPage(page) {
+ this.pageOffset = (page-1) * this.pageSize
+
+ this.calculatePages()
+ },
+ nextPage() {
+ this.pageOffset += this.pageSize
+
+ this.calculatePages()
+ },
+ previousPage() {
+ this.pageOffset -= this.pageSize
+
+ this.calculatePages()
+ },
+ setUsers(users) {
+ this.users = users
+ this.calculatePages()
+ this.fetching = false
+ },
+ setUserPeers(peers) {
+ this.userPeers = peers
+ this.fetching = false
+ },
+ async LoadUsers() {
+ this.fetching = true
+ return apiWrapper.get(`${baseUrl}/all`)
+ .then(this.setUsers)
+ .catch(error => {
+ this.setUsers([])
+ console.log("Failed to load users: ", error)
+ notify({
+ title: "Backend Connection Failure",
+ text: "Failed to load users!",
+ })
+ })
+ },
+ async DeleteUser(id) {
+ this.fetching = true
+ return apiWrapper.delete(`${baseUrl}/${base64_url_encode(id)}`)
+ .then(() => {
+ this.users = this.users.filter(u => u.Identifier !== id)
+ this.fetching = false
+ })
+ .catch(error => {
+ this.fetching = false
+ console.log(error)
+ throw new Error(error)
+ })
+ },
+ async UpdateUser(id, formData) {
+ this.fetching = true
+ return apiWrapper.put(`${baseUrl}/${base64_url_encode(id)}`, formData)
+ .then(user => {
+ let idx = this.users.findIndex((u) => u.Identifier === id)
+ this.users[idx] = user
+ this.fetching = false
+ })
+ .catch(error => {
+ this.fetching = false
+ console.log(error)
+ throw new Error(error)
+ })
+ },
+ async CreateUser(formData) {
+ this.fetching = true
+ return apiWrapper.post(`${baseUrl}/new`, formData)
+ .then(user => {
+ this.users.push(user)
+ this.fetching = false
+ })
+ .catch(error => {
+ this.fetching = false
+ console.log(error)
+ throw new Error(error)
+ })
+ },
+ async LoadUserPeers(id) {
+ this.fetching = true
+ return apiWrapper.get(`${baseUrl}/${base64_url_encode(id)}/peers`)
+ .then(this.setUserPeers)
+ .catch(error => {
+ this.setUserPeers([])
+ console.log("Failed to load user peers for ",id ,": ", error)
+ notify({
+ title: "Backend Connection Failure",
+ text: "Failed to load user peers!",
+ })
+ })
+ },
+ }
+})
diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue
new file mode 100644
index 0000000..b0dafe8
--- /dev/null
+++ b/frontend/src/views/HomeView.vue
@@ -0,0 +1,73 @@
+
+
+
+
+
+ {{ $t('home.abstract') }}
+
+
+
+
{{ $t('home.profiles.headline') }}
+
{{ $t('home.profiles.abstract') }}
+
+
{{ $t('home.profiles.content') }}
+
+ {{ $t('home.profiles.button') }}
+
+
+
+
+
{{ $t('home.admin.headline') }}
+
{{ $t('home.admin.abstract') }}
+
+
{{ $t('home.admin.content') }}
+
+ {{ $t('home.admin.button-admin') }}
+ {{ $t('home.admin.button-user') }}
+
+
+
+ {{ $t('home.info-headline') }}
+
+
diff --git a/frontend/src/views/InterfaceView.vue b/frontend/src/views/InterfaceView.vue
new file mode 100644
index 0000000..e88e0c2
--- /dev/null
+++ b/frontend/src/views/InterfaceView.vue
@@ -0,0 +1,390 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ $t('interfaces.no-interface.headline') }}
+
{{ $t('interfaces.no-interface.abstract') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('interfaces.interface.key') }}:
+ {{interfaces.GetSelected.PublicKey}}
+
+
+ {{ $t('interfaces.interface.endpoint') }}:
+ {{interfaces.GetSelected.PeerDefEndpoint}}
+
+
+ {{ $t('interfaces.interface.port') }}:
+ {{interfaces.GetSelected.ListenPort}}
+
+
+ {{ $t('interfaces.interface.peers') }}:
+ {{interfaces.GetSelected.EnabledPeers}}
+
+
+ {{ $t('interfaces.interface.total-peers') }}:
+ {{interfaces.GetSelected.TotalPeers}}
+
+
+
+
+
+
+
+
+ {{ $t('interfaces.interface.ip') }}:
+ {{addr}}
+
+
+ {{ $t('interfaces.interface.dns') }}:
+ {{addr}}
+
+
+ {{ $t('interfaces.interface.mtu') }}:
+ {{interfaces.GetSelected.Mtu}}
+
+
+ {{ $t('interfaces.interface.default-keep-alive') }}:
+ {{interfaces.GetSelected.PeerDefPersistentKeepalive}}
+
+
+ {{ $t('interfaces.interface.default-allowed-ip') }}:
+ {{addr}}
+
+
+
+
+
+
+
+
+
+
+ {{ $t('interfaces.interface.key') }}:
+ {{interfaces.GetSelected.PublicKey}}
+
+
+ {{ $t('interfaces.interface.endpoints') }}:
+ {{interfaces.GetSelected.EnabledPeers}}
+
+
+ {{ $t('interfaces.interface.total-endpoints') }}:
+ {{interfaces.GetSelected.TotalPeers}}
+
+
+
+
+
+
+
+
+ {{ $t('interfaces.interface.ip') }}:
+ {{addr}}
+
+
+ {{ $t('interfaces.interface.dns') }}:
+ {{addr}}
+
+
+ {{ $t('interfaces.interface.mtu') }}:
+ {{interfaces.GetSelected.Mtu}}
+
+
+
+
+
+
+
+
+
+
+ {{ $t('interfaces.interface.key') }}:
+ {{interfaces.GetSelected.PublicKey}}
+
+
+ {{ $t('interfaces.interface.endpoint') }}:
+ {{interfaces.GetSelected.PeerDefEndpoint}}
+
+
+ {{ $t('interfaces.interface.port') }}:
+ {{interfaces.GetSelected.ListenPort}}
+
+
+ {{ $t('interfaces.interface.peers') }}:
+ {{interfaces.GetSelected.EnabledPeers}}
+
+
+ {{ $t('interfaces.interface.total-peers') }}:
+ {{interfaces.GetSelected.TotalPeers}}
+
+
+
+
+
+
+
+
+ {{ $t('interfaces.interface.ip') }}:
+ {{addr}}
+
+
+ {{ $t('interfaces.interface.default-allowed-ip') }}:
+ {{addr}}
+
+
+ {{ $t('interfaces.interface.dns') }}:
+ {{addr}}
+
+
+ {{ $t('interfaces.interface.mtu') }}:
+ {{interfaces.GetSelected.Mtu}}
+
+
+ {{ $t('interfaces.interface.default-keep-alive') }}:
+ {{interfaces.GetSelected.PeerDefPersistentKeepalive}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ $t('interfaces.headline-peers') }}
+ {{ $t('interfaces.headline-endpoints') }}
+
+
+
+
+
+
+
{{ $t('interfaces.no-peer.headline') }}
+
{{ $t('interfaces.no-peer.abstract') }}
+
+
+
+
+
+
diff --git a/frontend/src/views/LoginView.vue b/frontend/src/views/LoginView.vue
new file mode 100644
index 0000000..1523c27
--- /dev/null
+++ b/frontend/src/views/LoginView.vue
@@ -0,0 +1,113 @@
+
+
+
+
+
diff --git a/frontend/src/views/ProfileView.vue b/frontend/src/views/ProfileView.vue
new file mode 100644
index 0000000..9020481
--- /dev/null
+++ b/frontend/src/views/ProfileView.vue
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
{{ $t('profile.headline') }}
+
+
+
+
+
+
+
{{ $t('profile.no-peer.headline') }}
+
{{ $t('profile.no-peer.abstract') }}
+
+
+
+
+
+
diff --git a/frontend/src/views/UserView.vue b/frontend/src/views/UserView.vue
new file mode 100644
index 0000000..c460060
--- /dev/null
+++ b/frontend/src/views/UserView.vue
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
{{ $t('users.headline') }}
+
+
+
+
+
+
+
{{ $t('users.no-user.headline') }}
+
{{ $t('users.no-user.abstract') }}
+
+
+
+
+
+
diff --git a/frontend/vite.config.js b/frontend/vite.config.js
new file mode 100644
index 0000000..1a0bc42
--- /dev/null
+++ b/frontend/vite.config.js
@@ -0,0 +1,34 @@
+import { fileURLToPath, URL } from 'url'
+
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [vue()],
+ resolve: {
+ alias: {
+ '@': fileURLToPath(new URL('./src', import.meta.url))
+ }
+ },
+ build: {
+ outDir: '../internal/app/api/core/frontend-dist',
+ emptyOutDir: true
+ },
+ // local dev api (proxy to avoid cors problems)
+ server: {
+ port: 5000,
+ proxy: {
+ "/api/v0": {
+ target: "http://localhost:8888",
+ changeOrigin: true,
+ secure: false,
+ withCredentials: true,
+ headers: {
+ "x-wg-dev": true,
+ },
+ rewrite: (path) => path,
+ },
+ },
+ },
+})
diff --git a/go.mod b/go.mod
index 0523699..7786e4e 100644
--- a/go.mod
+++ b/go.mod
@@ -1,78 +1,106 @@
module github.com/h44z/wg-portal
-go 1.18
+go 1.20
require (
- git.prolicht.digital/golib/healthcheck v1.1.1
- github.com/evanphx/json-patch v5.6.0+incompatible
+ github.com/coreos/go-oidc/v3 v3.6.0
+ github.com/gin-contrib/cors v1.4.0
github.com/gin-contrib/sessions v0.0.5
- github.com/gin-gonic/gin v1.8.2
- github.com/go-ldap/ldap/v3 v3.4.4
- github.com/go-playground/validator/v10 v10.11.2
- github.com/kelseyhightower/envconfig v1.4.0
- github.com/milosgajdos/tenus v0.0.3
- github.com/pkg/errors v0.9.1
- github.com/sirupsen/logrus v1.9.0
- github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
- github.com/swaggo/files v1.0.0
- github.com/swaggo/gin-swagger v1.5.3
- github.com/swaggo/swag v1.8.10
- github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e
+ github.com/gin-gonic/gin v1.9.1
+ github.com/glebarez/sqlite v1.9.0
+ github.com/go-ldap/ldap/v3 v3.4.5
+ github.com/prometheus-community/pro-bing v0.3.0
+ github.com/sirupsen/logrus v1.9.3
+ github.com/stretchr/testify v1.8.4
+ github.com/swaggo/swag v1.16.1
github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f
github.com/utrack/gin-csrf v0.0.0-20190424104817-40fb8d2c8fca
- github.com/xhit/go-simple-mail/v2 v2.13.0
- golang.org/x/crypto v0.6.0
- golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230215201556-9c5414ab4bde
- gopkg.in/yaml.v3 v3.0.1
- gorm.io/driver/mysql v1.4.7
- gorm.io/driver/sqlite v1.4.4
- gorm.io/gorm v1.24.5
+ github.com/vardius/message-bus v1.1.5
+ github.com/vishvananda/netlink v1.1.0
+ github.com/xhit/go-simple-mail/v2 v2.15.0
+ github.com/yeqown/go-qrcode/v2 v2.2.2
+ golang.org/x/crypto v0.11.0
+ golang.org/x/oauth2 v0.10.0
+ golang.org/x/sys v0.10.0
+ golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
+ gopkg.in/yaml.v2 v2.4.0
+ gorm.io/driver/mysql v1.5.1
+ gorm.io/driver/postgres v1.5.2
+ gorm.io/driver/sqlserver v1.5.1
+ gorm.io/gorm v1.25.2
)
require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
+ github.com/bytedance/sonic v1.9.1 // indirect
+ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dchest/uniuri v1.2.0 // indirect
- github.com/docker/libcontainer v2.2.1+incompatible // indirect
+ github.com/dustin/go-humanize v1.0.1 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
+ github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
+ github.com/go-jose/go-jose/v3 v3.0.0 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
- github.com/go-openapi/spec v0.20.8 // indirect
- github.com/go-openapi/swag v0.22.3 // indirect
+ github.com/go-openapi/spec v0.20.9 // indirect
+ github.com/go-openapi/swag v0.22.4 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
- github.com/go-sql-driver/mysql v1.7.0 // indirect
+ github.com/go-playground/validator/v10 v10.14.1 // indirect
+ github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/go-test/deep v1.0.8 // indirect
- github.com/goccy/go-json v0.10.0 // indirect
+ github.com/goccy/go-json v0.10.2 // indirect
+ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
+ github.com/golang-sql/sqlexp v0.1.0 // indirect
+ github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.5.9 // indirect
+ github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/gorilla/sessions v1.2.1 // indirect
+ github.com/jackc/pgpassfile v1.0.0 // indirect
+ github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
+ github.com/jackc/pgx/v5 v5.4.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
- github.com/leodido/go-urn v1.2.1 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.5 // indirect
+ github.com/leodido/go-urn v1.2.4 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
- github.com/mattn/go-isatty v0.0.17 // indirect
- github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
- github.com/mdlayher/genetlink v1.3.1 // indirect
- github.com/mdlayher/netlink v1.7.1 // indirect
- github.com/mdlayher/socket v0.4.0 // indirect
+ github.com/mattn/go-isatty v0.0.19 // indirect
+ github.com/mdlayher/genetlink v1.3.2 // indirect
+ github.com/mdlayher/netlink v1.7.2 // indirect
+ github.com/mdlayher/socket v0.4.1 // indirect
+ github.com/microsoft/go-mssqldb v1.1.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
- github.com/pelletier/go-toml/v2 v2.0.6 // indirect
+ github.com/pelletier/go-toml/v2 v2.0.8 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b // indirect
+ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
+ github.com/stretchr/objx v0.5.0 // indirect
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
- github.com/ugorji/go/codec v1.2.9 // indirect
- golang.org/x/net v0.7.0 // indirect
- golang.org/x/sync v0.1.0 // indirect
- golang.org/x/sys v0.5.0 // indirect
- golang.org/x/text v0.7.0 // indirect
- golang.org/x/tools v0.6.0 // indirect
- golang.zx2c4.com/wireguard v0.0.0-20230216153314-c7b76d3d9ecd // indirect
- google.golang.org/protobuf v1.28.1 // indirect
- gopkg.in/yaml.v2 v2.4.0 // indirect
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+ github.com/ugorji/go/codec v1.2.11 // indirect
+ github.com/vishvananda/netns v0.0.4 // indirect
+ github.com/yeqown/reedsolomon v1.0.0 // indirect
+ golang.org/x/arch v0.3.0 // indirect
+ golang.org/x/net v0.12.0 // indirect
+ golang.org/x/sync v0.3.0 // indirect
+ golang.org/x/text v0.11.0 // indirect
+ golang.org/x/tools v0.9.3 // indirect
+ golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b // indirect
+ google.golang.org/appengine v1.6.7 // indirect
+ google.golang.org/protobuf v1.31.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+ modernc.org/libc v1.22.5 // indirect
+ modernc.org/mathutil v1.5.0 // indirect
+ modernc.org/memory v1.5.0 // indirect
+ modernc.org/sqlite v1.23.1 // indirect
+ sigs.k8s.io/yaml v1.3.0 // indirect
)
diff --git a/go.sum b/go.sum
index d1f4a12..5490328 100644
--- a/go.sum
+++ b/go.sum
@@ -1,18 +1,24 @@
-git.prolicht.digital/golib/healthcheck v1.1.1 h1:bdx0MuGqAq0PCooPpiuPXsr4/Ok+yfJwq8P9ITq2eLI=
-git.prolicht.digital/golib/healthcheck v1.1.1/go.mod h1:wEqVrqHJ8NsSx5qlFGUlw74wJ/wDSKaA34QoyvsEkdc=
-github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
-github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
-github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
-github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
-github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
+github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA=
+github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
-github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
+github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
+github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
+github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
+github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
+github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
+github.com/coreos/go-oidc/v3 v3.6.0 h1:AKVxfYw1Gmkn/w96z0DbT/B/xFnzTd3MkZvWLjF4n/o=
+github.com/coreos/go-oidc/v3 v3.6.0/go.mod h1:ZpHUsHBucTUj6WOkrP4E20UPynbLZzhTQ1XKCXkxyPc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -20,14 +26,15 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
github.com/dchest/uniuri v1.2.0 h1:koIcOUdrTIivZgSLhHQvKgqdWZq5d7KdMEWF1Ud6+5g=
github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY=
-github.com/docker/libcontainer v2.2.1+incompatible h1:++SbbkCw+X8vAd4j2gOCzZ2Nn7s2xFALTf7LZKmM1/0=
-github.com/docker/libcontainer v2.2.1+incompatible/go.mod h1:osvj61pYsqhNCMLGX31xr7klUBhHb/ZBuXS0o1Fvwbw=
-github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U=
-github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
+github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
+github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
+github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
-github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
-github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
-github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
+github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g=
+github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs=
github.com/gin-contrib/sessions v0.0.0-20190101140330-dc5246754963/go.mod h1:4lkInX8nHSR62NSmhXM3xtPeMSyfiR58NaEz+om1lHM=
github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE=
github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY=
@@ -36,28 +43,33 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
-github.com/gin-gonic/gin v1.8.2 h1:UzKToD9/PoFj/V4rvlKqTRKnQYyz8Sc1MJlv4JHPtvY=
-github.com/gin-gonic/gin v1.8.2/go.mod h1:qw5AYuDrzRTnhvusDsrov+fDIxp9Dleuu12h8nfB398=
+github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
+github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
+github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
+github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
+github.com/glebarez/sqlite v1.9.0 h1:Aj6bPA12ZEx5GbSF6XADmCkYXlljPNUY+Zf1EQxynXs=
+github.com/glebarez/sqlite v1.9.0/go.mod h1:YBYCoyupOao60lzp1MVBLEjZfgkq0tdB1voAQ09K9zw=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A=
github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
-github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs=
-github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI=
+github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo=
+github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
+github.com/go-ldap/ldap/v3 v3.4.5 h1:ekEKmaDrpvR2yf5Nc/DClsGG9lAmdDixe44mLzlW5r8=
+github.com/go-ldap/ldap/v3 v3.4.5/go.mod h1:bMGIq3AGbytbaMwf8wdv5Phdxz0FWHTIYMSzyrYgnQs=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
-github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
-github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
-github.com/go-openapi/spec v0.20.8 h1:ubHmXNY3FCIOinT8RNrrPfGc9t7I1qhPtdOGoG2AxRU=
-github.com/go-openapi/spec v0.20.8/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
+github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8=
+github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
-github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
+github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
+github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
@@ -67,23 +79,36 @@ github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
-github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
-github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
-github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
+github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k=
+github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
+github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
+github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
-github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
-github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
+github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
+github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
+github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
+github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
@@ -92,9 +117,22 @@ github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
+github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgx/v5 v5.4.0 h1:BSr+GCm4N6QcgIwv0DyTFHK9ugfEFF9DzSbbzxOiXU0=
+github.com/jackc/pgx/v5 v5.4.0/go.mod h1:q6iHT8uDNXWiFNOlRqJzBTaSH3+2xCXkokxHZC5qWFY=
+github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
+github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
+github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
+github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
+github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
+github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
-github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
@@ -104,10 +142,10 @@ github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2C
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
-github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
-github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
-github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
+github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
@@ -116,8 +154,10 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
+github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
+github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
@@ -125,169 +165,179 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
-github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
-github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
-github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
-github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
-github.com/mdlayher/genetlink v1.3.1 h1:roBiPnual+eqtRkKX2Jb8UQN5ZPWnhDCGj/wR6Jlz2w=
-github.com/mdlayher/genetlink v1.3.1/go.mod h1:uaIPxkWmGk753VVIzDtROxQ8+T+dkHqOI0vB1NA9S/Q=
-github.com/mdlayher/netlink v1.7.1 h1:FdUaT/e33HjEXagwELR8R3/KL1Fq5x3G5jgHLp/BTmg=
-github.com/mdlayher/netlink v1.7.1/go.mod h1:nKO5CSjE/DJjVhk/TNp6vCE1ktVxEA8VEh8drhZzxsQ=
-github.com/mdlayher/socket v0.4.0 h1:280wsy40IC9M9q1uPGcLBwXpcTQDtoGwVt+BNoITxIw=
-github.com/mdlayher/socket v0.4.0/go.mod h1:xxFqz5GRCUN3UEOm9CZqEJsAbe1C8OwSK46NlmWuVoc=
+github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
+github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
+github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
+github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
+github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
+github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
+github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
+github.com/microsoft/go-mssqldb v1.1.0 h1:jsV+tpvcPTbNNKW0o3kiCD69kOHICsfjZ2VcVu2lKYc=
+github.com/microsoft/go-mssqldb v1.1.0/go.mod h1:LzkFdl4z2Ck+Hi+ycGOTbL56VEfgoyA2DvYejrNGbRk=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
-github.com/milosgajdos/tenus v0.0.3 h1:jmaJzwaY1DUyYVD0lM4U+uvP2kkEg1VahDqRFxIkVBE=
-github.com/milosgajdos/tenus v0.0.3/go.mod h1:eIjx29vNeDOYWJuCnaHY2r4fq5egetV26ry3on7p8qY=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
+github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
-github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U=
-github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
-github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
-github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
-github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
-github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
-github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
+github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
+github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
+github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
-github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
-github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/prometheus-community/pro-bing v0.3.0 h1:SFT6gHqXwbItEDJhTkzPWVqU6CLEtqEfNAPp47RUON4=
+github.com/prometheus-community/pro-bing v0.3.0/go.mod h1:p9dLb9zdmv+eLxWfCT6jESWuDrS+YzpPkQBgysQF8a0=
github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b h1:aUNXCGgukb4gtY99imuIeoh8Vr0GSwAlYxPAhqZrpFc=
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
+github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
-github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
-github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
-github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
-github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
-github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
-github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
-github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
-github.com/swaggo/files v1.0.0 h1:1gGXVIeUFCS/dta17rnP0iOpr6CXFwKD7EO5ID233e4=
-github.com/swaggo/files v1.0.0/go.mod h1:N59U6URJLyU1PQgFqPM7wXLMhJx7QAolnvfQkqO13kc=
-github.com/swaggo/gin-swagger v1.5.3 h1:8mWmHLolIbrhJJTflsaFoZzRBYVmEE7JZGIq08EiC0Q=
-github.com/swaggo/gin-swagger v1.5.3/go.mod h1:3XJKSfHjDMB5dBo/0rrTXidPmgLeqsX89Yp4uA50HpI=
-github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ=
-github.com/swaggo/swag v1.8.10 h1:eExW4bFa52WOjqRzRD58bgWsWfdFJso50lpbeTcmTfo=
-github.com/swaggo/swag v1.8.10/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk=
-github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e h1:nt2877sKfojlHCTOBXbpWjBkuWKritFaGIfgQwbQUls=
-github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e/go.mod h1:B4+Kq1u5FlULTjFSM707Q6e/cOHFv0z/6QRoxubDIQ8=
+github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/swaggo/swag v1.16.1 h1:fTNRhKstPKxcnoKsytm4sahr8FaYzUcT7i1/3nd/fBg=
+github.com/swaggo/swag v1.16.1/go.mod h1:9/LMvHycG3NFHfR6LwvikHv5iFvmPADQ359cKikGxto=
github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f h1:oqdnd6OGlOUu1InG37hWcCB3a+Jy3fwjylyVboaNMwY=
github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f/go.mod h1:X3Dd1SB8Gt1V968NTzpKFjMM6O8ccta2NPC6MprOxZQ=
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 h1:PM5hJF7HVfNWmCjMdEfbuOBNXSVF2cMFGgQTPdKCbwM=
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
+github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
-github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU=
-github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
-github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
+github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
+github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/utrack/gin-csrf v0.0.0-20190424104817-40fb8d2c8fca h1:lpvAjPK+PcxnbcB8H7axIb4fMNwjX9bE4DzwPjGg8aE=
github.com/utrack/gin-csrf v0.0.0-20190424104817-40fb8d2c8fca/go.mod h1:XXKxNbpoLihvvT7orUZbs/iZayg1n4ip7iJakJPAwA8=
-github.com/xhit/go-simple-mail/v2 v2.13.0 h1:OANWU9jHZrVfBkNkvLf8Ww0fexwpQVF/v/5f96fFTLI=
-github.com/xhit/go-simple-mail/v2 v2.13.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
-github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/vardius/message-bus v1.1.5 h1:YSAC2WB4HRlwc4neFPTmT88kzzoiQ+9WRRbej/E/LZc=
+github.com/vardius/message-bus v1.1.5/go.mod h1:6xladCV2lMkUAE4bzzS85qKOiB5miV7aBVRafiTJGqw=
+github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0=
+github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
+github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
+github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
+github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
+github.com/xhit/go-simple-mail/v2 v2.15.0 h1:qMXeqcZErUW/Dw6EXxmPuxHzVI8MdxWnEnu2xcisohU=
+github.com/xhit/go-simple-mail/v2 v2.15.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
+github.com/yeqown/go-qrcode/v2 v2.2.2 h1:0comk6jEwi0oWNhKEmzx4JI+Q7XIneAApmFSMKWmSVc=
+github.com/yeqown/go-qrcode/v2 v2.2.2/go.mod h1:2Qsk2APUCPne0TsRo40DIkI5MYnbzYKCnKGEFWrxd24=
+github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0=
+github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
+golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
-golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
+golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
+golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
+golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
-golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
-golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
+golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
+golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
+golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
+golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
+golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
+golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM=
+golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.zx2c4.com/wireguard v0.0.0-20230216153314-c7b76d3d9ecd h1:thMXEWXMWIiGlp5T/V+CoetkzBJi4INNaglxdvyfK0c=
-golang.zx2c4.com/wireguard v0.0.0-20230216153314-c7b76d3d9ecd/go.mod h1:whfbyDBt09xhCYQWtO2+3UVjlaq6/9hDZrjg2ZE6SyA=
-golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230215201556-9c5414ab4bde h1:ybF7AMzIUikL9x4LgwEmzhXtzRpKNqngme1VGDWz+Nk=
-golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230215201556-9c5414ab4bde/go.mod h1:mQqgjkW8GQQcJQsbBvK890TKqUK1DfKWkuBGbOkuMHQ=
+golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b h1:J1CaxgLerRR5lgx3wnr6L04cJFbWoceSK9JWBdglINo=
+golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b/go.mod h1:tqur9LnfstdR9ep2LaJT4lFUl0EjlHtge+gAjmsHUG4=
+golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE=
+golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
+google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
-google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -297,8 +347,11 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
+gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU=
+gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -306,11 +359,23 @@ gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gorm.io/driver/mysql v1.4.7 h1:rY46lkCspzGHn7+IYsNpSfEv9tA+SU4SkkB+GFX125Y=
-gorm.io/driver/mysql v1.4.7/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8oc=
-gorm.io/driver/sqlite v1.4.4 h1:gIufGoR0dQzjkyqDyYSCvsYR6fba1Gw5YKDqKeChxFc=
-gorm.io/driver/sqlite v1.4.4/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
-gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
-gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
-gorm.io/gorm v1.24.5 h1:g6OPREKqqlWq4kh/3MCQbZKImeB9e6Xgc4zD+JgNZGE=
-gorm.io/gorm v1.24.5/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
+gorm.io/driver/mysql v1.5.1 h1:WUEH5VF9obL/lTtzjmML/5e6VfFR/788coz2uaVCAZw=
+gorm.io/driver/mysql v1.5.1/go.mod h1:Jo3Xu7mMhCyj8dlrb3WoCaRd1FhsVh+yMXb1jUInf5o=
+gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0=
+gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8=
+gorm.io/driver/sqlserver v1.5.1 h1:wpyW/pR26U94uaujltiFGXY7fd2Jw5hC9PB1ZF/Y5s4=
+gorm.io/driver/sqlserver v1.5.1/go.mod h1:AYHzzte2msKTmYBYsSIq8ZUsznLJwBdkB2wpI+kt0nM=
+gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
+gorm.io/gorm v1.25.2 h1:gs1o6Vsa+oVKG/a9ElL3XgyGfghFfkKA2SInQaCyMho=
+gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
+modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
+modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
+modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
+modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
+modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
+modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
+modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
+modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
+sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
diff --git a/internal/adapters/database.go b/internal/adapters/database.go
new file mode 100644
index 0000000..8ddc31f
--- /dev/null
+++ b/internal/adapters/database.go
@@ -0,0 +1,887 @@
+package adapters
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "github.com/sirupsen/logrus"
+ "gorm.io/gorm/clause"
+ "gorm.io/gorm/logger"
+ "gorm.io/gorm/utils"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/glebarez/sqlite"
+ "github.com/h44z/wg-portal/internal/config"
+ "github.com/h44z/wg-portal/internal/domain"
+ gormMySQL "gorm.io/driver/mysql"
+ "gorm.io/driver/postgres"
+ "gorm.io/driver/sqlserver"
+ "gorm.io/gorm"
+)
+
+// SchemaVersion describes the current database schema version. It must be incremented if a manual migration is needed.
+var SchemaVersion uint64 = 1
+
+// SysStat stores the current database schema version and the timestamp when it was applied.
+type SysStat struct {
+ MigratedAt time.Time `gorm:"column:migrated_at"`
+ SchemaVersion uint64 `gorm:"primaryKey,column:schema_version"`
+}
+
+// GormLogger is a custom logger for Gorm, making it use logrus.
+type GormLogger struct {
+ SlowThreshold time.Duration
+ SourceField string
+ IgnoreErrRecordNotFound bool
+ Debug bool
+ Silent bool
+}
+
+func NewLogger(slowThreshold time.Duration, debug bool) *GormLogger {
+ return &GormLogger{
+ SlowThreshold: slowThreshold,
+ Debug: debug,
+ IgnoreErrRecordNotFound: true,
+ Silent: false,
+ SourceField: "src",
+ }
+}
+
+func (l *GormLogger) LogMode(level logger.LogLevel) logger.Interface {
+ if level == logger.Silent {
+ l.Silent = true
+ } else {
+ l.Silent = false
+ }
+ return l
+}
+
+func (l *GormLogger) Info(ctx context.Context, s string, args ...interface{}) {
+ if l.Silent {
+ return
+ }
+ logrus.WithContext(ctx).Infof(s, args...)
+}
+
+func (l *GormLogger) Warn(ctx context.Context, s string, args ...interface{}) {
+ if l.Silent {
+ return
+ }
+ logrus.WithContext(ctx).Warnf(s, args...)
+}
+
+func (l *GormLogger) Error(ctx context.Context, s string, args ...interface{}) {
+ if l.Silent {
+ return
+ }
+ logrus.WithContext(ctx).Errorf(s, args...)
+}
+
+func (l *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
+ if l.Silent {
+ return
+ }
+
+ elapsed := time.Since(begin)
+ sql, rows := fc()
+ fields := logrus.Fields{
+ "rows": rows,
+ "duration": elapsed,
+ }
+ if l.SourceField != "" {
+ fields[l.SourceField] = utils.FileWithLineNum()
+ }
+ if err != nil && !(errors.Is(err, gorm.ErrRecordNotFound) && l.IgnoreErrRecordNotFound) {
+ fields[logrus.ErrorKey] = err
+ logrus.WithContext(ctx).WithFields(fields).Errorf("%s", sql)
+ return
+ }
+
+ if l.SlowThreshold != 0 && elapsed > l.SlowThreshold {
+ logrus.WithContext(ctx).WithFields(fields).Warnf("%s", sql)
+ return
+ }
+
+ if l.Debug {
+ logrus.WithContext(ctx).WithFields(fields).Tracef("%s", sql)
+ }
+}
+
+func NewDatabase(cfg config.DatabaseConfig) (*gorm.DB, error) {
+ var gormDb *gorm.DB
+ var err error
+
+ switch cfg.Type {
+ case config.DatabaseMySQL:
+ gormDb, err = gorm.Open(gormMySQL.Open(cfg.DSN), &gorm.Config{
+ Logger: NewLogger(cfg.SlowQueryThreshold, cfg.Debug),
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to open MySQL database: %w", err)
+ }
+
+ sqlDB, _ := gormDb.DB()
+ sqlDB.SetConnMaxLifetime(time.Minute * 5)
+ sqlDB.SetMaxIdleConns(2)
+ sqlDB.SetMaxOpenConns(10)
+ err = sqlDB.Ping() // This DOES open a connection if necessary. This makes sure the database is accessible
+ if err != nil {
+ return nil, fmt.Errorf("failed to ping MySQL database: %w", err)
+ }
+ case config.DatabaseMsSQL:
+ gormDb, err = gorm.Open(sqlserver.Open(cfg.DSN), &gorm.Config{
+ Logger: NewLogger(cfg.SlowQueryThreshold, cfg.Debug),
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to open sqlserver database: %w", err)
+ }
+ case config.DatabasePostgres:
+ gormDb, err = gorm.Open(postgres.Open(cfg.DSN), &gorm.Config{
+ Logger: NewLogger(cfg.SlowQueryThreshold, cfg.Debug),
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to open Postgres database: %w", err)
+ }
+ case config.DatabaseSQLite:
+ if _, err = os.Stat(filepath.Dir(cfg.DSN)); os.IsNotExist(err) {
+ if err = os.MkdirAll(filepath.Dir(cfg.DSN), 0700); err != nil {
+ return nil, fmt.Errorf("failed to create database base directory: %w", err)
+ }
+ }
+ gormDb, err = gorm.Open(sqlite.Open(cfg.DSN), &gorm.Config{
+ Logger: NewLogger(cfg.SlowQueryThreshold, cfg.Debug),
+ DisableForeignKeyConstraintWhenMigrating: true,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to open sqlite database: %w", err)
+ }
+ sqlDB, _ := gormDb.DB()
+ sqlDB.SetMaxOpenConns(1)
+ }
+
+ return gormDb, nil
+}
+
+// SqlRepo is a SQL database repository implementation.
+// Currently, it supports MySQL, SQLite, Microsoft SQL and Postgresql database systems.
+type SqlRepo struct {
+ db *gorm.DB
+}
+
+func NewSqlRepository(db *gorm.DB) (*SqlRepo, error) {
+ repo := &SqlRepo{
+ db: db,
+ }
+
+ if err := repo.preCheck(); err != nil {
+ return nil, fmt.Errorf("failed to initialize database: %w", err)
+ }
+
+ if err := repo.migrate(); err != nil {
+ return nil, fmt.Errorf("failed to initialize database: %w", err)
+ }
+
+ return repo, nil
+}
+
+func (r *SqlRepo) preCheck() error {
+ // WireGuard Portal v1 database migration table
+ type DatabaseMigrationInfo struct {
+ Version string `gorm:"primaryKey"`
+ Applied time.Time
+ }
+
+ // temporarily disable logger as the next request might fail (intentionally)
+ r.db.Logger.LogMode(logger.Silent)
+ defer func() { r.db.Logger.LogMode(logger.Info) }()
+
+ lastVersion := DatabaseMigrationInfo{}
+ err := r.db.Order("applied desc, version desc").FirstOrInit(&lastVersion).Error
+ if err != nil {
+ return nil // we probably don't have a V1 database =)
+ }
+
+ return fmt.Errorf("detected a WireGuard Portal V1 database (version: %s) - please migrate first", lastVersion.Version)
+}
+
+func (r *SqlRepo) migrate() error {
+ logrus.Tracef("sysstat migration: %v", r.db.AutoMigrate(&SysStat{}))
+ logrus.Tracef("user migration: %v", r.db.AutoMigrate(&domain.User{}))
+ logrus.Tracef("interface migration: %v", r.db.AutoMigrate(&domain.Interface{}))
+ logrus.Tracef("peer migration: %v", r.db.AutoMigrate(&domain.Peer{}))
+ logrus.Tracef("peer status migration: %v", r.db.AutoMigrate(&domain.PeerStatus{}))
+ logrus.Tracef("interface status migration: %v", r.db.AutoMigrate(&domain.InterfaceStatus{}))
+ logrus.Tracef("audit data migration: %v", r.db.AutoMigrate(&domain.AuditEntry{}))
+
+ existingSysStat := SysStat{}
+ r.db.Where("schema_version = ?", SchemaVersion).First(&existingSysStat)
+ if existingSysStat.SchemaVersion == 0 {
+ sysStat := SysStat{
+ MigratedAt: time.Now(),
+ SchemaVersion: SchemaVersion,
+ }
+ if err := r.db.Create(&sysStat).Error; err != nil {
+ return fmt.Errorf("failed to write sysstat entry for schema version %d: %w", SchemaVersion, err)
+ }
+ logrus.Debugf("sysstat entry for schema version %d written", SchemaVersion)
+ }
+
+ return nil
+}
+
+// region interfaces
+
+func (r *SqlRepo) GetInterface(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, error) {
+ var in domain.Interface
+
+ err := r.db.WithContext(ctx).Preload("Addresses").First(&in, id).Error
+
+ if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
+ return nil, domain.ErrNotFound
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ return &in, nil
+}
+
+func (r *SqlRepo) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error) {
+ in, err := r.GetInterface(ctx, id)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to load interface: %w", err)
+ }
+
+ peers, err := r.GetInterfacePeers(ctx, id)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to load peers: %w", err)
+ }
+
+ return in, peers, nil
+}
+
+func (r *SqlRepo) GetPeersStats(ctx context.Context, ids ...domain.PeerIdentifier) ([]domain.PeerStatus, error) {
+ if len(ids) == 0 {
+ return nil, nil
+ }
+
+ var stats []domain.PeerStatus
+
+ err := r.db.WithContext(ctx).Where("identifier IN ?", ids).Find(&stats).Error
+ if err != nil {
+ return nil, err
+ }
+
+ return stats, nil
+}
+
+func (r *SqlRepo) GetAllInterfaces(ctx context.Context) ([]domain.Interface, error) {
+ var interfaces []domain.Interface
+
+ err := r.db.WithContext(ctx).Preload("Addresses").Find(&interfaces).Error
+ if err != nil {
+ return nil, err
+ }
+
+ return interfaces, nil
+}
+
+func (r *SqlRepo) FindInterfaces(ctx context.Context, search string) ([]domain.Interface, error) {
+ var users []domain.Interface
+
+ searchValue := "%" + strings.ToLower(search) + "%"
+ err := r.db.WithContext(ctx).
+ Where("identifier LIKE ?", searchValue).
+ Or("display_name LIKE ?", searchValue).
+ Preload("Addresses").
+ Find(&users).Error
+ if err != nil {
+ return nil, err
+ }
+
+ return users, nil
+}
+
+func (r *SqlRepo) SaveInterface(ctx context.Context, id domain.InterfaceIdentifier, updateFunc func(in *domain.Interface) (*domain.Interface, error)) error {
+ userInfo := domain.GetUserInfo(ctx)
+ err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ in, err := r.getOrCreateInterface(userInfo, tx, id)
+ if err != nil {
+ return err // return any error will roll back
+ }
+
+ in, err = updateFunc(in)
+ if err != nil {
+ return err
+ }
+
+ err = r.upsertInterface(userInfo, tx, in)
+ if err != nil {
+ return err
+ }
+
+ // return nil will commit the whole transaction
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (r *SqlRepo) getOrCreateInterface(ui *domain.ContextUserInfo, tx *gorm.DB, id domain.InterfaceIdentifier) (*domain.Interface, error) {
+ var in domain.Interface
+
+ // interfaceDefaults will be applied to newly created interface records
+ interfaceDefaults := domain.Interface{
+ BaseModel: domain.BaseModel{
+ CreatedBy: ui.UserId(),
+ UpdatedBy: ui.UserId(),
+ CreatedAt: time.Now(),
+ UpdatedAt: time.Now(),
+ },
+ Identifier: id,
+ }
+
+ err := tx.Attrs(interfaceDefaults).FirstOrCreate(&in, id).Error
+ if err != nil {
+ return nil, err
+ }
+
+ return &in, nil
+}
+
+func (r *SqlRepo) upsertInterface(ui *domain.ContextUserInfo, tx *gorm.DB, in *domain.Interface) error {
+ in.UpdatedBy = ui.UserId()
+ in.UpdatedAt = time.Now()
+
+ err := tx.Save(in).Error
+ if err != nil {
+ return err
+ }
+
+ err = tx.Model(in).Association("Addresses").Replace(in.Addresses)
+ if err != nil {
+ return fmt.Errorf("failed to update interface addresses: %w", err)
+ }
+
+ return nil
+}
+
+func (r *SqlRepo) DeleteInterface(ctx context.Context, id domain.InterfaceIdentifier) error {
+ err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ err := tx.Where("interface_identifier = ?", id).Delete(&domain.Peer{}).Error
+ if err != nil {
+ return err
+ }
+
+ err = tx.Delete(&domain.InterfaceStatus{InterfaceId: id}).Error
+ if err != nil {
+ return err
+ }
+
+ err = tx.Select(clause.Associations).Delete(&domain.Interface{Identifier: id}).Error
+ if err != nil {
+ return err
+ }
+
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (r *SqlRepo) GetInterfaceIps(ctx context.Context) (map[domain.InterfaceIdentifier][]domain.Cidr, error) {
+ var ips []struct {
+ domain.Cidr
+ InterfaceId domain.InterfaceIdentifier `gorm:"column:interface_identifier"`
+ }
+
+ err := r.db.WithContext(ctx).
+ Table("interface_addresses").
+ Joins("LEFT JOIN cidrs ON interface_addresses.cidr_cidr = cidrs.cidr").
+ Scan(&ips).Error
+ if err != nil {
+ return nil, err
+ }
+
+ result := make(map[domain.InterfaceIdentifier][]domain.Cidr)
+ for _, ip := range ips {
+ result[ip.InterfaceId] = append(result[ip.InterfaceId], ip.Cidr)
+ }
+ return result, nil
+}
+
+// endregion interfaces
+
+// region peers
+
+func (r *SqlRepo) GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error) {
+ var peer domain.Peer
+
+ err := r.db.WithContext(ctx).Preload("Addresses").First(&peer, id).Error
+
+ if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
+ return nil, domain.ErrNotFound
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ return &peer, nil
+}
+
+func (r *SqlRepo) GetInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.Peer, error) {
+ var peers []domain.Peer
+
+ err := r.db.WithContext(ctx).Preload("Addresses").Where("interface_identifier = ?", id).Find(&peers).Error
+ if err != nil {
+ return nil, err
+ }
+
+ return peers, nil
+}
+
+func (r *SqlRepo) FindInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier, search string) ([]domain.Peer, error) {
+ var peers []domain.Peer
+
+ searchValue := "%" + strings.ToLower(search) + "%"
+ err := r.db.WithContext(ctx).Where("interface_identifier = ?", id).
+ Where("identifier LIKE ?", searchValue).
+ Or("display_name LIKE ?", searchValue).
+ Or("iface_address_str_v LIKE ?", searchValue).
+ Find(&peers).Error
+ if err != nil {
+ return nil, err
+ }
+
+ return peers, nil
+}
+
+func (r *SqlRepo) GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error) {
+ var peers []domain.Peer
+
+ err := r.db.WithContext(ctx).Preload("Addresses").Where("user_identifier = ?", id).Find(&peers).Error
+ if err != nil {
+ return nil, err
+ }
+
+ return peers, nil
+}
+
+func (r *SqlRepo) FindUserPeers(ctx context.Context, id domain.UserIdentifier, search string) ([]domain.Peer, error) {
+ var peers []domain.Peer
+
+ searchValue := "%" + strings.ToLower(search) + "%"
+ err := r.db.WithContext(ctx).Where("user_identifier = ?", id).
+ Where("identifier LIKE ?", searchValue).
+ Or("display_name LIKE ?", searchValue).
+ Or("iface_address_str_v LIKE ?", searchValue).
+ Find(&peers).Error
+ if err != nil {
+ return nil, err
+ }
+
+ return peers, nil
+}
+
+func (r *SqlRepo) SavePeer(ctx context.Context, id domain.PeerIdentifier, updateFunc func(in *domain.Peer) (*domain.Peer, error)) error {
+ userInfo := domain.GetUserInfo(ctx)
+ err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ peer, err := r.getOrCreatePeer(userInfo, tx, id)
+ if err != nil {
+ return err // return any error will roll back
+ }
+
+ peer, err = updateFunc(peer)
+ if err != nil {
+ return err
+ }
+
+ err = r.upsertPeer(userInfo, tx, peer)
+ if err != nil {
+ return err
+ }
+
+ // return nil will commit the whole transaction
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (r *SqlRepo) getOrCreatePeer(ui *domain.ContextUserInfo, tx *gorm.DB, id domain.PeerIdentifier) (*domain.Peer, error) {
+ var peer domain.Peer
+
+ // interfaceDefaults will be applied to newly created interface records
+ interfaceDefaults := domain.Peer{
+ BaseModel: domain.BaseModel{
+ CreatedBy: ui.UserId(),
+ UpdatedBy: ui.UserId(),
+ CreatedAt: time.Now(),
+ UpdatedAt: time.Now(),
+ },
+ Identifier: id,
+ }
+
+ err := tx.Attrs(interfaceDefaults).FirstOrCreate(&peer, id).Error
+ if err != nil {
+ return nil, err
+ }
+
+ return &peer, nil
+}
+
+func (r *SqlRepo) upsertPeer(ui *domain.ContextUserInfo, tx *gorm.DB, peer *domain.Peer) error {
+ peer.UpdatedBy = ui.UserId()
+ peer.UpdatedAt = time.Now()
+
+ err := tx.Save(peer).Error
+ if err != nil {
+ return err
+ }
+
+ err = tx.Model(peer).Association("Addresses").Replace(peer.Interface.Addresses)
+ if err != nil {
+ return fmt.Errorf("failed to update peer addresses: %w", err)
+ }
+
+ return nil
+}
+
+func (r *SqlRepo) DeletePeer(ctx context.Context, id domain.PeerIdentifier) error {
+ err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ err := tx.Delete(&domain.PeerStatus{PeerId: id}).Error
+ if err != nil {
+ return err
+ }
+
+ err = tx.Select(clause.Associations).Delete(&domain.Peer{Identifier: id}).Error
+ if err != nil {
+ return err
+ }
+
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (r *SqlRepo) GetPeerIps(ctx context.Context) (map[domain.PeerIdentifier][]domain.Cidr, error) {
+ var ips []struct {
+ domain.Cidr
+ PeerId domain.PeerIdentifier `gorm:"column:peer_identifier"`
+ }
+
+ err := r.db.WithContext(ctx).
+ Table("peer_addresses").
+ Joins("LEFT JOIN cidrs ON peer_addresses.cidr_cidr = cidrs.cidr").
+ Scan(&ips).Error
+ if err != nil {
+ return nil, err
+ }
+
+ result := make(map[domain.PeerIdentifier][]domain.Cidr)
+ for _, ip := range ips {
+ result[ip.PeerId] = append(result[ip.PeerId], ip.Cidr)
+ }
+ return result, nil
+}
+
+func (r *SqlRepo) GetUsedIpsPerSubnet(ctx context.Context) (map[domain.Cidr][]domain.Cidr, error) {
+ var peerIps []struct {
+ domain.Cidr
+ PeerId domain.PeerIdentifier `gorm:"column:peer_identifier"`
+ }
+
+ err := r.db.WithContext(ctx).
+ Table("peer_addresses").
+ Joins("LEFT JOIN cidrs ON peer_addresses.cidr_cidr = cidrs.cidr").
+ Scan(&peerIps).Error
+ if err != nil {
+ return nil, fmt.Errorf("failed to fetch peer IP's: %w", err)
+ }
+
+ var interfaceIps []struct {
+ domain.Cidr
+ InterfaceId domain.InterfaceIdentifier `gorm:"column:interface_identifier"`
+ }
+
+ err = r.db.WithContext(ctx).
+ Table("interface_addresses").
+ Joins("LEFT JOIN cidrs ON interface_addresses.cidr_cidr = cidrs.cidr").
+ Scan(&interfaceIps).Error
+ if err != nil {
+ return nil, fmt.Errorf("failed to fetch interface IP's: %w", err)
+ }
+
+ result := make(map[domain.Cidr][]domain.Cidr)
+ for _, ip := range interfaceIps {
+ networkAddr := ip.Cidr.NetworkAddr()
+ result[networkAddr] = append(result[networkAddr], ip.Cidr)
+ }
+ for _, ip := range peerIps {
+ networkAddr := ip.Cidr.NetworkAddr()
+ result[networkAddr] = append(result[networkAddr], ip.Cidr)
+ }
+ return result, nil
+}
+
+// endregion peers
+
+// region users
+
+func (r *SqlRepo) GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) {
+ var user domain.User
+
+ err := r.db.WithContext(ctx).First(&user, id).Error
+
+ if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
+ return nil, domain.ErrNotFound
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ return &user, nil
+}
+
+func (r *SqlRepo) GetAllUsers(ctx context.Context) ([]domain.User, error) {
+ var users []domain.User
+
+ err := r.db.WithContext(ctx).Find(&users).Error
+ if err != nil {
+ return nil, err
+ }
+
+ return users, nil
+}
+
+func (r *SqlRepo) FindUsers(ctx context.Context, search string) ([]domain.User, error) {
+ var users []domain.User
+
+ searchValue := "%" + strings.ToLower(search) + "%"
+ err := r.db.WithContext(ctx).
+ Where("identifier LIKE ?", searchValue).
+ Or("firstname LIKE ?", searchValue).
+ Or("lastname LIKE ?", searchValue).
+ Or("email LIKE ?", searchValue).
+ Find(&users).Error
+ if err != nil {
+ return nil, err
+ }
+
+ return users, nil
+}
+
+func (r *SqlRepo) SaveUser(ctx context.Context, id domain.UserIdentifier, updateFunc func(u *domain.User) (*domain.User, error)) error {
+ userInfo := domain.GetUserInfo(ctx)
+
+ err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ user, err := r.getOrCreateUser(userInfo, tx, id)
+ if err != nil {
+ return err // return any error will roll back
+ }
+
+ user, err = updateFunc(user)
+ if err != nil {
+ return err
+ }
+
+ err = r.upsertUser(userInfo, tx, user)
+ if err != nil {
+ return err
+ }
+
+ // return nil will commit the whole transaction
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (r *SqlRepo) DeleteUser(ctx context.Context, id domain.UserIdentifier) error {
+ err := r.db.WithContext(ctx).Delete(&domain.User{}, id).Error
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (r *SqlRepo) getOrCreateUser(ui *domain.ContextUserInfo, tx *gorm.DB, id domain.UserIdentifier) (*domain.User, error) {
+ var user domain.User
+
+ // userDefaults will be applied to newly created user records
+ userDefaults := domain.User{
+ BaseModel: domain.BaseModel{
+ CreatedBy: ui.UserId(),
+ UpdatedBy: ui.UserId(),
+ CreatedAt: time.Now(),
+ UpdatedAt: time.Now(),
+ },
+ Identifier: id,
+ Source: domain.UserSourceDatabase,
+ IsAdmin: false,
+ }
+
+ err := tx.Attrs(userDefaults).FirstOrCreate(&user, id).Error
+ if err != nil {
+ return nil, err
+ }
+
+ return &user, nil
+}
+
+func (r *SqlRepo) upsertUser(ui *domain.ContextUserInfo, tx *gorm.DB, user *domain.User) error {
+ user.UpdatedBy = ui.UserId()
+ user.UpdatedAt = time.Now()
+
+ err := tx.Save(user).Error
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// endregion users
+
+// region statistics
+
+func (r *SqlRepo) UpdateInterfaceStatus(ctx context.Context, id domain.InterfaceIdentifier, updateFunc func(in *domain.InterfaceStatus) (*domain.InterfaceStatus, error)) error {
+ err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ in, err := r.getOrCreateInterfaceStatus(tx, id)
+ if err != nil {
+ return err // return any error will roll back
+ }
+
+ in, err = updateFunc(in)
+ if err != nil {
+ return err
+ }
+
+ err = r.upsertInterfaceStatus(tx, in)
+ if err != nil {
+ return err
+ }
+
+ // return nil will commit the whole transaction
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (r *SqlRepo) getOrCreateInterfaceStatus(tx *gorm.DB, id domain.InterfaceIdentifier) (*domain.InterfaceStatus, error) {
+ var in domain.InterfaceStatus
+
+ // defaults will be applied to newly created record
+ defaults := domain.InterfaceStatus{
+ InterfaceId: id,
+ UpdatedAt: time.Now(),
+ }
+
+ err := tx.Attrs(defaults).FirstOrCreate(&in, id).Error
+ if err != nil {
+ return nil, err
+ }
+
+ return &in, nil
+}
+
+func (r *SqlRepo) upsertInterfaceStatus(tx *gorm.DB, in *domain.InterfaceStatus) error {
+ err := tx.Save(in).Error
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (r *SqlRepo) UpdatePeerStatus(ctx context.Context, id domain.PeerIdentifier, updateFunc func(in *domain.PeerStatus) (*domain.PeerStatus, error)) error {
+ err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ in, err := r.getOrCreatePeerStatus(tx, id)
+ if err != nil {
+ return err // return any error will roll back
+ }
+
+ in, err = updateFunc(in)
+ if err != nil {
+ return err
+ }
+
+ err = r.upsertPeerStatus(tx, in)
+ if err != nil {
+ return err
+ }
+
+ // return nil will commit the whole transaction
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (r *SqlRepo) getOrCreatePeerStatus(tx *gorm.DB, id domain.PeerIdentifier) (*domain.PeerStatus, error) {
+ var in domain.PeerStatus
+
+ // defaults will be applied to newly created record
+ defaults := domain.PeerStatus{
+ PeerId: id,
+ UpdatedAt: time.Now(),
+ }
+
+ err := tx.Attrs(defaults).FirstOrCreate(&in, id).Error
+ if err != nil {
+ return nil, err
+ }
+
+ return &in, nil
+}
+
+func (r *SqlRepo) upsertPeerStatus(tx *gorm.DB, in *domain.PeerStatus) error {
+ err := tx.Save(in).Error
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// endregion statistics
+
+// region audit
+
+func (r *SqlRepo) SaveAuditEntry(ctx context.Context, entry *domain.AuditEntry) error {
+ err := r.db.WithContext(ctx).Save(entry).Error
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// endregion audit
diff --git a/internal/adapters/database_integration_test.go b/internal/adapters/database_integration_test.go
new file mode 100644
index 0000000..0ad8419
--- /dev/null
+++ b/internal/adapters/database_integration_test.go
@@ -0,0 +1,43 @@
+//go:build integration
+
+package adapters
+
+import (
+ "database/sql"
+ "fmt"
+
+ "github.com/glebarez/sqlite"
+ "github.com/stretchr/testify/assert"
+ "gorm.io/gorm"
+
+ "testing"
+)
+
+func tempSqliteDb(t *testing.T) *gorm.DB {
+
+ // github.com/mattn/go-sqlite3
+ db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
+ if err != nil {
+ t.Fatal(err)
+ }
+ return db
+}
+
+func Test_sqlRepo_migrate(t *testing.T) {
+ db := tempSqliteDb(t)
+
+ r := SqlRepo{db: db}
+
+ err := r.migrate()
+ assert.NoError(t, err)
+
+ // check result
+ var sqlStatement []sql.NullString
+ db.Raw("SELECT sql FROM sqlite_master").Find(&sqlStatement)
+ fmt.Println("Table Schemas:")
+ for _, stm := range sqlStatement {
+ if stm.Valid {
+ fmt.Println(stm.String)
+ }
+ }
+}
diff --git a/internal/adapters/filesystem.go b/internal/adapters/filesystem.go
new file mode 100644
index 0000000..22bccc4
--- /dev/null
+++ b/internal/adapters/filesystem.go
@@ -0,0 +1,54 @@
+package adapters
+
+import (
+ "fmt"
+ "github.com/sirupsen/logrus"
+ "io"
+ "os"
+ "path/filepath"
+)
+
+type FilesystemRepo struct {
+ basePath string
+}
+
+func NewFileSystemRepository(basePath string) (*FilesystemRepo, error) {
+ if basePath == "" {
+ return nil, nil // no path, return empty repository
+ }
+
+ r := &FilesystemRepo{basePath: basePath}
+
+ if err := os.MkdirAll(r.basePath, os.ModePerm); err != nil {
+ return nil, fmt.Errorf("failed to create base directory %s: %w", basePath, err)
+ }
+
+ return r, nil
+}
+
+func (r *FilesystemRepo) WriteFile(path string, contents io.Reader) error {
+ filePath := filepath.Join(r.basePath, path)
+ parentDirectory := filepath.Dir(filePath)
+
+ if err := os.MkdirAll(parentDirectory, os.ModePerm); err != nil {
+ return fmt.Errorf("failed to create parent directory %s: %w", parentDirectory, err)
+ }
+
+ file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, os.ModePerm)
+ if err != nil {
+ return fmt.Errorf("failed to open file %s: %w", file.Name(), err)
+ }
+ defer func(file *os.File) {
+ if err := file.Close(); err != nil {
+ logrus.Errorf("failed to close file %s: %v", file.Name(), err)
+ }
+ }(file)
+
+ _, err = io.Copy(file, contents)
+ if err != nil {
+ return fmt.Errorf("failed to write file contents: %w", err)
+ }
+
+ return nil
+
+}
diff --git a/internal/adapters/mailer.go b/internal/adapters/mailer.go
new file mode 100644
index 0000000..becd433
--- /dev/null
+++ b/internal/adapters/mailer.go
@@ -0,0 +1,138 @@
+package adapters
+
+import (
+ "context"
+ "crypto/tls"
+ "errors"
+ "fmt"
+ "github.com/h44z/wg-portal/internal"
+ "github.com/h44z/wg-portal/internal/config"
+ "github.com/h44z/wg-portal/internal/domain"
+ mail "github.com/xhit/go-simple-mail/v2"
+ "io"
+ "time"
+)
+
+type MailRepo struct {
+ cfg *config.MailConfig
+}
+
+func NewSmtpMailRepo(cfg config.MailConfig) MailRepo {
+ return MailRepo{cfg: &cfg}
+}
+
+// Send sends a mail.
+func (r MailRepo) Send(_ context.Context, subject, body string, to []string, options *domain.MailOptions) error {
+ if options == nil {
+ options = &domain.MailOptions{}
+ }
+ r.setDefaultOptions(r.cfg.From, options)
+
+ if len(to) == 0 {
+ return errors.New("missing email recipient")
+ }
+
+ uniqueTo := internal.UniqueStringSlice(to)
+ email := mail.NewMSG()
+ email.SetFrom(r.cfg.From).
+ AddTo(uniqueTo...).
+ SetReplyTo(options.ReplyTo).
+ SetSubject(subject).
+ SetBody(mail.TextPlain, body)
+
+ if len(options.Cc) > 0 {
+ // the underlying mail library does not allow the same address to appear in TO and CC... so filter entries that are already included
+ // in the TO addresses
+ cc := RemoveDuplicates(internal.UniqueStringSlice(options.Cc), uniqueTo)
+ email.AddCc(cc...)
+ }
+ if len(options.Bcc) > 0 {
+ // the underlying mail library does not allow the same address to appear in TO or CC and BCC... so filter entries that are already
+ // included in the TO and CC addresses
+ bcc := RemoveDuplicates(internal.UniqueStringSlice(options.Bcc), uniqueTo)
+ bcc = RemoveDuplicates(bcc, options.Cc)
+
+ email.AddCc(internal.UniqueStringSlice(options.Bcc)...)
+ }
+ if options.HtmlBody != "" {
+ email.AddAlternative(mail.TextHTML, options.HtmlBody)
+ }
+
+ for _, attachment := range options.Attachments {
+ attachmentData, err := io.ReadAll(attachment.Data)
+ if err != nil {
+ return fmt.Errorf("failed to read attachment data for %s: %w", attachment.Name, err)
+ }
+
+ if attachment.Embedded {
+ email.AddInlineData(attachmentData, attachment.Name, attachment.ContentType)
+ } else {
+ email.AddAttachmentData(attachmentData, attachment.Name, attachment.ContentType)
+ }
+ }
+
+ // Call Send and pass the client
+ srv := r.getMailServer()
+ client, err := srv.Connect()
+ if err != nil {
+ return fmt.Errorf("failed to connect to SMTP server: %w", err)
+ }
+
+ err = email.Send(client)
+ if err != nil {
+ return fmt.Errorf("failed to send email: %w", err)
+ }
+
+ return nil
+}
+
+func (r MailRepo) setDefaultOptions(sender string, options *domain.MailOptions) {
+ if options.ReplyTo == "" {
+ options.ReplyTo = sender
+ }
+}
+
+func (r MailRepo) getMailServer() *mail.SMTPServer {
+ srv := mail.NewSMTPClient()
+
+ srv.ConnectTimeout = 30 * time.Second
+ srv.SendTimeout = 30 * time.Second
+ srv.Host = r.cfg.Host
+ srv.Port = r.cfg.Port
+ srv.Username = r.cfg.Username
+ srv.Password = r.cfg.Password
+
+ switch r.cfg.Encryption {
+ case config.MailEncryptionTLS:
+ srv.Encryption = mail.EncryptionSSLTLS
+ case config.MailEncryptionStartTLS:
+ srv.Encryption = mail.EncryptionSTARTTLS
+ default: // MailEncryptionNone
+ srv.Encryption = mail.EncryptionNone
+ }
+ srv.TLSConfig = &tls.Config{ServerName: srv.Host, InsecureSkipVerify: !r.cfg.CertValidation}
+ switch r.cfg.AuthType {
+ case config.MailAuthPlain:
+ srv.Authentication = mail.AuthPlain
+ case config.MailAuthLogin:
+ srv.Authentication = mail.AuthLogin
+ case config.MailAuthCramMD5:
+ srv.Authentication = mail.AuthCRAMMD5
+ }
+
+ return srv
+}
+
+// RemoveDuplicates removes addresses from the given string slice which are contained in the remove slice.
+func RemoveDuplicates(slice []string, remove []string) []string {
+ uniqueSlice := make([]string, 0, len(slice))
+
+ for _, i := range remove {
+ for _, j := range slice {
+ if i != j {
+ uniqueSlice = append(uniqueSlice, j)
+ }
+ }
+ }
+ return uniqueSlice
+}
diff --git a/internal/adapters/wgquick.go b/internal/adapters/wgquick.go
new file mode 100644
index 0000000..58de124
--- /dev/null
+++ b/internal/adapters/wgquick.go
@@ -0,0 +1,99 @@
+package adapters
+
+import (
+ "bytes"
+ "fmt"
+ "github.com/h44z/wg-portal/internal"
+ "github.com/h44z/wg-portal/internal/domain"
+ "github.com/sirupsen/logrus"
+ "os/exec"
+ "strings"
+)
+
+// WgQuickRepo implements higher level wg-quick like interactions like setting DNS, routing tables or interface hooks.
+type WgQuickRepo struct {
+ shellCmd string
+ resolvConfIfacePrefix string
+}
+
+func NewWgQuickRepo() *WgQuickRepo {
+ return &WgQuickRepo{
+ shellCmd: "bash",
+ resolvConfIfacePrefix: "tun.",
+ }
+}
+
+func (r *WgQuickRepo) ExecuteInterfaceHook(id domain.InterfaceIdentifier, hookCmd string) error {
+ if hookCmd == "" {
+ return nil
+ }
+
+ err := r.exec(hookCmd, id)
+ if err != nil {
+ return fmt.Errorf("failed to exec hook: %w", err)
+ }
+
+ return nil
+}
+
+func (r *WgQuickRepo) SetDNS(id domain.InterfaceIdentifier, dnsStr, dnsSearchStr string) error {
+ if dnsStr == "" && dnsSearchStr == "" {
+ return nil
+ }
+
+ dnsServers := internal.SliceString(dnsStr)
+ dnsSearchDomains := internal.SliceString(dnsSearchStr)
+
+ dnsCommand := "resolvconf -a %resPref%i -m 0 -x"
+ dnsCommandInput := make([]string, 0, len(dnsServers)+len(dnsSearchDomains))
+
+ for _, dnsServer := range dnsServers {
+ dnsCommandInput = append(dnsCommandInput, fmt.Sprintf("nameserver %s", dnsServer))
+ }
+ for _, searchDomain := range dnsSearchDomains {
+ dnsCommandInput = append(dnsCommandInput, fmt.Sprintf("search %s", searchDomain))
+ }
+
+ err := r.exec(dnsCommand, id, dnsCommandInput...)
+ if err != nil {
+ return fmt.Errorf("failed to set dns settings: %w", err)
+ }
+
+ return nil
+}
+
+func (r *WgQuickRepo) UnsetDNS(id domain.InterfaceIdentifier) error {
+ dnsCommand := "resolvconf -d %resPref%i -f"
+
+ err := r.exec(dnsCommand, id)
+ if err != nil {
+ return fmt.Errorf("failed to unset dns settings: %w", err)
+ }
+
+ return nil
+}
+
+func (r *WgQuickRepo) replaceCommandPlaceHolders(command string, interfaceId domain.InterfaceIdentifier) string {
+ command = strings.ReplaceAll(command, "%resPref", r.resolvConfIfacePrefix)
+ return strings.ReplaceAll(command, "%i", string(interfaceId))
+}
+
+func (r *WgQuickRepo) exec(command string, interfaceId domain.InterfaceIdentifier, stdin ...string) error {
+ commandWithInterfaceName := r.replaceCommandPlaceHolders(command, interfaceId)
+ cmd := exec.Command(r.shellCmd, "-ce", commandWithInterfaceName)
+ if len(stdin) > 0 {
+ b := &bytes.Buffer{}
+ for _, ln := range stdin {
+ if _, err := fmt.Fprint(b, ln); err != nil {
+ return err
+ }
+ }
+ cmd.Stdin = b
+ }
+ out, err := cmd.CombinedOutput() // execute and wait for output
+ if err != nil {
+ return fmt.Errorf("failed to exexute shell command %s: %w", commandWithInterfaceName, err)
+ }
+ logrus.Tracef("executed shell command %s, with output: %s", commandWithInterfaceName, string(out))
+ return nil
+}
diff --git a/internal/adapters/wireguard.go b/internal/adapters/wireguard.go
new file mode 100644
index 0000000..434c246
--- /dev/null
+++ b/internal/adapters/wireguard.go
@@ -0,0 +1,430 @@
+package adapters
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "github.com/h44z/wg-portal/internal/domain"
+ "github.com/h44z/wg-portal/internal/lowlevel"
+ "github.com/vishvananda/netlink"
+ "golang.zx2c4.com/wireguard/wgctrl"
+ "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
+ "os"
+)
+
+// WgRepo implements all low-level WireGuard interactions.
+type WgRepo struct {
+ wg lowlevel.WireGuardClient
+ nl lowlevel.NetlinkClient
+}
+
+func NewWireGuardRepository() *WgRepo {
+ wg, err := wgctrl.New()
+ if err != nil {
+ panic("failed to init wgctrl: " + err.Error())
+ }
+
+ nl := &lowlevel.NetlinkManager{}
+
+ repo := &WgRepo{
+ wg: wg,
+ nl: nl,
+ }
+
+ return repo
+}
+
+func (r *WgRepo) GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, error) {
+ devices, err := r.wg.Devices()
+ if err != nil {
+ return nil, fmt.Errorf("device list error: %w", err)
+ }
+
+ interfaces := make([]domain.PhysicalInterface, 0, len(devices))
+ for _, device := range devices {
+ interfaceModel, err := r.convertWireGuardInterface(device)
+ if err != nil {
+ return nil, fmt.Errorf("interface convert failed for %s: %w", device.Name, err)
+ }
+ interfaces = append(interfaces, interfaceModel)
+ }
+
+ return interfaces, nil
+}
+
+func (r *WgRepo) GetInterface(_ context.Context, id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error) {
+ return r.getInterface(id)
+}
+
+func (r *WgRepo) GetPeers(_ context.Context, deviceId domain.InterfaceIdentifier) ([]domain.PhysicalPeer, error) {
+ device, err := r.wg.Device(string(deviceId))
+ if err != nil {
+ return nil, fmt.Errorf("device error: %w", err)
+ }
+
+ peers := make([]domain.PhysicalPeer, 0, len(device.Peers))
+ for _, peer := range device.Peers {
+ peerModel, err := r.convertWireGuardPeer(&peer)
+ if err != nil {
+ return nil, fmt.Errorf("peer convert failed for %v: %w", peer.PublicKey, err)
+ }
+ peers = append(peers, peerModel)
+ }
+
+ return peers, nil
+}
+
+func (r *WgRepo) GetPeer(_ context.Context, deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier) (*domain.PhysicalPeer, error) {
+ return r.getPeer(deviceId, id)
+}
+
+func (r *WgRepo) convertWireGuardInterface(device *wgtypes.Device) (domain.PhysicalInterface, error) {
+ // read data from wgctrl interface
+
+ iface := domain.PhysicalInterface{
+ Identifier: domain.InterfaceIdentifier(device.Name),
+ KeyPair: domain.KeyPair{
+ PrivateKey: device.PrivateKey.String(),
+ PublicKey: device.PublicKey.String(),
+ },
+ ListenPort: device.ListenPort,
+ Addresses: nil,
+ Mtu: 0,
+ FirewallMark: int32(device.FirewallMark),
+ DeviceUp: false,
+ ImportSource: "wgctrl",
+ DeviceType: device.Type.String(),
+ BytesUpload: 0,
+ BytesDownload: 0,
+ }
+
+ // read data from netlink interface
+
+ lowLevelInterface, err := r.nl.LinkByName(device.Name)
+ if err != nil {
+ return domain.PhysicalInterface{}, fmt.Errorf("netlink error for %s: %w", device.Name, err)
+ }
+ ipAddresses, err := r.nl.AddrList(lowLevelInterface)
+ if err != nil {
+ return domain.PhysicalInterface{}, fmt.Errorf("ip read error for %s: %w", device.Name, err)
+ }
+
+ for _, addr := range ipAddresses {
+ iface.Addresses = append(iface.Addresses, domain.CidrFromNetlinkAddr(addr))
+ }
+ iface.Mtu = lowLevelInterface.Attrs().MTU
+ iface.DeviceUp = lowLevelInterface.Attrs().OperState == netlink.OperUnknown // wg only supports unknown
+ if stats := lowLevelInterface.Attrs().Statistics; stats != nil {
+ iface.BytesUpload = stats.TxBytes
+ iface.BytesDownload = stats.RxBytes
+ }
+
+ return iface, nil
+}
+
+func (r *WgRepo) convertWireGuardPeer(peer *wgtypes.Peer) (domain.PhysicalPeer, error) {
+ peerModel := domain.PhysicalPeer{
+ Identifier: domain.PeerIdentifier(peer.PublicKey.String()),
+ Endpoint: "",
+ AllowedIPs: nil,
+ KeyPair: domain.KeyPair{
+ PublicKey: peer.PublicKey.String(),
+ },
+ PresharedKey: "",
+ PersistentKeepalive: int(peer.PersistentKeepaliveInterval.Seconds()),
+ LastHandshake: peer.LastHandshakeTime,
+ ProtocolVersion: peer.ProtocolVersion,
+ BytesUpload: uint64(peer.ReceiveBytes),
+ BytesDownload: uint64(peer.TransmitBytes),
+ }
+
+ for _, addr := range peer.AllowedIPs {
+ peerModel.AllowedIPs = append(peerModel.AllowedIPs, domain.CidrFromIpNet(addr))
+ }
+ if peer.Endpoint != nil {
+ peerModel.Endpoint = peer.Endpoint.String()
+ }
+ if peer.PresharedKey != (wgtypes.Key{}) {
+ peerModel.PresharedKey = domain.PreSharedKey(peer.PresharedKey.String())
+ }
+
+ return peerModel, nil
+}
+
+func (r *WgRepo) SaveInterface(_ context.Context, id domain.InterfaceIdentifier, updateFunc func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error)) error {
+ physicalInterface, err := r.getOrCreateInterface(id)
+ if err != nil {
+ return err
+ }
+
+ if updateFunc != nil {
+ physicalInterface, err = updateFunc(physicalInterface)
+ if err != nil {
+ return err
+ }
+ }
+
+ if err := r.updateLowLevelInterface(physicalInterface); err != nil {
+ return err
+ }
+ if err := r.updateWireGuardInterface(physicalInterface); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (r *WgRepo) getOrCreateInterface(id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error) {
+ device, err := r.getInterface(id)
+ if err == nil {
+ return device, nil
+ }
+ if err != nil && !errors.Is(err, os.ErrNotExist) {
+ return nil, fmt.Errorf("device error: %w", err)
+ }
+
+ // create new device
+ if err := r.createLowLevelInterface(id); err != nil {
+ return nil, err
+ }
+
+ device, err = r.getInterface(id)
+ return device, err
+}
+
+func (r *WgRepo) getInterface(id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error) {
+ device, err := r.wg.Device(string(id))
+ if err != nil {
+ return nil, err
+ }
+
+ pi, err := r.convertWireGuardInterface(device)
+ return &pi, err
+}
+
+func (r *WgRepo) createLowLevelInterface(id domain.InterfaceIdentifier) error {
+ link := &netlink.GenericLink{
+ LinkAttrs: netlink.LinkAttrs{
+ Name: string(id),
+ },
+ LinkType: "wireguard",
+ }
+ err := r.nl.LinkAdd(link)
+ if err != nil {
+ return fmt.Errorf("link add failed: %w", err)
+ }
+
+ return nil
+}
+
+func (r *WgRepo) updateLowLevelInterface(pi *domain.PhysicalInterface) error {
+ link, err := r.nl.LinkByName(string(pi.Identifier))
+ if err != nil {
+ return err
+ }
+ if pi.Mtu != 0 {
+ if err := r.nl.LinkSetMTU(link, pi.Mtu); err != nil {
+ return fmt.Errorf("mtu error: %w", err)
+ }
+ }
+
+ for _, addr := range pi.Addresses {
+ err := r.nl.AddrReplace(link, addr.NetlinkAddr())
+ if err != nil {
+ return fmt.Errorf("failed to set ip %s: %w", addr.String(), err)
+ }
+ }
+
+ // Remove unwanted IP addresses
+ rawAddresses, err := r.nl.AddrList(link)
+ if err != nil {
+ return fmt.Errorf("failed to fetch interface ips: %w", err)
+ }
+ for _, rawAddr := range rawAddresses {
+ netlinkAddr := domain.CidrFromNetlinkAddr(rawAddr)
+ remove := true
+ for _, addr := range pi.Addresses {
+ if addr == netlinkAddr {
+ remove = false
+ break
+ }
+ }
+
+ if !remove {
+ continue
+ }
+
+ err := r.nl.AddrDel(link, &rawAddr)
+ if err != nil {
+ return fmt.Errorf("failed to remove deprecated ip %s: %w", netlinkAddr.String(), err)
+ }
+ }
+
+ // Update link state
+ if pi.DeviceUp {
+ if err := r.nl.LinkSetUp(link); err != nil {
+ return fmt.Errorf("failed to bring up device: %w", err)
+ }
+ } else {
+ if err := r.nl.LinkSetDown(link); err != nil {
+ return fmt.Errorf("failed to bring down device: %w", err)
+ }
+ }
+
+ return nil
+}
+
+func (r *WgRepo) updateWireGuardInterface(pi *domain.PhysicalInterface) error {
+ pKey, err := wgtypes.NewKey(pi.KeyPair.GetPrivateKeyBytes())
+ if err != nil {
+ return err
+ }
+
+ var fwMark *int
+ if pi.FirewallMark != 0 {
+ *fwMark = int(pi.FirewallMark)
+ }
+ err = r.wg.ConfigureDevice(string(pi.Identifier), wgtypes.Config{
+ PrivateKey: &pKey,
+ ListenPort: &pi.ListenPort,
+ FirewallMark: fwMark,
+ ReplacePeers: false,
+ })
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (r *WgRepo) DeleteInterface(_ context.Context, id domain.InterfaceIdentifier) error {
+ if err := r.deleteLowLevelInterface(id); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (r *WgRepo) deleteLowLevelInterface(id domain.InterfaceIdentifier) error {
+ link, err := r.nl.LinkByName(string(id))
+ if err != nil {
+ var linkNotFoundError netlink.LinkNotFoundError
+ if errors.As(err, &linkNotFoundError) {
+ return nil // ignore not found error
+ }
+ return fmt.Errorf("unable to find low level interface: %w", err)
+ }
+
+ err = r.nl.LinkDel(link)
+ if err != nil {
+ return fmt.Errorf("failed to delete low level interface: %w", err)
+ }
+
+ return nil
+}
+
+func (r *WgRepo) SavePeer(_ context.Context, deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier, updateFunc func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error)) error {
+ physicalPeer, err := r.getOrCreatePeer(deviceId, id)
+ if err != nil {
+ return err
+ }
+
+ physicalPeer, err = updateFunc(physicalPeer)
+ if err != nil {
+ return err
+ }
+
+ if err := r.updatePeer(deviceId, physicalPeer); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (r *WgRepo) getOrCreatePeer(deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier) (*domain.PhysicalPeer, error) {
+ peer, err := r.getPeer(deviceId, id)
+ if err == nil {
+ return peer, nil
+ }
+ if err != nil && !errors.Is(err, os.ErrNotExist) {
+ return nil, fmt.Errorf("peer error: %w", err)
+ }
+
+ // create new peer
+ err = r.wg.ConfigureDevice(string(deviceId), wgtypes.Config{Peers: []wgtypes.PeerConfig{{
+ PublicKey: id.ToPublicKey(),
+ }}})
+
+ peer, err = r.getPeer(deviceId, id)
+ return peer, nil
+}
+
+func (r *WgRepo) getPeer(deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier) (*domain.PhysicalPeer, error) {
+ if !id.IsPublicKey() {
+ return nil, errors.New("invalid public key")
+ }
+
+ device, err := r.wg.Device(string(deviceId))
+ if err != nil {
+ return nil, err
+ }
+
+ publicKey := id.ToPublicKey()
+ for _, peer := range device.Peers {
+ if peer.PublicKey != publicKey {
+ continue
+ }
+
+ peerModel, err := r.convertWireGuardPeer(&peer)
+ return &peerModel, err
+ }
+
+ return nil, os.ErrNotExist
+}
+
+func (r *WgRepo) updatePeer(deviceId domain.InterfaceIdentifier, pp *domain.PhysicalPeer) error {
+ cfg := wgtypes.PeerConfig{
+ PublicKey: pp.GetPublicKey(),
+ Remove: false,
+ UpdateOnly: true,
+ PresharedKey: pp.GetPresharedKey(),
+ Endpoint: pp.GetEndpointAddress(),
+ PersistentKeepaliveInterval: pp.GetPersistentKeepaliveTime(),
+ ReplaceAllowedIPs: true,
+ AllowedIPs: pp.GetAllowedIPs(),
+ }
+
+ err := r.wg.ConfigureDevice(string(deviceId), wgtypes.Config{ReplacePeers: false, Peers: []wgtypes.PeerConfig{cfg}})
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (r *WgRepo) DeletePeer(_ context.Context, deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier) error {
+ if !id.IsPublicKey() {
+ return errors.New("invalid public key")
+ }
+
+ err := r.deletePeer(deviceId, id)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (r *WgRepo) deletePeer(deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier) error {
+ cfg := wgtypes.PeerConfig{
+ PublicKey: id.ToPublicKey(),
+ Remove: true,
+ }
+
+ err := r.wg.ConfigureDevice(string(deviceId), wgtypes.Config{ReplacePeers: false, Peers: []wgtypes.PeerConfig{cfg}})
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/internal/adapters/wireguard_integration_test.go b/internal/adapters/wireguard_integration_test.go
new file mode 100644
index 0000000..07b35f2
--- /dev/null
+++ b/internal/adapters/wireguard_integration_test.go
@@ -0,0 +1,121 @@
+//go:build integration
+
+package adapters
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "os"
+ "os/exec"
+ "strconv"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/h44z/wg-portal/internal/domain"
+
+ "github.com/stretchr/testify/assert"
+)
+
+// setup WireGuard manager with no linked store
+func setup(t *testing.T) *WgRepo {
+ if getProcessOwner() != "root" {
+ t.Fatalf("this tests need to be executed as root user")
+ }
+
+ repo := NewWireGuardRepository()
+
+ return repo
+}
+
+func getProcessOwner() string {
+ stdout, err := exec.Command("ps", "-o", "user=", "-p", strconv.Itoa(os.Getpid())).Output()
+ if err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ }
+ return strings.TrimSpace(string(stdout))
+}
+
+func Test_wgRepository_GetInterfaces(t *testing.T) {
+ mgr := setup(t)
+
+ interfaceName := domain.InterfaceIdentifier("wg_test_001")
+ defer mgr.DeleteInterface(context.Background(), interfaceName)
+ err := mgr.SaveInterface(context.Background(), interfaceName, nil)
+ require.NoError(t, err)
+
+ interfaceName2 := domain.InterfaceIdentifier("wg_test_002")
+ defer mgr.DeleteInterface(context.Background(), interfaceName2)
+ err = mgr.SaveInterface(context.Background(), interfaceName2, nil)
+ require.NoError(t, err)
+
+ interfaces, err := mgr.GetInterfaces(context.Background())
+ assert.NoError(t, err)
+ assert.Len(t, interfaces, 2)
+ for _, iface := range interfaces {
+ assert.True(t, iface.Identifier == interfaceName || iface.Identifier == interfaceName2)
+ }
+}
+
+func TestWireGuardCreateInterface(t *testing.T) {
+ mgr := setup(t)
+
+ interfaceName := domain.InterfaceIdentifier("wg_test_001")
+ ipAddress := "10.11.12.13"
+ ipV6Address := "1337:d34d:b33f::2"
+ defer mgr.DeleteInterface(context.Background(), interfaceName)
+
+ err := mgr.SaveInterface(context.Background(), interfaceName, func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error) {
+ pi.Addresses = []domain.Cidr{
+ domain.CidrFromIpNet(net.IPNet{IP: net.ParseIP(ipAddress), Mask: net.CIDRMask(24, 32)}),
+ domain.CidrFromIpNet(net.IPNet{IP: net.ParseIP(ipV6Address), Mask: net.CIDRMask(64, 128)}),
+ }
+ return pi, nil
+ })
+ assert.NoError(t, err)
+
+ // Validate that the interface has been created
+ cmd := exec.Command("ip", "addr")
+ out, err := cmd.CombinedOutput()
+ assert.NoError(t, err)
+ assert.Contains(t, string(out), interfaceName)
+ assert.Contains(t, string(out), ipAddress)
+ assert.Contains(t, string(out), ipV6Address)
+}
+
+func TestWireGuardUpdateInterface(t *testing.T) {
+ mgr := setup(t)
+
+ interfaceName := domain.InterfaceIdentifier("wg_test_001")
+ defer mgr.DeleteInterface(context.Background(), interfaceName)
+
+ err := mgr.SaveInterface(context.Background(), interfaceName, nil)
+ require.NoError(t, err)
+
+ cmd := exec.Command("ip", "addr")
+ out, err := cmd.CombinedOutput()
+ require.NoError(t, err)
+ require.Contains(t, string(out), interfaceName)
+
+ ipAddress := "10.11.12.13"
+ ipV6Address := "1337:d34d:b33f::2"
+ err = mgr.SaveInterface(context.Background(), interfaceName, func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error) {
+ pi.Addresses = []domain.Cidr{
+ domain.CidrFromIpNet(net.IPNet{IP: net.ParseIP(ipAddress), Mask: net.CIDRMask(24, 32)}),
+ domain.CidrFromIpNet(net.IPNet{IP: net.ParseIP(ipV6Address), Mask: net.CIDRMask(64, 128)}),
+ }
+ return pi, nil
+ })
+ assert.NoError(t, err)
+
+ // Validate that the interface has been updated
+ cmd = exec.Command("ip", "addr")
+ out, err = cmd.CombinedOutput()
+ assert.NoError(t, err)
+ assert.Contains(t, string(out), interfaceName)
+ assert.Contains(t, string(out), ipAddress)
+ assert.Contains(t, string(out), ipV6Address)
+}
diff --git a/internal/app/api/core/assets.go b/internal/app/api/core/assets.go
new file mode 100644
index 0000000..67be5be
--- /dev/null
+++ b/internal/app/api/core/assets.go
@@ -0,0 +1,21 @@
+package core
+
+import "embed"
+
+//go:embed assets/tpl/*
+var apiTemplates embed.FS
+
+//go:embed assets/css/*
+//go:embed assets/fonts/*
+//go:embed assets/img/*
+//go:embed assets/js/*
+//go:embed assets/doc/*
+var apiStatics embed.FS
+
+//go:embed frontend-dist/assets/*
+//go:embed frontend-dist/img/*
+//go:embed frontend-dist/index.html
+//go:embed frontend-dist/favicon.ico
+//go:embed frontend-dist/favicon.png
+//go:embed frontend-dist/favicon-large.png
+var frontendStatics embed.FS
diff --git a/assets/css/bootstrap.min.css b/internal/app/api/core/assets/css/bootstrap.min.css
similarity index 100%
rename from assets/css/bootstrap.min.css
rename to internal/app/api/core/assets/css/bootstrap.min.css
diff --git a/internal/app/api/core/assets/doc/v0_swagger.json b/internal/app/api/core/assets/doc/v0_swagger.json
new file mode 100644
index 0000000..ee1ed7a
--- /dev/null
+++ b/internal/app/api/core/assets/doc/v0_swagger.json
@@ -0,0 +1,1569 @@
+{
+ "swagger": "2.0",
+ "info": {
+ "description": "WireGuard Portal API - a testing API endpoint",
+ "title": "WireGuard Portal API",
+ "contact": {
+ "name": "WireGuard Portal Developers",
+ "url": "https://github.com/h44z/wg-portal"
+ },
+ "version": "0.0"
+ },
+ "basePath": "/api/v0",
+ "paths": {
+ "/auth/login": {
+ "post": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Authentication"
+ ],
+ "summary": "Get all available external login providers.",
+ "operationId": "auth_handleLoginPost",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/model.LoginProviderInfo"
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/logout": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Authentication"
+ ],
+ "summary": "Get all available external login providers.",
+ "operationId": "auth_handleLogoutGet",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/model.LoginProviderInfo"
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/providers": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Authentication"
+ ],
+ "summary": "Get all available external login providers.",
+ "operationId": "auth_handleExternalLoginProvidersGet",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/model.LoginProviderInfo"
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/session": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Authentication"
+ ],
+ "summary": "Get information about the currently logged-in user.",
+ "operationId": "auth_handleSessionInfoGet",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/model.SessionInfo"
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ }
+ }
+ }
+ },
+ "/auth/{provider}/callback": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Authentication"
+ ],
+ "summary": "Handle the OAuth callback.",
+ "operationId": "auth_handleOauthCallbackGet",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/model.LoginProviderInfo"
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/{provider}/init": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Authentication"
+ ],
+ "summary": "Initiate the OAuth login flow.",
+ "operationId": "auth_handleOauthInitiateGet",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/model.LoginProviderInfo"
+ }
+ }
+ }
+ }
+ }
+ },
+ "/config/frontend.js": {
+ "get": {
+ "produces": [
+ "text/javascript"
+ ],
+ "tags": [
+ "Configuration"
+ ],
+ "summary": "Get the dynamic frontend configuration javascript.",
+ "operationId": "config_handleConfigJsGet",
+ "responses": {
+ "200": {
+ "description": "The JavaScript contents",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/csrf": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Security"
+ ],
+ "summary": "Get a CSRF token for the current session.",
+ "operationId": "base_handleCsrfGet",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/hostname": {
+ "get": {
+ "description": "Nothing more to describe...",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Testing"
+ ],
+ "summary": "Get the current host name.",
+ "operationId": "test_handleHostnameGet",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ }
+ }
+ }
+ },
+ "/interface/all": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Interface"
+ ],
+ "summary": "Get all available interfaces.",
+ "operationId": "interfaces_handleAllGet",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/model.Interface"
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ }
+ }
+ }
+ },
+ "/interface/config/{id}": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Interface"
+ ],
+ "summary": "Get interface configuration as string.",
+ "operationId": "interfaces_handleConfigGet",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ }
+ }
+ }
+ },
+ "/interface/get/{id}": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Interface"
+ ],
+ "summary": "Get single interface.",
+ "operationId": "interfaces_handleSingleGet",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/model.Interface"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ }
+ }
+ }
+ },
+ "/interface/new": {
+ "post": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Interface"
+ ],
+ "summary": "Create the new interface record.",
+ "operationId": "interfaces_handleCreatePost",
+ "parameters": [
+ {
+ "description": "The interface data",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/model.Interface"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/model.Interface"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ }
+ }
+ }
+ },
+ "/interface/peers/{id}": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Interface"
+ ],
+ "summary": "Get peers for the given interface.",
+ "operationId": "interfaces_handlePeersGet",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/model.Peer"
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ }
+ }
+ }
+ },
+ "/interface/prepare": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Interface"
+ ],
+ "summary": "Prepare a new interface.",
+ "operationId": "interfaces_handlePrepareGet",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/model.Interface"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ }
+ }
+ }
+ },
+ "/interface/{id}": {
+ "put": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Interface"
+ ],
+ "summary": "Update the interface record.",
+ "operationId": "interfaces_handleUpdatePut",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "The interface identifier",
+ "name": "id",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "The interface data",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/model.Interface"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/model.Interface"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ }
+ }
+ },
+ "delete": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Interface"
+ ],
+ "summary": "Delete the interface record.",
+ "operationId": "interfaces_handleDelete",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "The interface identifier",
+ "name": "id",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": "No content if deletion was successful"
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ }
+ }
+ }
+ },
+ "/now": {
+ "get": {
+ "description": "Nothing more to describe...",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Testing"
+ ],
+ "summary": "Get the current local time.",
+ "operationId": "test_handleCurrentTimeGet",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ }
+ }
+ }
+ },
+ "/peer/config-qr/{id}": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Peer"
+ ],
+ "summary": "Get peer configuration as qr code.",
+ "operationId": "peers_handleQrCodeGet",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ }
+ }
+ }
+ },
+ "/peer/config/{id}": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Peer"
+ ],
+ "summary": "Get peer configuration as string.",
+ "operationId": "peers_handleConfigGet",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ }
+ }
+ }
+ },
+ "/peer/iface/{iface}/all": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Peer"
+ ],
+ "summary": "Get peers for the given interface.",
+ "operationId": "peers_handleAllGet",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "The interface identifier",
+ "name": "iface",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/model.Peer"
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ }
+ }
+ }
+ },
+ "/peer/iface/{iface}/new": {
+ "post": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Peer"
+ ],
+ "summary": "Prepare a new peer for the given interface.",
+ "operationId": "peers_handleCreatePost",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "The interface identifier",
+ "name": "iface",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "The peer data",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/model.Peer"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/model.Peer"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ }
+ }
+ }
+ },
+ "/peer/iface/{iface}/prepare": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Peer"
+ ],
+ "summary": "Prepare a new peer for the given interface.",
+ "operationId": "peers_handlePrepareGet",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "The interface identifier",
+ "name": "iface",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/model.Peer"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ }
+ }
+ }
+ },
+ "/peer/{id}": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Peer"
+ ],
+ "summary": "Get peer for the given identifier.",
+ "operationId": "peers_handleSingleGet",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "The peer identifier",
+ "name": "id",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/model.Peer"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ }
+ }
+ },
+ "put": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Peer"
+ ],
+ "summary": "Update the given peer record.",
+ "operationId": "peers_handleUpdatePut",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "The peer identifier",
+ "name": "id",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "The peer data",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/model.Peer"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/model.Peer"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ }
+ }
+ },
+ "delete": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Peer"
+ ],
+ "summary": "Delete the peer record.",
+ "operationId": "peers_handleDelete",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "The peer identifier",
+ "name": "id",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": "No content if deletion was successful"
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ }
+ }
+ }
+ },
+ "/user/all": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Users"
+ ],
+ "summary": "Get all user records.",
+ "operationId": "users_handleAllGet",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/model.User"
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ }
+ }
+ }
+ },
+ "/user/new": {
+ "post": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Users"
+ ],
+ "summary": "Create the new user record.",
+ "operationId": "users_handleCreatePost",
+ "parameters": [
+ {
+ "description": "The user data",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/model.User"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/model.User"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ }
+ }
+ }
+ },
+ "/user/{id}": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Users"
+ ],
+ "summary": "Get a single user record.",
+ "operationId": "users_handleSingleGet",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "The user identifier",
+ "name": "id",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/model.User"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ }
+ }
+ },
+ "put": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Users"
+ ],
+ "summary": "Update the user record.",
+ "operationId": "users_handleUpdatePut",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "The user identifier",
+ "name": "id",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "The user data",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/model.User"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/model.User"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ }
+ }
+ },
+ "delete": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Users"
+ ],
+ "summary": "Delete the user record.",
+ "operationId": "users_handleDelete",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "The user identifier",
+ "name": "id",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": "No content if deletion was successful"
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ }
+ }
+ }
+ },
+ "/user/{id}/peers": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Users"
+ ],
+ "summary": "Get peers for the given user.",
+ "operationId": "users_handlePeersGet",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/model.Peer"
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/model.Error"
+ }
+ }
+ }
+ }
+ }
+ },
+ "definitions": {
+ "model.Error": {
+ "type": "object",
+ "properties": {
+ "Code": {
+ "type": "integer"
+ },
+ "Message": {
+ "type": "string"
+ }
+ }
+ },
+ "model.Int32ConfigOption": {
+ "type": "object",
+ "properties": {
+ "Overridable": {
+ "type": "boolean"
+ },
+ "Value": {
+ "type": "integer"
+ }
+ }
+ },
+ "model.IntConfigOption": {
+ "type": "object",
+ "properties": {
+ "Overridable": {
+ "type": "boolean"
+ },
+ "Value": {
+ "type": "integer"
+ }
+ }
+ },
+ "model.Interface": {
+ "type": "object",
+ "properties": {
+ "Addresses": {
+ "description": "the interface ip addresses",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "Disabled": {
+ "description": "flag that specifies if the interface is enabled (up) or not (down)",
+ "type": "boolean"
+ },
+ "DisabledReason": {
+ "description": "the reason why the interface has been disabled",
+ "type": "string"
+ },
+ "DisplayName": {
+ "description": "a nice display name/ description for the interface",
+ "type": "string"
+ },
+ "Dns": {
+ "description": "the dns server that should be set if the interface is up, comma separated",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "DnsSearch": {
+ "description": "the dns search option string that should be set if the interface is up, will be appended to DnsStr",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "EnabledPeers": {
+ "type": "integer"
+ },
+ "FirewallMark": {
+ "description": "a firewall mark",
+ "type": "integer"
+ },
+ "Identifier": {
+ "description": "device name, for example: wg0",
+ "type": "string",
+ "example": "wg0"
+ },
+ "ListenPort": {
+ "description": "the listening port, for example: 51820",
+ "type": "integer"
+ },
+ "Mode": {
+ "description": "the interface type, either 'server', 'client' or 'any'",
+ "type": "string",
+ "example": "server"
+ },
+ "Mtu": {
+ "description": "the device MTU",
+ "type": "integer"
+ },
+ "PeerDefAllowedIPs": {
+ "description": "the default allowed IP string for the peer",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "PeerDefDns": {
+ "description": "the default dns server for the peer",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "PeerDefDnsSearch": {
+ "description": "the default dns search options for the peer",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "PeerDefEndpoint": {
+ "description": "the default endpoint for the peer",
+ "type": "string"
+ },
+ "PeerDefFirewallMark": {
+ "description": "default firewall mark",
+ "type": "integer"
+ },
+ "PeerDefMtu": {
+ "description": "the default device MTU",
+ "type": "integer"
+ },
+ "PeerDefNetwork": {
+ "description": "the default subnets from which peers will get their IP addresses, comma seperated",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "PeerDefPersistentKeepalive": {
+ "description": "the default persistent keep-alive Value",
+ "type": "integer"
+ },
+ "PeerDefPostDown": {
+ "description": "default action that is executed after the device is down",
+ "type": "string"
+ },
+ "PeerDefPostUp": {
+ "description": "default action that is executed after the device is up",
+ "type": "string"
+ },
+ "PeerDefPreDown": {
+ "description": "default action that is executed before the device is down",
+ "type": "string"
+ },
+ "PeerDefPreUp": {
+ "description": "default action that is executed before the device is up",
+ "type": "string"
+ },
+ "PeerDefRoutingTable": {
+ "description": "the default routing table",
+ "type": "string"
+ },
+ "PostDown": {
+ "description": "action that is executed after the device is down",
+ "type": "string"
+ },
+ "PostUp": {
+ "description": "action that is executed after the device is up",
+ "type": "string"
+ },
+ "PreDown": {
+ "description": "action that is executed before the device is down",
+ "type": "string"
+ },
+ "PreUp": {
+ "description": "action that is executed before the device is up",
+ "type": "string"
+ },
+ "PrivateKey": {
+ "description": "private Key of the server interface",
+ "type": "string",
+ "example": "abcdef=="
+ },
+ "PublicKey": {
+ "description": "public Key of the server interface",
+ "type": "string",
+ "example": "abcdef=="
+ },
+ "RoutingTable": {
+ "description": "the routing table",
+ "type": "string"
+ },
+ "SaveConfig": {
+ "description": "automatically persist config changes to the wgX.conf file",
+ "type": "boolean"
+ },
+ "TotalPeers": {
+ "type": "integer"
+ }
+ }
+ },
+ "model.LoginProviderInfo": {
+ "type": "object",
+ "properties": {
+ "CallbackUrl": {
+ "type": "string",
+ "example": "/auth/google/callback"
+ },
+ "Identifier": {
+ "type": "string",
+ "example": "google"
+ },
+ "Name": {
+ "type": "string",
+ "example": "Login with Google"
+ },
+ "ProviderUrl": {
+ "type": "string",
+ "example": "/auth/google/login"
+ }
+ }
+ },
+ "model.Peer": {
+ "type": "object",
+ "properties": {
+ "Addresses": {
+ "description": "the interface ip addresses",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "AllowedIPs": {
+ "description": "all allowed ip subnets, comma seperated",
+ "allOf": [
+ {
+ "$ref": "#/definitions/model.StringSliceConfigOption"
+ }
+ ]
+ },
+ "CheckAliveAddress": {
+ "description": "optional ip address or DNS name that is used for ping checks",
+ "type": "string"
+ },
+ "Disabled": {
+ "description": "flag that specifies if the peer is enabled (up) or not (down)",
+ "type": "boolean"
+ },
+ "DisabledReason": {
+ "description": "the reason why the peer has been disabled",
+ "type": "string"
+ },
+ "DisplayName": {
+ "description": "a nice display name/ description for the peer",
+ "type": "string"
+ },
+ "Dns": {
+ "description": "the dns server that should be set if the interface is up, comma separated",
+ "allOf": [
+ {
+ "$ref": "#/definitions/model.StringSliceConfigOption"
+ }
+ ]
+ },
+ "DnsSearch": {
+ "description": "the dns search option string that should be set if the interface is up, will be appended to DnsStr",
+ "allOf": [
+ {
+ "$ref": "#/definitions/model.StringSliceConfigOption"
+ }
+ ]
+ },
+ "Endpoint": {
+ "description": "the endpoint address",
+ "allOf": [
+ {
+ "$ref": "#/definitions/model.StringConfigOption"
+ }
+ ]
+ },
+ "EndpointPublicKey": {
+ "description": "the endpoint public key",
+ "allOf": [
+ {
+ "$ref": "#/definitions/model.StringConfigOption"
+ }
+ ]
+ },
+ "ExpiresAt": {
+ "description": "expiry dates for peers",
+ "type": "string"
+ },
+ "ExtraAllowedIPs": {
+ "description": "all allowed ip subnets on the server side, comma seperated",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "FirewallMark": {
+ "description": "a firewall mark",
+ "allOf": [
+ {
+ "$ref": "#/definitions/model.Int32ConfigOption"
+ }
+ ]
+ },
+ "Identifier": {
+ "description": "peer unique identifier",
+ "type": "string",
+ "example": "super_nice_peer"
+ },
+ "InterfaceIdentifier": {
+ "description": "the interface id",
+ "type": "string"
+ },
+ "Mode": {
+ "description": "the peer interface type (server, client, any)",
+ "type": "string"
+ },
+ "Mtu": {
+ "description": "the device MTU",
+ "allOf": [
+ {
+ "$ref": "#/definitions/model.IntConfigOption"
+ }
+ ]
+ },
+ "Notes": {
+ "description": "a note field for peers",
+ "type": "string"
+ },
+ "PersistentKeepalive": {
+ "description": "the persistent keep-alive interval",
+ "allOf": [
+ {
+ "$ref": "#/definitions/model.IntConfigOption"
+ }
+ ]
+ },
+ "PostDown": {
+ "description": "action that is executed after the device is down",
+ "allOf": [
+ {
+ "$ref": "#/definitions/model.StringConfigOption"
+ }
+ ]
+ },
+ "PostUp": {
+ "description": "action that is executed after the device is up",
+ "allOf": [
+ {
+ "$ref": "#/definitions/model.StringConfigOption"
+ }
+ ]
+ },
+ "PreDown": {
+ "description": "action that is executed before the device is down",
+ "allOf": [
+ {
+ "$ref": "#/definitions/model.StringConfigOption"
+ }
+ ]
+ },
+ "PreUp": {
+ "description": "action that is executed before the device is up",
+ "allOf": [
+ {
+ "$ref": "#/definitions/model.StringConfigOption"
+ }
+ ]
+ },
+ "PresharedKey": {
+ "description": "the pre-shared Key of the peer",
+ "type": "string"
+ },
+ "PrivateKey": {
+ "description": "private Key of the server peer",
+ "type": "string",
+ "example": "abcdef=="
+ },
+ "PublicKey": {
+ "description": "public Key of the server peer",
+ "type": "string",
+ "example": "abcdef=="
+ },
+ "RoutingTable": {
+ "description": "the routing table",
+ "allOf": [
+ {
+ "$ref": "#/definitions/model.StringConfigOption"
+ }
+ ]
+ },
+ "UserIdentifier": {
+ "description": "the owner",
+ "type": "string"
+ }
+ }
+ },
+ "model.SessionInfo": {
+ "type": "object",
+ "properties": {
+ "IsAdmin": {
+ "type": "boolean"
+ },
+ "LoggedIn": {
+ "type": "boolean"
+ },
+ "UserEmail": {
+ "type": "string"
+ },
+ "UserFirstname": {
+ "type": "string"
+ },
+ "UserIdentifier": {
+ "type": "string"
+ },
+ "UserLastname": {
+ "type": "string"
+ }
+ }
+ },
+ "model.StringConfigOption": {
+ "type": "object",
+ "properties": {
+ "Overridable": {
+ "type": "boolean"
+ },
+ "Value": {
+ "type": "string"
+ }
+ }
+ },
+ "model.StringSliceConfigOption": {
+ "type": "object",
+ "properties": {
+ "Overridable": {
+ "type": "boolean"
+ },
+ "Value": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "model.User": {
+ "type": "object",
+ "properties": {
+ "Department": {
+ "type": "string"
+ },
+ "Disabled": {
+ "description": "if this field is set, the user is disabled",
+ "type": "boolean"
+ },
+ "DisabledReason": {
+ "description": "the reason why the user has been disabled",
+ "type": "string"
+ },
+ "Email": {
+ "type": "string"
+ },
+ "Firstname": {
+ "type": "string"
+ },
+ "Identifier": {
+ "type": "string"
+ },
+ "IsAdmin": {
+ "type": "boolean"
+ },
+ "Lastname": {
+ "type": "string"
+ },
+ "Notes": {
+ "type": "string"
+ },
+ "Password": {
+ "type": "string"
+ },
+ "PeerCount": {
+ "type": "integer"
+ },
+ "Phone": {
+ "type": "string"
+ },
+ "ProviderName": {
+ "type": "string"
+ },
+ "Source": {
+ "type": "string"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/internal/app/api/core/assets/doc/v0_swagger.yaml b/internal/app/api/core/assets/doc/v0_swagger.yaml
new file mode 100644
index 0000000..5e921db
--- /dev/null
+++ b/internal/app/api/core/assets/doc/v0_swagger.yaml
@@ -0,0 +1,1041 @@
+basePath: /api/v0
+definitions:
+ model.Error:
+ properties:
+ Code:
+ type: integer
+ Message:
+ type: string
+ type: object
+ model.Int32ConfigOption:
+ properties:
+ Overridable:
+ type: boolean
+ Value:
+ type: integer
+ type: object
+ model.IntConfigOption:
+ properties:
+ Overridable:
+ type: boolean
+ Value:
+ type: integer
+ type: object
+ model.Interface:
+ properties:
+ Addresses:
+ description: the interface ip addresses
+ items:
+ type: string
+ type: array
+ Disabled:
+ description: flag that specifies if the interface is enabled (up) or not (down)
+ type: boolean
+ DisabledReason:
+ description: the reason why the interface has been disabled
+ type: string
+ DisplayName:
+ description: a nice display name/ description for the interface
+ type: string
+ Dns:
+ description: the dns server that should be set if the interface is up, comma
+ separated
+ items:
+ type: string
+ type: array
+ DnsSearch:
+ description: the dns search option string that should be set if the interface
+ is up, will be appended to DnsStr
+ items:
+ type: string
+ type: array
+ EnabledPeers:
+ type: integer
+ FirewallMark:
+ description: a firewall mark
+ type: integer
+ Identifier:
+ description: 'device name, for example: wg0'
+ example: wg0
+ type: string
+ ListenPort:
+ description: 'the listening port, for example: 51820'
+ type: integer
+ Mode:
+ description: the interface type, either 'server', 'client' or 'any'
+ example: server
+ type: string
+ Mtu:
+ description: the device MTU
+ type: integer
+ PeerDefAllowedIPs:
+ description: the default allowed IP string for the peer
+ items:
+ type: string
+ type: array
+ PeerDefDns:
+ description: the default dns server for the peer
+ items:
+ type: string
+ type: array
+ PeerDefDnsSearch:
+ description: the default dns search options for the peer
+ items:
+ type: string
+ type: array
+ PeerDefEndpoint:
+ description: the default endpoint for the peer
+ type: string
+ PeerDefFirewallMark:
+ description: default firewall mark
+ type: integer
+ PeerDefMtu:
+ description: the default device MTU
+ type: integer
+ PeerDefNetwork:
+ description: the default subnets from which peers will get their IP addresses,
+ comma seperated
+ items:
+ type: string
+ type: array
+ PeerDefPersistentKeepalive:
+ description: the default persistent keep-alive Value
+ type: integer
+ PeerDefPostDown:
+ description: default action that is executed after the device is down
+ type: string
+ PeerDefPostUp:
+ description: default action that is executed after the device is up
+ type: string
+ PeerDefPreDown:
+ description: default action that is executed before the device is down
+ type: string
+ PeerDefPreUp:
+ description: default action that is executed before the device is up
+ type: string
+ PeerDefRoutingTable:
+ description: the default routing table
+ type: string
+ PostDown:
+ description: action that is executed after the device is down
+ type: string
+ PostUp:
+ description: action that is executed after the device is up
+ type: string
+ PreDown:
+ description: action that is executed before the device is down
+ type: string
+ PreUp:
+ description: action that is executed before the device is up
+ type: string
+ PrivateKey:
+ description: private Key of the server interface
+ example: abcdef==
+ type: string
+ PublicKey:
+ description: public Key of the server interface
+ example: abcdef==
+ type: string
+ RoutingTable:
+ description: the routing table
+ type: string
+ SaveConfig:
+ description: automatically persist config changes to the wgX.conf file
+ type: boolean
+ TotalPeers:
+ type: integer
+ type: object
+ model.LoginProviderInfo:
+ properties:
+ CallbackUrl:
+ example: /auth/google/callback
+ type: string
+ Identifier:
+ example: google
+ type: string
+ Name:
+ example: Login with Google
+ type: string
+ ProviderUrl:
+ example: /auth/google/login
+ type: string
+ type: object
+ model.Peer:
+ properties:
+ Addresses:
+ description: the interface ip addresses
+ items:
+ type: string
+ type: array
+ AllowedIPs:
+ allOf:
+ - $ref: '#/definitions/model.StringSliceConfigOption'
+ description: all allowed ip subnets, comma seperated
+ CheckAliveAddress:
+ description: optional ip address or DNS name that is used for ping checks
+ type: string
+ Disabled:
+ description: flag that specifies if the peer is enabled (up) or not (down)
+ type: boolean
+ DisabledReason:
+ description: the reason why the peer has been disabled
+ type: string
+ DisplayName:
+ description: a nice display name/ description for the peer
+ type: string
+ Dns:
+ allOf:
+ - $ref: '#/definitions/model.StringSliceConfigOption'
+ description: the dns server that should be set if the interface is up, comma
+ separated
+ DnsSearch:
+ allOf:
+ - $ref: '#/definitions/model.StringSliceConfigOption'
+ description: the dns search option string that should be set if the interface
+ is up, will be appended to DnsStr
+ Endpoint:
+ allOf:
+ - $ref: '#/definitions/model.StringConfigOption'
+ description: the endpoint address
+ EndpointPublicKey:
+ allOf:
+ - $ref: '#/definitions/model.StringConfigOption'
+ description: the endpoint public key
+ ExpiresAt:
+ description: expiry dates for peers
+ type: string
+ ExtraAllowedIPs:
+ description: all allowed ip subnets on the server side, comma seperated
+ items:
+ type: string
+ type: array
+ FirewallMark:
+ allOf:
+ - $ref: '#/definitions/model.Int32ConfigOption'
+ description: a firewall mark
+ Identifier:
+ description: peer unique identifier
+ example: super_nice_peer
+ type: string
+ InterfaceIdentifier:
+ description: the interface id
+ type: string
+ Mode:
+ description: the peer interface type (server, client, any)
+ type: string
+ Mtu:
+ allOf:
+ - $ref: '#/definitions/model.IntConfigOption'
+ description: the device MTU
+ Notes:
+ description: a note field for peers
+ type: string
+ PersistentKeepalive:
+ allOf:
+ - $ref: '#/definitions/model.IntConfigOption'
+ description: the persistent keep-alive interval
+ PostDown:
+ allOf:
+ - $ref: '#/definitions/model.StringConfigOption'
+ description: action that is executed after the device is down
+ PostUp:
+ allOf:
+ - $ref: '#/definitions/model.StringConfigOption'
+ description: action that is executed after the device is up
+ PreDown:
+ allOf:
+ - $ref: '#/definitions/model.StringConfigOption'
+ description: action that is executed before the device is down
+ PreUp:
+ allOf:
+ - $ref: '#/definitions/model.StringConfigOption'
+ description: action that is executed before the device is up
+ PresharedKey:
+ description: the pre-shared Key of the peer
+ type: string
+ PrivateKey:
+ description: private Key of the server peer
+ example: abcdef==
+ type: string
+ PublicKey:
+ description: public Key of the server peer
+ example: abcdef==
+ type: string
+ RoutingTable:
+ allOf:
+ - $ref: '#/definitions/model.StringConfigOption'
+ description: the routing table
+ UserIdentifier:
+ description: the owner
+ type: string
+ type: object
+ model.SessionInfo:
+ properties:
+ IsAdmin:
+ type: boolean
+ LoggedIn:
+ type: boolean
+ UserEmail:
+ type: string
+ UserFirstname:
+ type: string
+ UserIdentifier:
+ type: string
+ UserLastname:
+ type: string
+ type: object
+ model.StringConfigOption:
+ properties:
+ Overridable:
+ type: boolean
+ Value:
+ type: string
+ type: object
+ model.StringSliceConfigOption:
+ properties:
+ Overridable:
+ type: boolean
+ Value:
+ items:
+ type: string
+ type: array
+ type: object
+ model.User:
+ properties:
+ Department:
+ type: string
+ Disabled:
+ description: if this field is set, the user is disabled
+ type: boolean
+ DisabledReason:
+ description: the reason why the user has been disabled
+ type: string
+ Email:
+ type: string
+ Firstname:
+ type: string
+ Identifier:
+ type: string
+ IsAdmin:
+ type: boolean
+ Lastname:
+ type: string
+ Notes:
+ type: string
+ Password:
+ type: string
+ PeerCount:
+ type: integer
+ Phone:
+ type: string
+ ProviderName:
+ type: string
+ Source:
+ type: string
+ type: object
+info:
+ contact:
+ name: WireGuard Portal Developers
+ url: https://github.com/h44z/wg-portal
+ description: WireGuard Portal API - a testing API endpoint
+ title: WireGuard Portal API
+ version: "0.0"
+paths:
+ /auth/{provider}/callback:
+ get:
+ operationId: auth_handleOauthCallbackGet
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ items:
+ $ref: '#/definitions/model.LoginProviderInfo'
+ type: array
+ summary: Handle the OAuth callback.
+ tags:
+ - Authentication
+ /auth/{provider}/init:
+ get:
+ operationId: auth_handleOauthInitiateGet
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ items:
+ $ref: '#/definitions/model.LoginProviderInfo'
+ type: array
+ summary: Initiate the OAuth login flow.
+ tags:
+ - Authentication
+ /auth/login:
+ post:
+ operationId: auth_handleLoginPost
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ items:
+ $ref: '#/definitions/model.LoginProviderInfo'
+ type: array
+ summary: Get all available external login providers.
+ tags:
+ - Authentication
+ /auth/logout:
+ get:
+ operationId: auth_handleLogoutGet
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ items:
+ $ref: '#/definitions/model.LoginProviderInfo'
+ type: array
+ summary: Get all available external login providers.
+ tags:
+ - Authentication
+ /auth/providers:
+ get:
+ operationId: auth_handleExternalLoginProvidersGet
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ items:
+ $ref: '#/definitions/model.LoginProviderInfo'
+ type: array
+ summary: Get all available external login providers.
+ tags:
+ - Authentication
+ /auth/session:
+ get:
+ operationId: auth_handleSessionInfoGet
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ items:
+ $ref: '#/definitions/model.SessionInfo'
+ type: array
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/model.Error'
+ summary: Get information about the currently logged-in user.
+ tags:
+ - Authentication
+ /config/frontend.js:
+ get:
+ operationId: config_handleConfigJsGet
+ produces:
+ - text/javascript
+ responses:
+ "200":
+ description: The JavaScript contents
+ schema:
+ type: string
+ summary: Get the dynamic frontend configuration javascript.
+ tags:
+ - Configuration
+ /csrf:
+ get:
+ operationId: base_handleCsrfGet
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ type: string
+ summary: Get a CSRF token for the current session.
+ tags:
+ - Security
+ /hostname:
+ get:
+ description: Nothing more to describe...
+ operationId: test_handleHostnameGet
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ type: string
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/model.Error'
+ summary: Get the current host name.
+ tags:
+ - Testing
+ /interface/{id}:
+ delete:
+ operationId: interfaces_handleDelete
+ parameters:
+ - description: The interface identifier
+ in: path
+ name: id
+ required: true
+ type: string
+ produces:
+ - application/json
+ responses:
+ "204":
+ description: No content if deletion was successful
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/model.Error'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/model.Error'
+ summary: Delete the interface record.
+ tags:
+ - Interface
+ put:
+ operationId: interfaces_handleUpdatePut
+ parameters:
+ - description: The interface identifier
+ in: path
+ name: id
+ required: true
+ type: string
+ - description: The interface data
+ in: body
+ name: request
+ required: true
+ schema:
+ $ref: '#/definitions/model.Interface'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/model.Interface'
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/model.Error'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/model.Error'
+ summary: Update the interface record.
+ tags:
+ - Interface
+ /interface/all:
+ get:
+ operationId: interfaces_handleAllGet
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ items:
+ $ref: '#/definitions/model.Interface'
+ type: array
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/model.Error'
+ summary: Get all available interfaces.
+ tags:
+ - Interface
+ /interface/config/{id}:
+ get:
+ operationId: interfaces_handleConfigGet
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ type: string
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/model.Error'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/model.Error'
+ summary: Get interface configuration as string.
+ tags:
+ - Interface
+ /interface/get/{id}:
+ get:
+ operationId: interfaces_handleSingleGet
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/model.Interface'
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/model.Error'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/model.Error'
+ summary: Get single interface.
+ tags:
+ - Interface
+ /interface/new:
+ post:
+ operationId: interfaces_handleCreatePost
+ parameters:
+ - description: The interface data
+ in: body
+ name: request
+ required: true
+ schema:
+ $ref: '#/definitions/model.Interface'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/model.Interface'
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/model.Error'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/model.Error'
+ summary: Create the new interface record.
+ tags:
+ - Interface
+ /interface/peers/{id}:
+ get:
+ operationId: interfaces_handlePeersGet
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ items:
+ $ref: '#/definitions/model.Peer'
+ type: array
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/model.Error'
+ summary: Get peers for the given interface.
+ tags:
+ - Interface
+ /interface/prepare:
+ get:
+ operationId: interfaces_handlePrepareGet
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/model.Interface'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/model.Error'
+ summary: Prepare a new interface.
+ tags:
+ - Interface
+ /now:
+ get:
+ description: Nothing more to describe...
+ operationId: test_handleCurrentTimeGet
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ type: string
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/model.Error'
+ summary: Get the current local time.
+ tags:
+ - Testing
+ /peer/{id}:
+ delete:
+ operationId: peers_handleDelete
+ parameters:
+ - description: The peer identifier
+ in: path
+ name: id
+ required: true
+ type: string
+ produces:
+ - application/json
+ responses:
+ "204":
+ description: No content if deletion was successful
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/model.Error'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/model.Error'
+ summary: Delete the peer record.
+ tags:
+ - Peer
+ get:
+ operationId: peers_handleSingleGet
+ parameters:
+ - description: The peer identifier
+ in: path
+ name: id
+ required: true
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/model.Peer'
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/model.Error'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/model.Error'
+ summary: Get peer for the given identifier.
+ tags:
+ - Peer
+ put:
+ operationId: peers_handleUpdatePut
+ parameters:
+ - description: The peer identifier
+ in: path
+ name: id
+ required: true
+ type: string
+ - description: The peer data
+ in: body
+ name: request
+ required: true
+ schema:
+ $ref: '#/definitions/model.Peer'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/model.Peer'
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/model.Error'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/model.Error'
+ summary: Update the given peer record.
+ tags:
+ - Peer
+ /peer/config-qr/{id}:
+ get:
+ operationId: peers_handleQrCodeGet
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ type: string
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/model.Error'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/model.Error'
+ summary: Get peer configuration as qr code.
+ tags:
+ - Peer
+ /peer/config/{id}:
+ get:
+ operationId: peers_handleConfigGet
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ type: string
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/model.Error'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/model.Error'
+ summary: Get peer configuration as string.
+ tags:
+ - Peer
+ /peer/iface/{iface}/all:
+ get:
+ operationId: peers_handleAllGet
+ parameters:
+ - description: The interface identifier
+ in: path
+ name: iface
+ required: true
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ items:
+ $ref: '#/definitions/model.Peer'
+ type: array
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/model.Error'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/model.Error'
+ summary: Get peers for the given interface.
+ tags:
+ - Peer
+ /peer/iface/{iface}/new:
+ post:
+ operationId: peers_handleCreatePost
+ parameters:
+ - description: The interface identifier
+ in: path
+ name: iface
+ required: true
+ type: string
+ - description: The peer data
+ in: body
+ name: request
+ required: true
+ schema:
+ $ref: '#/definitions/model.Peer'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/model.Peer'
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/model.Error'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/model.Error'
+ summary: Prepare a new peer for the given interface.
+ tags:
+ - Peer
+ /peer/iface/{iface}/prepare:
+ get:
+ operationId: peers_handlePrepareGet
+ parameters:
+ - description: The interface identifier
+ in: path
+ name: iface
+ required: true
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/model.Peer'
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/model.Error'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/model.Error'
+ summary: Prepare a new peer for the given interface.
+ tags:
+ - Peer
+ /user/{id}:
+ delete:
+ operationId: users_handleDelete
+ parameters:
+ - description: The user identifier
+ in: path
+ name: id
+ required: true
+ type: string
+ produces:
+ - application/json
+ responses:
+ "204":
+ description: No content if deletion was successful
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/model.Error'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/model.Error'
+ summary: Delete the user record.
+ tags:
+ - Users
+ get:
+ operationId: users_handleSingleGet
+ parameters:
+ - description: The user identifier
+ in: path
+ name: id
+ required: true
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/model.User'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/model.Error'
+ summary: Get a single user record.
+ tags:
+ - Users
+ put:
+ operationId: users_handleUpdatePut
+ parameters:
+ - description: The user identifier
+ in: path
+ name: id
+ required: true
+ type: string
+ - description: The user data
+ in: body
+ name: request
+ required: true
+ schema:
+ $ref: '#/definitions/model.User'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/model.User'
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/model.Error'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/model.Error'
+ summary: Update the user record.
+ tags:
+ - Users
+ /user/{id}/peers:
+ get:
+ operationId: users_handlePeersGet
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ items:
+ $ref: '#/definitions/model.Peer'
+ type: array
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/model.Error'
+ summary: Get peers for the given user.
+ tags:
+ - Users
+ /user/all:
+ get:
+ operationId: users_handleAllGet
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ items:
+ $ref: '#/definitions/model.User'
+ type: array
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/model.Error'
+ summary: Get all user records.
+ tags:
+ - Users
+ /user/new:
+ post:
+ operationId: users_handleCreatePost
+ parameters:
+ - description: The user data
+ in: body
+ name: request
+ required: true
+ schema:
+ $ref: '#/definitions/model.User'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/model.User'
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/model.Error'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/model.Error'
+ summary: Create the new user record.
+ tags:
+ - Users
+swagger: "2.0"
diff --git a/assets/fonts/FontAwesome.otf b/internal/app/api/core/assets/fonts/FontAwesome.otf
similarity index 100%
rename from assets/fonts/FontAwesome.otf
rename to internal/app/api/core/assets/fonts/FontAwesome.otf
diff --git a/assets/fonts/fa-brands-400.eot b/internal/app/api/core/assets/fonts/fa-brands-400.eot
similarity index 100%
rename from assets/fonts/fa-brands-400.eot
rename to internal/app/api/core/assets/fonts/fa-brands-400.eot
diff --git a/assets/fonts/fa-brands-400.svg b/internal/app/api/core/assets/fonts/fa-brands-400.svg
similarity index 100%
rename from assets/fonts/fa-brands-400.svg
rename to internal/app/api/core/assets/fonts/fa-brands-400.svg
diff --git a/assets/fonts/fa-brands-400.ttf b/internal/app/api/core/assets/fonts/fa-brands-400.ttf
similarity index 100%
rename from assets/fonts/fa-brands-400.ttf
rename to internal/app/api/core/assets/fonts/fa-brands-400.ttf
diff --git a/assets/fonts/fa-brands-400.woff b/internal/app/api/core/assets/fonts/fa-brands-400.woff
similarity index 100%
rename from assets/fonts/fa-brands-400.woff
rename to internal/app/api/core/assets/fonts/fa-brands-400.woff
diff --git a/assets/fonts/fa-brands-400.woff2 b/internal/app/api/core/assets/fonts/fa-brands-400.woff2
similarity index 100%
rename from assets/fonts/fa-brands-400.woff2
rename to internal/app/api/core/assets/fonts/fa-brands-400.woff2
diff --git a/assets/fonts/fa-regular-400.eot b/internal/app/api/core/assets/fonts/fa-regular-400.eot
similarity index 100%
rename from assets/fonts/fa-regular-400.eot
rename to internal/app/api/core/assets/fonts/fa-regular-400.eot
diff --git a/assets/fonts/fa-regular-400.svg b/internal/app/api/core/assets/fonts/fa-regular-400.svg
similarity index 100%
rename from assets/fonts/fa-regular-400.svg
rename to internal/app/api/core/assets/fonts/fa-regular-400.svg
diff --git a/assets/fonts/fa-regular-400.ttf b/internal/app/api/core/assets/fonts/fa-regular-400.ttf
similarity index 100%
rename from assets/fonts/fa-regular-400.ttf
rename to internal/app/api/core/assets/fonts/fa-regular-400.ttf
diff --git a/assets/fonts/fa-regular-400.woff b/internal/app/api/core/assets/fonts/fa-regular-400.woff
similarity index 100%
rename from assets/fonts/fa-regular-400.woff
rename to internal/app/api/core/assets/fonts/fa-regular-400.woff
diff --git a/assets/fonts/fa-regular-400.woff2 b/internal/app/api/core/assets/fonts/fa-regular-400.woff2
similarity index 100%
rename from assets/fonts/fa-regular-400.woff2
rename to internal/app/api/core/assets/fonts/fa-regular-400.woff2
diff --git a/assets/fonts/fa-solid-900.eot b/internal/app/api/core/assets/fonts/fa-solid-900.eot
similarity index 100%
rename from assets/fonts/fa-solid-900.eot
rename to internal/app/api/core/assets/fonts/fa-solid-900.eot
diff --git a/assets/fonts/fa-solid-900.svg b/internal/app/api/core/assets/fonts/fa-solid-900.svg
similarity index 100%
rename from assets/fonts/fa-solid-900.svg
rename to internal/app/api/core/assets/fonts/fa-solid-900.svg
diff --git a/assets/fonts/fa-solid-900.ttf b/internal/app/api/core/assets/fonts/fa-solid-900.ttf
similarity index 100%
rename from assets/fonts/fa-solid-900.ttf
rename to internal/app/api/core/assets/fonts/fa-solid-900.ttf
diff --git a/assets/fonts/fa-solid-900.woff b/internal/app/api/core/assets/fonts/fa-solid-900.woff
similarity index 100%
rename from assets/fonts/fa-solid-900.woff
rename to internal/app/api/core/assets/fonts/fa-solid-900.woff
diff --git a/assets/fonts/fa-solid-900.woff2 b/internal/app/api/core/assets/fonts/fa-solid-900.woff2
similarity index 100%
rename from assets/fonts/fa-solid-900.woff2
rename to internal/app/api/core/assets/fonts/fa-solid-900.woff2
diff --git a/internal/app/api/core/assets/fonts/font-awesome.min.css b/internal/app/api/core/assets/fonts/font-awesome.min.css
new file mode 100644
index 0000000..09de3b7
--- /dev/null
+++ b/internal/app/api/core/assets/fonts/font-awesome.min.css
@@ -0,0 +1,4 @@
+/*!
+ * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome
+ * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
+ */@font-face{font-family:'FontAwesome';src:url('fontawesome-webfont.eot?v=4.7.0');src:url('fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('fontawesome-webfont.woff?v=4.7.0') format('woff'),url('fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale} .fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%} .fa-2x{font-size:2em} .fa-3x{font-size:3em} .fa-4x{font-size:4em} .fa-5x{font-size:5em} .fa-fw{width:1.28571429em;text-align:center} .fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none} .fa-ul>li{position:relative} .fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center} .fa-li.fa-lg{left:-1.85714286em} .fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em} .fa-pull-left{float:left} .fa-pull-right{float:right} .fa.fa-pull-left{margin-right:.3em} .fa.fa-pull-right{margin-left:.3em} .pull-right{float:right} .pull-left{float:left} .fa.pull-left{margin-right:.3em} .fa.pull-right{margin-left:.3em} .fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear} .fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)} @-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)} 100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}} @keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)} 100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}} .fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)} .fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)} .fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)} .fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)} .fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)} :root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none} .fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle} .fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center} .fa-stack-1x{line-height:inherit} .fa-stack-2x{font-size:2em} .fa-inverse{color:#fff} .fa-glass:before{content:"\f000"} .fa-music:before{content:"\f001"} .fa-search:before{content:"\f002"} .fa-envelope-o:before{content:"\f003"} .fa-heart:before{content:"\f004"} .fa-star:before{content:"\f005"} .fa-star-o:before{content:"\f006"} .fa-user:before{content:"\f007"} .fa-film:before{content:"\f008"} .fa-th-large:before{content:"\f009"} .fa-th:before{content:"\f00a"} .fa-th-list:before{content:"\f00b"} .fa-check:before{content:"\f00c"} .fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"} .fa-search-plus:before{content:"\f00e"} .fa-search-minus:before{content:"\f010"} .fa-power-off:before{content:"\f011"} .fa-signal:before{content:"\f012"} .fa-gear:before,.fa-cog:before{content:"\f013"} .fa-trash-o:before{content:"\f014"} .fa-home:before{content:"\f015"} .fa-file-o:before{content:"\f016"} .fa-clock-o:before{content:"\f017"} .fa-road:before{content:"\f018"} .fa-download:before{content:"\f019"} .fa-arrow-circle-o-down:before{content:"\f01a"} .fa-arrow-circle-o-up:before{content:"\f01b"} .fa-inbox:before{content:"\f01c"} .fa-play-circle-o:before{content:"\f01d"} .fa-rotate-right:before,.fa-repeat:before{content:"\f01e"} .fa-refresh:before{content:"\f021"} .fa-list-alt:before{content:"\f022"} .fa-lock:before{content:"\f023"} .fa-flag:before{content:"\f024"} .fa-headphones:before{content:"\f025"} .fa-volume-off:before{content:"\f026"} .fa-volume-down:before{content:"\f027"} .fa-volume-up:before{content:"\f028"} .fa-qrcode:before{content:"\f029"} .fa-barcode:before{content:"\f02a"} .fa-tag:before{content:"\f02b"} .fa-tags:before{content:"\f02c"} .fa-book:before{content:"\f02d"} .fa-bookmark:before{content:"\f02e"} .fa-print:before{content:"\f02f"} .fa-camera:before{content:"\f030"} .fa-font:before{content:"\f031"} .fa-bold:before{content:"\f032"} .fa-italic:before{content:"\f033"} .fa-text-height:before{content:"\f034"} .fa-text-width:before{content:"\f035"} .fa-align-left:before{content:"\f036"} .fa-align-center:before{content:"\f037"} .fa-align-right:before{content:"\f038"} .fa-align-justify:before{content:"\f039"} .fa-list:before{content:"\f03a"} .fa-dedent:before,.fa-outdent:before{content:"\f03b"} .fa-indent:before{content:"\f03c"} .fa-video-camera:before{content:"\f03d"} .fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"} .fa-pencil:before{content:"\f040"} .fa-map-marker:before{content:"\f041"} .fa-adjust:before{content:"\f042"} .fa-tint:before{content:"\f043"} .fa-edit:before,.fa-pencil-square-o:before{content:"\f044"} .fa-share-square-o:before{content:"\f045"} .fa-check-square-o:before{content:"\f046"} .fa-arrows:before{content:"\f047"} .fa-step-backward:before{content:"\f048"} .fa-fast-backward:before{content:"\f049"} .fa-backward:before{content:"\f04a"} .fa-play:before{content:"\f04b"} .fa-pause:before{content:"\f04c"} .fa-stop:before{content:"\f04d"} .fa-forward:before{content:"\f04e"} .fa-fast-forward:before{content:"\f050"} .fa-step-forward:before{content:"\f051"} .fa-eject:before{content:"\f052"} .fa-chevron-left:before{content:"\f053"} .fa-chevron-right:before{content:"\f054"} .fa-plus-circle:before{content:"\f055"} .fa-minus-circle:before{content:"\f056"} .fa-times-circle:before{content:"\f057"} .fa-check-circle:before{content:"\f058"} .fa-question-circle:before{content:"\f059"} .fa-info-circle:before{content:"\f05a"} .fa-crosshairs:before{content:"\f05b"} .fa-times-circle-o:before{content:"\f05c"} .fa-check-circle-o:before{content:"\f05d"} .fa-ban:before{content:"\f05e"} .fa-arrow-left:before{content:"\f060"} .fa-arrow-right:before{content:"\f061"} .fa-arrow-up:before{content:"\f062"} .fa-arrow-down:before{content:"\f063"} .fa-mail-forward:before,.fa-share:before{content:"\f064"} .fa-expand:before{content:"\f065"} .fa-compress:before{content:"\f066"} .fa-plus:before{content:"\f067"} .fa-minus:before{content:"\f068"} .fa-asterisk:before{content:"\f069"} .fa-exclamation-circle:before{content:"\f06a"} .fa-gift:before{content:"\f06b"} .fa-leaf:before{content:"\f06c"} .fa-fire:before{content:"\f06d"} .fa-eye:before{content:"\f06e"} .fa-eye-slash:before{content:"\f070"} .fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"} .fa-plane:before{content:"\f072"} .fa-calendar:before{content:"\f073"} .fa-random:before{content:"\f074"} .fa-comment:before{content:"\f075"} .fa-magnet:before{content:"\f076"} .fa-chevron-up:before{content:"\f077"} .fa-chevron-down:before{content:"\f078"} .fa-retweet:before{content:"\f079"} .fa-shopping-cart:before{content:"\f07a"} .fa-folder:before{content:"\f07b"} .fa-folder-open:before{content:"\f07c"} .fa-arrows-v:before{content:"\f07d"} .fa-arrows-h:before{content:"\f07e"} .fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"} .fa-twitter-square:before{content:"\f081"} .fa-facebook-square:before{content:"\f082"} .fa-camera-retro:before{content:"\f083"} .fa-key:before{content:"\f084"} .fa-gears:before,.fa-cogs:before{content:"\f085"} .fa-comments:before{content:"\f086"} .fa-thumbs-o-up:before{content:"\f087"} .fa-thumbs-o-down:before{content:"\f088"} .fa-star-half:before{content:"\f089"} .fa-heart-o:before{content:"\f08a"} .fa-sign-out:before{content:"\f08b"} .fa-linkedin-square:before{content:"\f08c"} .fa-thumb-tack:before{content:"\f08d"} .fa-external-link:before{content:"\f08e"} .fa-sign-in:before{content:"\f090"} .fa-trophy:before{content:"\f091"} .fa-github-square:before{content:"\f092"} .fa-upload:before{content:"\f093"} .fa-lemon-o:before{content:"\f094"} .fa-phone:before{content:"\f095"} .fa-square-o:before{content:"\f096"} .fa-bookmark-o:before{content:"\f097"} .fa-phone-square:before{content:"\f098"} .fa-twitter:before{content:"\f099"} .fa-facebook-f:before,.fa-facebook:before{content:"\f09a"} .fa-github:before{content:"\f09b"} .fa-unlock:before{content:"\f09c"} .fa-credit-card:before{content:"\f09d"} .fa-feed:before,.fa-rss:before{content:"\f09e"} .fa-hdd-o:before{content:"\f0a0"} .fa-bullhorn:before{content:"\f0a1"} .fa-bell:before{content:"\f0f3"} .fa-certificate:before{content:"\f0a3"} .fa-hand-o-right:before{content:"\f0a4"} .fa-hand-o-left:before{content:"\f0a5"} .fa-hand-o-up:before{content:"\f0a6"} .fa-hand-o-down:before{content:"\f0a7"} .fa-arrow-circle-left:before{content:"\f0a8"} .fa-arrow-circle-right:before{content:"\f0a9"} .fa-arrow-circle-up:before{content:"\f0aa"} .fa-arrow-circle-down:before{content:"\f0ab"} .fa-globe:before{content:"\f0ac"} .fa-wrench:before{content:"\f0ad"} .fa-tasks:before{content:"\f0ae"} .fa-filter:before{content:"\f0b0"} .fa-briefcase:before{content:"\f0b1"} .fa-arrows-alt:before{content:"\f0b2"} .fa-group:before,.fa-users:before{content:"\f0c0"} .fa-chain:before,.fa-link:before{content:"\f0c1"} .fa-cloud:before{content:"\f0c2"} .fa-flask:before{content:"\f0c3"} .fa-cut:before,.fa-scissors:before{content:"\f0c4"} .fa-copy:before,.fa-files-o:before{content:"\f0c5"} .fa-paperclip:before{content:"\f0c6"} .fa-save:before,.fa-floppy-o:before{content:"\f0c7"} .fa-square:before{content:"\f0c8"} .fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"} .fa-list-ul:before{content:"\f0ca"} .fa-list-ol:before{content:"\f0cb"} .fa-strikethrough:before{content:"\f0cc"} .fa-underline:before{content:"\f0cd"} .fa-table:before{content:"\f0ce"} .fa-magic:before{content:"\f0d0"} .fa-truck:before{content:"\f0d1"} .fa-pinterest:before{content:"\f0d2"} .fa-pinterest-square:before{content:"\f0d3"} .fa-google-plus-square:before{content:"\f0d4"} .fa-google-plus:before{content:"\f0d5"} .fa-money:before{content:"\f0d6"} .fa-caret-down:before{content:"\f0d7"} .fa-caret-up:before{content:"\f0d8"} .fa-caret-left:before{content:"\f0d9"} .fa-caret-right:before{content:"\f0da"} .fa-columns:before{content:"\f0db"} .fa-unsorted:before,.fa-sort:before{content:"\f0dc"} .fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"} .fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"} .fa-envelope:before{content:"\f0e0"} .fa-linkedin:before{content:"\f0e1"} .fa-rotate-left:before,.fa-undo:before{content:"\f0e2"} .fa-legal:before,.fa-gavel:before{content:"\f0e3"} .fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"} .fa-comment-o:before{content:"\f0e5"} .fa-comments-o:before{content:"\f0e6"} .fa-flash:before,.fa-bolt:before{content:"\f0e7"} .fa-sitemap:before{content:"\f0e8"} .fa-umbrella:before{content:"\f0e9"} .fa-paste:before,.fa-clipboard:before{content:"\f0ea"} .fa-lightbulb-o:before{content:"\f0eb"} .fa-exchange:before{content:"\f0ec"} .fa-cloud-download:before{content:"\f0ed"} .fa-cloud-upload:before{content:"\f0ee"} .fa-user-md:before{content:"\f0f0"} .fa-stethoscope:before{content:"\f0f1"} .fa-suitcase:before{content:"\f0f2"} .fa-bell-o:before{content:"\f0a2"} .fa-coffee:before{content:"\f0f4"} .fa-cutlery:before{content:"\f0f5"} .fa-file-text-o:before{content:"\f0f6"} .fa-building-o:before{content:"\f0f7"} .fa-hospital-o:before{content:"\f0f8"} .fa-ambulance:before{content:"\f0f9"} .fa-medkit:before{content:"\f0fa"} .fa-fighter-jet:before{content:"\f0fb"} .fa-beer:before{content:"\f0fc"} .fa-h-square:before{content:"\f0fd"} .fa-plus-square:before{content:"\f0fe"} .fa-angle-double-left:before{content:"\f100"} .fa-angle-double-right:before{content:"\f101"} .fa-angle-double-up:before{content:"\f102"} .fa-angle-double-down:before{content:"\f103"} .fa-angle-left:before{content:"\f104"} .fa-angle-right:before{content:"\f105"} .fa-angle-up:before{content:"\f106"} .fa-angle-down:before{content:"\f107"} .fa-desktop:before{content:"\f108"} .fa-laptop:before{content:"\f109"} .fa-tablet:before{content:"\f10a"} .fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"} .fa-circle-o:before{content:"\f10c"} .fa-quote-left:before{content:"\f10d"} .fa-quote-right:before{content:"\f10e"} .fa-spinner:before{content:"\f110"} .fa-circle:before{content:"\f111"} .fa-mail-reply:before,.fa-reply:before{content:"\f112"} .fa-github-alt:before{content:"\f113"} .fa-folder-o:before{content:"\f114"} .fa-folder-open-o:before{content:"\f115"} .fa-smile-o:before{content:"\f118"} .fa-frown-o:before{content:"\f119"} .fa-meh-o:before{content:"\f11a"} .fa-gamepad:before{content:"\f11b"} .fa-keyboard-o:before{content:"\f11c"} .fa-flag-o:before{content:"\f11d"} .fa-flag-checkered:before{content:"\f11e"} .fa-terminal:before{content:"\f120"} .fa-code:before{content:"\f121"} .fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"} .fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"} .fa-location-arrow:before{content:"\f124"} .fa-crop:before{content:"\f125"} .fa-code-fork:before{content:"\f126"} .fa-unlink:before,.fa-chain-broken:before{content:"\f127"} .fa-question:before{content:"\f128"} .fa-info:before{content:"\f129"} .fa-exclamation:before{content:"\f12a"} .fa-superscript:before{content:"\f12b"} .fa-subscript:before{content:"\f12c"} .fa-eraser:before{content:"\f12d"} .fa-puzzle-piece:before{content:"\f12e"} .fa-microphone:before{content:"\f130"} .fa-microphone-slash:before{content:"\f131"} .fa-shield:before{content:"\f132"} .fa-calendar-o:before{content:"\f133"} .fa-fire-extinguisher:before{content:"\f134"} .fa-rocket:before{content:"\f135"} .fa-maxcdn:before{content:"\f136"} .fa-chevron-circle-left:before{content:"\f137"} .fa-chevron-circle-right:before{content:"\f138"} .fa-chevron-circle-up:before{content:"\f139"} .fa-chevron-circle-down:before{content:"\f13a"} .fa-html5:before{content:"\f13b"} .fa-css3:before{content:"\f13c"} .fa-anchor:before{content:"\f13d"} .fa-unlock-alt:before{content:"\f13e"} .fa-bullseye:before{content:"\f140"} .fa-ellipsis-h:before{content:"\f141"} .fa-ellipsis-v:before{content:"\f142"} .fa-rss-square:before{content:"\f143"} .fa-play-circle:before{content:"\f144"} .fa-ticket:before{content:"\f145"} .fa-minus-square:before{content:"\f146"} .fa-minus-square-o:before{content:"\f147"} .fa-level-up:before{content:"\f148"} .fa-level-down:before{content:"\f149"} .fa-check-square:before{content:"\f14a"} .fa-pencil-square:before{content:"\f14b"} .fa-external-link-square:before{content:"\f14c"} .fa-share-square:before{content:"\f14d"} .fa-compass:before{content:"\f14e"} .fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"} .fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"} .fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"} .fa-euro:before,.fa-eur:before{content:"\f153"} .fa-gbp:before{content:"\f154"} .fa-dollar:before,.fa-usd:before{content:"\f155"} .fa-rupee:before,.fa-inr:before{content:"\f156"} .fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"} .fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"} .fa-won:before,.fa-krw:before{content:"\f159"} .fa-bitcoin:before,.fa-btc:before{content:"\f15a"} .fa-file:before{content:"\f15b"} .fa-file-text:before{content:"\f15c"} .fa-sort-alpha-asc:before{content:"\f15d"} .fa-sort-alpha-desc:before{content:"\f15e"} .fa-sort-amount-asc:before{content:"\f160"} .fa-sort-amount-desc:before{content:"\f161"} .fa-sort-numeric-asc:before{content:"\f162"} .fa-sort-numeric-desc:before{content:"\f163"} .fa-thumbs-up:before{content:"\f164"} .fa-thumbs-down:before{content:"\f165"} .fa-youtube-square:before{content:"\f166"} .fa-youtube:before{content:"\f167"} .fa-xing:before{content:"\f168"} .fa-xing-square:before{content:"\f169"} .fa-youtube-play:before{content:"\f16a"} .fa-dropbox:before{content:"\f16b"} .fa-stack-overflow:before{content:"\f16c"} .fa-instagram:before{content:"\f16d"} .fa-flickr:before{content:"\f16e"} .fa-adn:before{content:"\f170"} .fa-bitbucket:before{content:"\f171"} .fa-bitbucket-square:before{content:"\f172"} .fa-tumblr:before{content:"\f173"} .fa-tumblr-square:before{content:"\f174"} .fa-long-arrow-down:before{content:"\f175"} .fa-long-arrow-up:before{content:"\f176"} .fa-long-arrow-left:before{content:"\f177"} .fa-long-arrow-right:before{content:"\f178"} .fa-apple:before{content:"\f179"} .fa-windows:before{content:"\f17a"} .fa-android:before{content:"\f17b"} .fa-linux:before{content:"\f17c"} .fa-dribbble:before{content:"\f17d"} .fa-skype:before{content:"\f17e"} .fa-foursquare:before{content:"\f180"} .fa-trello:before{content:"\f181"} .fa-female:before{content:"\f182"} .fa-male:before{content:"\f183"} .fa-gittip:before,.fa-gratipay:before{content:"\f184"} .fa-sun-o:before{content:"\f185"} .fa-moon-o:before{content:"\f186"} .fa-archive:before{content:"\f187"} .fa-bug:before{content:"\f188"} .fa-vk:before{content:"\f189"} .fa-weibo:before{content:"\f18a"} .fa-renren:before{content:"\f18b"} .fa-pagelines:before{content:"\f18c"} .fa-stack-exchange:before{content:"\f18d"} .fa-arrow-circle-o-right:before{content:"\f18e"} .fa-arrow-circle-o-left:before{content:"\f190"} .fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"} .fa-dot-circle-o:before{content:"\f192"} .fa-wheelchair:before{content:"\f193"} .fa-vimeo-square:before{content:"\f194"} .fa-turkish-lira:before,.fa-try:before{content:"\f195"} .fa-plus-square-o:before{content:"\f196"} .fa-space-shuttle:before{content:"\f197"} .fa-slack:before{content:"\f198"} .fa-envelope-square:before{content:"\f199"} .fa-wordpress:before{content:"\f19a"} .fa-openid:before{content:"\f19b"} .fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"} .fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"} .fa-yahoo:before{content:"\f19e"} .fa-google:before{content:"\f1a0"} .fa-reddit:before{content:"\f1a1"} .fa-reddit-square:before{content:"\f1a2"} .fa-stumbleupon-circle:before{content:"\f1a3"} .fa-stumbleupon:before{content:"\f1a4"} .fa-delicious:before{content:"\f1a5"} .fa-digg:before{content:"\f1a6"} .fa-pied-piper-pp:before{content:"\f1a7"} .fa-pied-piper-alt:before{content:"\f1a8"} .fa-drupal:before{content:"\f1a9"} .fa-joomla:before{content:"\f1aa"} .fa-language:before{content:"\f1ab"} .fa-fax:before{content:"\f1ac"} .fa-building:before{content:"\f1ad"} .fa-child:before{content:"\f1ae"} .fa-paw:before{content:"\f1b0"} .fa-spoon:before{content:"\f1b1"} .fa-cube:before{content:"\f1b2"} .fa-cubes:before{content:"\f1b3"} .fa-behance:before{content:"\f1b4"} .fa-behance-square:before{content:"\f1b5"} .fa-steam:before{content:"\f1b6"} .fa-steam-square:before{content:"\f1b7"} .fa-recycle:before{content:"\f1b8"} .fa-automobile:before,.fa-car:before{content:"\f1b9"} .fa-cab:before,.fa-taxi:before{content:"\f1ba"} .fa-tree:before{content:"\f1bb"} .fa-spotify:before{content:"\f1bc"} .fa-deviantart:before{content:"\f1bd"} .fa-soundcloud:before{content:"\f1be"} .fa-database:before{content:"\f1c0"} .fa-file-pdf-o:before{content:"\f1c1"} .fa-file-word-o:before{content:"\f1c2"} .fa-file-excel-o:before{content:"\f1c3"} .fa-file-powerpoint-o:before{content:"\f1c4"} .fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"} .fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"} .fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"} .fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"} .fa-file-code-o:before{content:"\f1c9"} .fa-vine:before{content:"\f1ca"} .fa-codepen:before{content:"\f1cb"} .fa-jsfiddle:before{content:"\f1cc"} .fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"} .fa-circle-o-notch:before{content:"\f1ce"} .fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"} .fa-ge:before,.fa-empire:before{content:"\f1d1"} .fa-git-square:before{content:"\f1d2"} .fa-git:before{content:"\f1d3"} .fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"} .fa-tencent-weibo:before{content:"\f1d5"} .fa-qq:before{content:"\f1d6"} .fa-wechat:before,.fa-weixin:before{content:"\f1d7"} .fa-send:before,.fa-paper-plane:before{content:"\f1d8"} .fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"} .fa-history:before{content:"\f1da"} .fa-circle-thin:before{content:"\f1db"} .fa-header:before{content:"\f1dc"} .fa-paragraph:before{content:"\f1dd"} .fa-sliders:before{content:"\f1de"} .fa-share-alt:before{content:"\f1e0"} .fa-share-alt-square:before{content:"\f1e1"} .fa-bomb:before{content:"\f1e2"} .fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"} .fa-tty:before{content:"\f1e4"} .fa-binoculars:before{content:"\f1e5"} .fa-plug:before{content:"\f1e6"} .fa-slideshare:before{content:"\f1e7"} .fa-twitch:before{content:"\f1e8"} .fa-yelp:before{content:"\f1e9"} .fa-newspaper-o:before{content:"\f1ea"} .fa-wifi:before{content:"\f1eb"} .fa-calculator:before{content:"\f1ec"} .fa-paypal:before{content:"\f1ed"} .fa-google-wallet:before{content:"\f1ee"} .fa-cc-visa:before{content:"\f1f0"} .fa-cc-mastercard:before{content:"\f1f1"} .fa-cc-discover:before{content:"\f1f2"} .fa-cc-amex:before{content:"\f1f3"} .fa-cc-paypal:before{content:"\f1f4"} .fa-cc-stripe:before{content:"\f1f5"} .fa-bell-slash:before{content:"\f1f6"} .fa-bell-slash-o:before{content:"\f1f7"} .fa-trash:before{content:"\f1f8"} .fa-copyright:before{content:"\f1f9"} .fa-at:before{content:"\f1fa"} .fa-eyedropper:before{content:"\f1fb"} .fa-paint-brush:before{content:"\f1fc"} .fa-birthday-cake:before{content:"\f1fd"} .fa-area-chart:before{content:"\f1fe"} .fa-pie-chart:before{content:"\f200"} .fa-line-chart:before{content:"\f201"} .fa-lastfm:before{content:"\f202"} .fa-lastfm-square:before{content:"\f203"} .fa-toggle-off:before{content:"\f204"} .fa-toggle-on:before{content:"\f205"} .fa-bicycle:before{content:"\f206"} .fa-bus:before{content:"\f207"} .fa-ioxhost:before{content:"\f208"} .fa-angellist:before{content:"\f209"} .fa-cc:before{content:"\f20a"} .fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"} .fa-meanpath:before{content:"\f20c"} .fa-buysellads:before{content:"\f20d"} .fa-connectdevelop:before{content:"\f20e"} .fa-dashcube:before{content:"\f210"} .fa-forumbee:before{content:"\f211"} .fa-leanpub:before{content:"\f212"} .fa-sellsy:before{content:"\f213"} .fa-shirtsinbulk:before{content:"\f214"} .fa-simplybuilt:before{content:"\f215"} .fa-skyatlas:before{content:"\f216"} .fa-cart-plus:before{content:"\f217"} .fa-cart-arrow-down:before{content:"\f218"} .fa-diamond:before{content:"\f219"} .fa-ship:before{content:"\f21a"} .fa-user-secret:before{content:"\f21b"} .fa-motorcycle:before{content:"\f21c"} .fa-street-view:before{content:"\f21d"} .fa-heartbeat:before{content:"\f21e"} .fa-venus:before{content:"\f221"} .fa-mars:before{content:"\f222"} .fa-mercury:before{content:"\f223"} .fa-intersex:before,.fa-transgender:before{content:"\f224"} .fa-transgender-alt:before{content:"\f225"} .fa-venus-double:before{content:"\f226"} .fa-mars-double:before{content:"\f227"} .fa-venus-mars:before{content:"\f228"} .fa-mars-stroke:before{content:"\f229"} .fa-mars-stroke-v:before{content:"\f22a"} .fa-mars-stroke-h:before{content:"\f22b"} .fa-neuter:before{content:"\f22c"} .fa-genderless:before{content:"\f22d"} .fa-facebook-official:before{content:"\f230"} .fa-pinterest-p:before{content:"\f231"} .fa-whatsapp:before{content:"\f232"} .fa-server:before{content:"\f233"} .fa-user-plus:before{content:"\f234"} .fa-user-times:before{content:"\f235"} .fa-hotel:before,.fa-bed:before{content:"\f236"} .fa-viacoin:before{content:"\f237"} .fa-train:before{content:"\f238"} .fa-subway:before{content:"\f239"} .fa-medium:before{content:"\f23a"} .fa-yc:before,.fa-y-combinator:before{content:"\f23b"} .fa-optin-monster:before{content:"\f23c"} .fa-opencart:before{content:"\f23d"} .fa-expeditedssl:before{content:"\f23e"} .fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"} .fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"} .fa-battery-2:before,.fa-battery-half:before{content:"\f242"} .fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"} .fa-battery-0:before,.fa-battery-empty:before{content:"\f244"} .fa-mouse-pointer:before{content:"\f245"} .fa-i-cursor:before{content:"\f246"} .fa-object-group:before{content:"\f247"} .fa-object-ungroup:before{content:"\f248"} .fa-sticky-note:before{content:"\f249"} .fa-sticky-note-o:before{content:"\f24a"} .fa-cc-jcb:before{content:"\f24b"} .fa-cc-diners-club:before{content:"\f24c"} .fa-clone:before{content:"\f24d"} .fa-balance-scale:before{content:"\f24e"} .fa-hourglass-o:before{content:"\f250"} .fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"} .fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"} .fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"} .fa-hourglass:before{content:"\f254"} .fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"} .fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"} .fa-hand-scissors-o:before{content:"\f257"} .fa-hand-lizard-o:before{content:"\f258"} .fa-hand-spock-o:before{content:"\f259"} .fa-hand-pointer-o:before{content:"\f25a"} .fa-hand-peace-o:before{content:"\f25b"} .fa-trademark:before{content:"\f25c"} .fa-registered:before{content:"\f25d"} .fa-creative-commons:before{content:"\f25e"} .fa-gg:before{content:"\f260"} .fa-gg-circle:before{content:"\f261"} .fa-tripadvisor:before{content:"\f262"} .fa-odnoklassniki:before{content:"\f263"} .fa-odnoklassniki-square:before{content:"\f264"} .fa-get-pocket:before{content:"\f265"} .fa-wikipedia-w:before{content:"\f266"} .fa-safari:before{content:"\f267"} .fa-chrome:before{content:"\f268"} .fa-firefox:before{content:"\f269"} .fa-opera:before{content:"\f26a"} .fa-internet-explorer:before{content:"\f26b"} .fa-tv:before,.fa-television:before{content:"\f26c"} .fa-contao:before{content:"\f26d"} .fa-500px:before{content:"\f26e"} .fa-amazon:before{content:"\f270"} .fa-calendar-plus-o:before{content:"\f271"} .fa-calendar-minus-o:before{content:"\f272"} .fa-calendar-times-o:before{content:"\f273"} .fa-calendar-check-o:before{content:"\f274"} .fa-industry:before{content:"\f275"} .fa-map-pin:before{content:"\f276"} .fa-map-signs:before{content:"\f277"} .fa-map-o:before{content:"\f278"} .fa-map:before{content:"\f279"} .fa-commenting:before{content:"\f27a"} .fa-commenting-o:before{content:"\f27b"} .fa-houzz:before{content:"\f27c"} .fa-vimeo:before{content:"\f27d"} .fa-black-tie:before{content:"\f27e"} .fa-fonticons:before{content:"\f280"} .fa-reddit-alien:before{content:"\f281"} .fa-edge:before{content:"\f282"} .fa-credit-card-alt:before{content:"\f283"} .fa-codiepie:before{content:"\f284"} .fa-modx:before{content:"\f285"} .fa-fort-awesome:before{content:"\f286"} .fa-usb:before{content:"\f287"} .fa-product-hunt:before{content:"\f288"} .fa-mixcloud:before{content:"\f289"} .fa-scribd:before{content:"\f28a"} .fa-pause-circle:before{content:"\f28b"} .fa-pause-circle-o:before{content:"\f28c"} .fa-stop-circle:before{content:"\f28d"} .fa-stop-circle-o:before{content:"\f28e"} .fa-shopping-bag:before{content:"\f290"} .fa-shopping-basket:before{content:"\f291"} .fa-hashtag:before{content:"\f292"} .fa-bluetooth:before{content:"\f293"} .fa-bluetooth-b:before{content:"\f294"} .fa-percent:before{content:"\f295"} .fa-gitlab:before{content:"\f296"} .fa-wpbeginner:before{content:"\f297"} .fa-wpforms:before{content:"\f298"} .fa-envira:before{content:"\f299"} .fa-universal-access:before{content:"\f29a"} .fa-wheelchair-alt:before{content:"\f29b"} .fa-question-circle-o:before{content:"\f29c"} .fa-blind:before{content:"\f29d"} .fa-audio-description:before{content:"\f29e"} .fa-volume-control-phone:before{content:"\f2a0"} .fa-braille:before{content:"\f2a1"} .fa-assistive-listening-systems:before{content:"\f2a2"} .fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"} .fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"} .fa-glide:before{content:"\f2a5"} .fa-glide-g:before{content:"\f2a6"} .fa-signing:before,.fa-sign-language:before{content:"\f2a7"} .fa-low-vision:before{content:"\f2a8"} .fa-viadeo:before{content:"\f2a9"} .fa-viadeo-square:before{content:"\f2aa"} .fa-snapchat:before{content:"\f2ab"} .fa-snapchat-ghost:before{content:"\f2ac"} .fa-snapchat-square:before{content:"\f2ad"} .fa-pied-piper:before{content:"\f2ae"} .fa-first-order:before{content:"\f2b0"} .fa-yoast:before{content:"\f2b1"} .fa-themeisle:before{content:"\f2b2"} .fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"} .fa-fa:before,.fa-font-awesome:before{content:"\f2b4"} .fa-handshake-o:before{content:"\f2b5"} .fa-envelope-open:before{content:"\f2b6"} .fa-envelope-open-o:before{content:"\f2b7"} .fa-linode:before{content:"\f2b8"} .fa-address-book:before{content:"\f2b9"} .fa-address-book-o:before{content:"\f2ba"} .fa-vcard:before,.fa-address-card:before{content:"\f2bb"} .fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"} .fa-user-circle:before{content:"\f2bd"} .fa-user-circle-o:before{content:"\f2be"} .fa-user-o:before{content:"\f2c0"} .fa-id-badge:before{content:"\f2c1"} .fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"} .fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"} .fa-quora:before{content:"\f2c4"} .fa-free-code-camp:before{content:"\f2c5"} .fa-telegram:before{content:"\f2c6"} .fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"} .fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"} .fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"} .fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"} .fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"} .fa-shower:before{content:"\f2cc"} .fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"} .fa-podcast:before{content:"\f2ce"} .fa-window-maximize:before{content:"\f2d0"} .fa-window-minimize:before{content:"\f2d1"} .fa-window-restore:before{content:"\f2d2"} .fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"} .fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"} .fa-bandcamp:before{content:"\f2d5"} .fa-grav:before{content:"\f2d6"} .fa-etsy:before{content:"\f2d7"} .fa-imdb:before{content:"\f2d8"} .fa-ravelry:before{content:"\f2d9"} .fa-eercast:before{content:"\f2da"} .fa-microchip:before{content:"\f2db"} .fa-snowflake-o:before{content:"\f2dc"} .fa-superpowers:before{content:"\f2dd"} .fa-wpexplorer:before{content:"\f2de"} .fa-meetup:before{content:"\f2e0"} .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0} .sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}
diff --git a/assets/fonts/fontawesome-all.min.css b/internal/app/api/core/assets/fonts/fontawesome-all.min.css
similarity index 97%
rename from assets/fonts/fontawesome-all.min.css
rename to internal/app/api/core/assets/fonts/fontawesome-all.min.css
index 27e7ddd..9367cb6 100644
--- a/assets/fonts/fontawesome-all.min.css
+++ b/internal/app/api/core/assets/fonts/fontawesome-all.min.css
@@ -2,4 +2,4 @@
* Font Awesome Free 5.12.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
*/
-.fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adobe:before{content:"\f778"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asterisk:before{content:"\f069"}.fa-asymmetrik:before{content:"\f372"}.fa-at:before{content:"\f1fa"}.fa-atlas:before{content:"\f558"}.fa-atlassian:before{content:"\f77b"}.fa-atom:before{content:"\f5d2"}.fa-audible:before{content:"\f373"}.fa-audio-description:before{content:"\f29e"}.fa-autoprefixer:before{content:"\f41c"}.fa-avianex:before{content:"\f374"}.fa-aviato:before{content:"\f421"}.fa-award:before{content:"\f559"}.fa-aws:before{content:"\f375"}.fa-baby:before{content:"\f77c"}.fa-baby-carriage:before{content:"\f77d"}.fa-backspace:before{content:"\f55a"}.fa-backward:before{content:"\f04a"}.fa-bacon:before{content:"\f7e5"}.fa-bahai:before{content:"\f666"}.fa-balance-scale:before{content:"\f24e"}.fa-balance-scale-left:before{content:"\f515"}.fa-balance-scale-right:before{content:"\f516"}.fa-ban:before{content:"\f05e"}.fa-band-aid:before{content:"\f462"}.fa-bandcamp:before{content:"\f2d5"}.fa-barcode:before{content:"\f02a"}.fa-bars:before{content:"\f0c9"}.fa-baseball-ball:before{content:"\f433"}.fa-basketball-ball:before{content:"\f434"}.fa-bath:before{content:"\f2cd"}.fa-battery-empty:before{content:"\f244"}.fa-battery-full:before{content:"\f240"}.fa-battery-half:before{content:"\f242"}.fa-battery-quarter:before{content:"\f243"}.fa-battery-three-quarters:before{content:"\f241"}.fa-battle-net:before{content:"\f835"}.fa-bed:before{content:"\f236"}.fa-beer:before{content:"\f0fc"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-bell:before{content:"\f0f3"}.fa-bell-slash:before{content:"\f1f6"}.fa-bezier-curve:before{content:"\f55b"}.fa-bible:before{content:"\f647"}.fa-bicycle:before{content:"\f206"}.fa-biking:before{content:"\f84a"}.fa-bimobject:before{content:"\f378"}.fa-binoculars:before{content:"\f1e5"}.fa-biohazard:before{content:"\f780"}.fa-birthday-cake:before{content:"\f1fd"}.fa-bitbucket:before{content:"\f171"}.fa-bitcoin:before{content:"\f379"}.fa-bity:before{content:"\f37a"}.fa-black-tie:before{content:"\f27e"}.fa-blackberry:before{content:"\f37b"}.fa-blender:before{content:"\f517"}.fa-blender-phone:before{content:"\f6b6"}.fa-blind:before{content:"\f29d"}.fa-blog:before{content:"\f781"}.fa-blogger:before{content:"\f37c"}.fa-blogger-b:before{content:"\f37d"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-bold:before{content:"\f032"}.fa-bolt:before{content:"\f0e7"}.fa-bomb:before{content:"\f1e2"}.fa-bone:before{content:"\f5d7"}.fa-bong:before{content:"\f55c"}.fa-book:before{content:"\f02d"}.fa-book-dead:before{content:"\f6b7"}.fa-book-medical:before{content:"\f7e6"}.fa-book-open:before{content:"\f518"}.fa-book-reader:before{content:"\f5da"}.fa-bookmark:before{content:"\f02e"}.fa-bootstrap:before{content:"\f836"}.fa-border-all:before{content:"\f84c"}.fa-border-none:before{content:"\f850"}.fa-border-style:before{content:"\f853"}.fa-bowling-ball:before{content:"\f436"}.fa-box:before{content:"\f466"}.fa-box-open:before{content:"\f49e"}.fa-boxes:before{content:"\f468"}.fa-braille:before{content:"\f2a1"}.fa-brain:before{content:"\f5dc"}.fa-bread-slice:before{content:"\f7ec"}.fa-briefcase:before{content:"\f0b1"}.fa-briefcase-medical:before{content:"\f469"}.fa-broadcast-tower:before{content:"\f519"}.fa-broom:before{content:"\f51a"}.fa-brush:before{content:"\f55d"}.fa-btc:before{content:"\f15a"}.fa-buffer:before{content:"\f837"}.fa-bug:before{content:"\f188"}.fa-building:before{content:"\f1ad"}.fa-bullhorn:before{content:"\f0a1"}.fa-bullseye:before{content:"\f140"}.fa-burn:before{content:"\f46a"}.fa-buromobelexperte:before{content:"\f37f"}.fa-bus:before{content:"\f207"}.fa-bus-alt:before{content:"\f55e"}.fa-business-time:before{content:"\f64a"}.fa-buy-n-large:before{content:"\f8a6"}.fa-buysellads:before{content:"\f20d"}.fa-calculator:before{content:"\f1ec"}.fa-calendar:before{content:"\f133"}.fa-calendar-alt:before{content:"\f073"}.fa-calendar-check:before{content:"\f274"}.fa-calendar-day:before{content:"\f783"}.fa-calendar-minus:before{content:"\f272"}.fa-calendar-plus:before{content:"\f271"}.fa-calendar-times:before{content:"\f273"}.fa-calendar-week:before{content:"\f784"}.fa-camera:before{content:"\f030"}.fa-camera-retro:before{content:"\f083"}.fa-campground:before{content:"\f6bb"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-candy-cane:before{content:"\f786"}.fa-cannabis:before{content:"\f55f"}.fa-capsules:before{content:"\f46b"}.fa-car:before{content:"\f1b9"}.fa-car-alt:before{content:"\f5de"}.fa-car-battery:before{content:"\f5df"}.fa-car-crash:before{content:"\f5e1"}.fa-car-side:before{content:"\f5e4"}.fa-caravan:before{content:"\f8ff"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-caret-square-down:before{content:"\f150"}.fa-caret-square-left:before{content:"\f191"}.fa-caret-square-right:before{content:"\f152"}.fa-caret-square-up:before{content:"\f151"}.fa-caret-up:before{content:"\f0d8"}.fa-carrot:before{content:"\f787"}.fa-cart-arrow-down:before{content:"\f218"}.fa-cart-plus:before{content:"\f217"}.fa-cash-register:before{content:"\f788"}.fa-cat:before{content:"\f6be"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-apple-pay:before{content:"\f416"}.fa-cc-diners-club:before{content:"\f24c"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-cc-visa:before{content:"\f1f0"}.fa-centercode:before{content:"\f380"}.fa-centos:before{content:"\f789"}.fa-certificate:before{content:"\f0a3"}.fa-chair:before{content:"\f6c0"}.fa-chalkboard:before{content:"\f51b"}.fa-chalkboard-teacher:before{content:"\f51c"}.fa-charging-station:before{content:"\f5e7"}.fa-chart-area:before{content:"\f1fe"}.fa-chart-bar:before{content:"\f080"}.fa-chart-line:before{content:"\f201"}.fa-chart-pie:before{content:"\f200"}.fa-check:before{content:"\f00c"}.fa-check-circle:before{content:"\f058"}.fa-check-double:before{content:"\f560"}.fa-check-square:before{content:"\f14a"}.fa-cheese:before{content:"\f7ef"}.fa-chess:before{content:"\f439"}.fa-chess-bishop:before{content:"\f43a"}.fa-chess-board:before{content:"\f43c"}.fa-chess-king:before{content:"\f43f"}.fa-chess-knight:before{content:"\f441"}.fa-chess-pawn:before{content:"\f443"}.fa-chess-queen:before{content:"\f445"}.fa-chess-rook:before{content:"\f447"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-down:before{content:"\f078"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-chevron-up:before{content:"\f077"}.fa-child:before{content:"\f1ae"}.fa-chrome:before{content:"\f268"}.fa-chromecast:before{content:"\f838"}.fa-church:before{content:"\f51d"}.fa-circle:before{content:"\f111"}.fa-circle-notch:before{content:"\f1ce"}.fa-city:before{content:"\f64f"}.fa-clinic-medical:before{content:"\f7f2"}.fa-clipboard:before{content:"\f328"}.fa-clipboard-check:before{content:"\f46c"}.fa-clipboard-list:before{content:"\f46d"}.fa-clock:before{content:"\f017"}.fa-clone:before{content:"\f24d"}.fa-closed-captioning:before{content:"\f20a"}.fa-cloud:before{content:"\f0c2"}.fa-cloud-download-alt:before{content:"\f381"}.fa-cloud-meatball:before{content:"\f73b"}.fa-cloud-moon:before{content:"\f6c3"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-cloud-rain:before{content:"\f73d"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-cloud-sun:before{content:"\f6c4"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-cloud-upload-alt:before{content:"\f382"}.fa-cloudscale:before{content:"\f383"}.fa-cloudsmith:before{content:"\f384"}.fa-cloudversify:before{content:"\f385"}.fa-cocktail:before{content:"\f561"}.fa-code:before{content:"\f121"}.fa-code-branch:before{content:"\f126"}.fa-codepen:before{content:"\f1cb"}.fa-codiepie:before{content:"\f284"}.fa-coffee:before{content:"\f0f4"}.fa-cog:before{content:"\f013"}.fa-cogs:before{content:"\f085"}.fa-coins:before{content:"\f51e"}.fa-columns:before{content:"\f0db"}.fa-comment:before{content:"\f075"}.fa-comment-alt:before{content:"\f27a"}.fa-comment-dollar:before{content:"\f651"}.fa-comment-dots:before{content:"\f4ad"}.fa-comment-medical:before{content:"\f7f5"}.fa-comment-slash:before{content:"\f4b3"}.fa-comments:before{content:"\f086"}.fa-comments-dollar:before{content:"\f653"}.fa-compact-disc:before{content:"\f51f"}.fa-compass:before{content:"\f14e"}.fa-compress:before{content:"\f066"}.fa-compress-alt:before{content:"\f422"}.fa-compress-arrows-alt:before{content:"\f78c"}.fa-concierge-bell:before{content:"\f562"}.fa-confluence:before{content:"\f78d"}.fa-connectdevelop:before{content:"\f20e"}.fa-contao:before{content:"\f26d"}.fa-cookie:before{content:"\f563"}.fa-cookie-bite:before{content:"\f564"}.fa-copy:before{content:"\f0c5"}.fa-copyright:before{content:"\f1f9"}.fa-cotton-bureau:before{content:"\f89e"}.fa-couch:before{content:"\f4b8"}.fa-cpanel:before{content:"\f388"}.fa-creative-commons:before{content:"\f25e"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-credit-card:before{content:"\f09d"}.fa-critical-role:before{content:"\f6c9"}.fa-crop:before{content:"\f125"}.fa-crop-alt:before{content:"\f565"}.fa-cross:before{content:"\f654"}.fa-crosshairs:before{content:"\f05b"}.fa-crow:before{content:"\f520"}.fa-crown:before{content:"\f521"}.fa-crutch:before{content:"\f7f7"}.fa-css3:before{content:"\f13c"}.fa-css3-alt:before{content:"\f38b"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-cut:before{content:"\f0c4"}.fa-cuttlefish:before{content:"\f38c"}.fa-d-and-d:before{content:"\f38d"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-dashcube:before{content:"\f210"}.fa-database:before{content:"\f1c0"}.fa-deaf:before{content:"\f2a4"}.fa-delicious:before{content:"\f1a5"}.fa-democrat:before{content:"\f747"}.fa-deploydog:before{content:"\f38e"}.fa-deskpro:before{content:"\f38f"}.fa-desktop:before{content:"\f108"}.fa-dev:before{content:"\f6cc"}.fa-deviantart:before{content:"\f1bd"}.fa-dharmachakra:before{content:"\f655"}.fa-dhl:before{content:"\f790"}.fa-diagnoses:before{content:"\f470"}.fa-diaspora:before{content:"\f791"}.fa-dice:before{content:"\f522"}.fa-dice-d20:before{content:"\f6cf"}.fa-dice-d6:before{content:"\f6d1"}.fa-dice-five:before{content:"\f523"}.fa-dice-four:before{content:"\f524"}.fa-dice-one:before{content:"\f525"}.fa-dice-six:before{content:"\f526"}.fa-dice-three:before{content:"\f527"}.fa-dice-two:before{content:"\f528"}.fa-digg:before{content:"\f1a6"}.fa-digital-ocean:before{content:"\f391"}.fa-digital-tachograph:before{content:"\f566"}.fa-directions:before{content:"\f5eb"}.fa-discord:before{content:"\f392"}.fa-discourse:before{content:"\f393"}.fa-divide:before{content:"\f529"}.fa-dizzy:before{content:"\f567"}.fa-dna:before{content:"\f471"}.fa-dochub:before{content:"\f394"}.fa-docker:before{content:"\f395"}.fa-dog:before{content:"\f6d3"}.fa-dollar-sign:before{content:"\f155"}.fa-dolly:before{content:"\f472"}.fa-dolly-flatbed:before{content:"\f474"}.fa-donate:before{content:"\f4b9"}.fa-door-closed:before{content:"\f52a"}.fa-door-open:before{content:"\f52b"}.fa-dot-circle:before{content:"\f192"}.fa-dove:before{content:"\f4ba"}.fa-download:before{content:"\f019"}.fa-draft2digital:before{content:"\f396"}.fa-drafting-compass:before{content:"\f568"}.fa-dragon:before{content:"\f6d5"}.fa-draw-polygon:before{content:"\f5ee"}.fa-dribbble:before{content:"\f17d"}.fa-dribbble-square:before{content:"\f397"}.fa-dropbox:before{content:"\f16b"}.fa-drum:before{content:"\f569"}.fa-drum-steelpan:before{content:"\f56a"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-drupal:before{content:"\f1a9"}.fa-dumbbell:before{content:"\f44b"}.fa-dumpster:before{content:"\f793"}.fa-dumpster-fire:before{content:"\f794"}.fa-dungeon:before{content:"\f6d9"}.fa-dyalog:before{content:"\f399"}.fa-earlybirds:before{content:"\f39a"}.fa-ebay:before{content:"\f4f4"}.fa-edge:before{content:"\f282"}.fa-edit:before{content:"\f044"}.fa-egg:before{content:"\f7fb"}.fa-eject:before{content:"\f052"}.fa-elementor:before{content:"\f430"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-ello:before{content:"\f5f1"}.fa-ember:before{content:"\f423"}.fa-empire:before{content:"\f1d1"}.fa-envelope:before{content:"\f0e0"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-text:before{content:"\f658"}.fa-envelope-square:before{content:"\f199"}.fa-envira:before{content:"\f299"}.fa-equals:before{content:"\f52c"}.fa-eraser:before{content:"\f12d"}.fa-erlang:before{content:"\f39d"}.fa-ethereum:before{content:"\f42e"}.fa-ethernet:before{content:"\f796"}.fa-etsy:before{content:"\f2d7"}.fa-euro-sign:before{content:"\f153"}.fa-evernote:before{content:"\f839"}.fa-exchange-alt:before{content:"\f362"}.fa-exclamation:before{content:"\f12a"}.fa-exclamation-circle:before{content:"\f06a"}.fa-exclamation-triangle:before{content:"\f071"}.fa-expand:before{content:"\f065"}.fa-expand-alt:before{content:"\f424"}.fa-expand-arrows-alt:before{content:"\f31e"}.fa-expeditedssl:before{content:"\f23e"}.fa-external-link-alt:before{content:"\f35d"}.fa-external-link-square-alt:before{content:"\f360"}.fa-eye:before{content:"\f06e"}.fa-eye-dropper:before{content:"\f1fb"}.fa-eye-slash:before{content:"\f070"}.fa-facebook:before{content:"\f09a"}.fa-facebook-f:before{content:"\f39e"}.fa-facebook-messenger:before{content:"\f39f"}.fa-facebook-square:before{content:"\f082"}.fa-fan:before{content:"\f863"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-fast-backward:before{content:"\f049"}.fa-fast-forward:before{content:"\f050"}.fa-fax:before{content:"\f1ac"}.fa-feather:before{content:"\f52d"}.fa-feather-alt:before{content:"\f56b"}.fa-fedex:before{content:"\f797"}.fa-fedora:before{content:"\f798"}.fa-female:before{content:"\f182"}.fa-fighter-jet:before{content:"\f0fb"}.fa-figma:before{content:"\f799"}.fa-file:before{content:"\f15b"}.fa-file-alt:before{content:"\f15c"}.fa-file-archive:before{content:"\f1c6"}.fa-file-audio:before{content:"\f1c7"}.fa-file-code:before{content:"\f1c9"}.fa-file-contract:before{content:"\f56c"}.fa-file-csv:before{content:"\f6dd"}.fa-file-download:before{content:"\f56d"}.fa-file-excel:before{content:"\f1c3"}.fa-file-export:before{content:"\f56e"}.fa-file-image:before{content:"\f1c5"}.fa-file-import:before{content:"\f56f"}.fa-file-invoice:before{content:"\f570"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-file-medical:before{content:"\f477"}.fa-file-medical-alt:before{content:"\f478"}.fa-file-pdf:before{content:"\f1c1"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-file-prescription:before{content:"\f572"}.fa-file-signature:before{content:"\f573"}.fa-file-upload:before{content:"\f574"}.fa-file-video:before{content:"\f1c8"}.fa-file-word:before{content:"\f1c2"}.fa-fill:before{content:"\f575"}.fa-fill-drip:before{content:"\f576"}.fa-film:before{content:"\f008"}.fa-filter:before{content:"\f0b0"}.fa-fingerprint:before{content:"\f577"}.fa-fire:before{content:"\f06d"}.fa-fire-alt:before{content:"\f7e4"}.fa-fire-extinguisher:before{content:"\f134"}.fa-firefox:before{content:"\f269"}.fa-firefox-browser:before{content:"\f907"}.fa-first-aid:before{content:"\f479"}.fa-first-order:before{content:"\f2b0"}.fa-first-order-alt:before{content:"\f50a"}.fa-firstdraft:before{content:"\f3a1"}.fa-fish:before{content:"\f578"}.fa-fist-raised:before{content:"\f6de"}.fa-flag:before{content:"\f024"}.fa-flag-checkered:before{content:"\f11e"}.fa-flag-usa:before{content:"\f74d"}.fa-flask:before{content:"\f0c3"}.fa-flickr:before{content:"\f16e"}.fa-flipboard:before{content:"\f44d"}.fa-flushed:before{content:"\f579"}.fa-fly:before{content:"\f417"}.fa-folder:before{content:"\f07b"}.fa-folder-minus:before{content:"\f65d"}.fa-folder-open:before{content:"\f07c"}.fa-folder-plus:before{content:"\f65e"}.fa-font:before{content:"\f031"}.fa-font-awesome:before{content:"\f2b4"}.fa-font-awesome-alt:before{content:"\f35c"}.fa-font-awesome-flag:before{content:"\f425"}.fa-font-awesome-logo-full:before{content:"\f4e6"}.fa-fonticons:before{content:"\f280"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-football-ball:before{content:"\f44e"}.fa-fort-awesome:before{content:"\f286"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-forumbee:before{content:"\f211"}.fa-forward:before{content:"\f04e"}.fa-foursquare:before{content:"\f180"}.fa-free-code-camp:before{content:"\f2c5"}.fa-freebsd:before{content:"\f3a4"}.fa-frog:before{content:"\f52e"}.fa-frown:before{content:"\f119"}.fa-frown-open:before{content:"\f57a"}.fa-fulcrum:before{content:"\f50b"}.fa-funnel-dollar:before{content:"\f662"}.fa-futbol:before{content:"\f1e3"}.fa-galactic-republic:before{content:"\f50c"}.fa-galactic-senate:before{content:"\f50d"}.fa-gamepad:before{content:"\f11b"}.fa-gas-pump:before{content:"\f52f"}.fa-gavel:before{content:"\f0e3"}.fa-gem:before{content:"\f3a5"}.fa-genderless:before{content:"\f22d"}.fa-get-pocket:before{content:"\f265"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-ghost:before{content:"\f6e2"}.fa-gift:before{content:"\f06b"}.fa-gifts:before{content:"\f79c"}.fa-git:before{content:"\f1d3"}.fa-git-alt:before{content:"\f841"}.fa-git-square:before{content:"\f1d2"}.fa-github:before{content:"\f09b"}.fa-github-alt:before{content:"\f113"}.fa-github-square:before{content:"\f092"}.fa-gitkraken:before{content:"\f3a6"}.fa-gitlab:before{content:"\f296"}.fa-gitter:before{content:"\f426"}.fa-glass-cheers:before{content:"\f79f"}.fa-glass-martini:before{content:"\f000"}.fa-glass-martini-alt:before{content:"\f57b"}.fa-glass-whiskey:before{content:"\f7a0"}.fa-glasses:before{content:"\f530"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-globe:before{content:"\f0ac"}.fa-globe-africa:before{content:"\f57c"}.fa-globe-americas:before{content:"\f57d"}.fa-globe-asia:before{content:"\f57e"}.fa-globe-europe:before{content:"\f7a2"}.fa-gofore:before{content:"\f3a7"}.fa-golf-ball:before{content:"\f450"}.fa-goodreads:before{content:"\f3a8"}.fa-goodreads-g:before{content:"\f3a9"}.fa-google:before{content:"\f1a0"}.fa-google-drive:before{content:"\f3aa"}.fa-google-play:before{content:"\f3ab"}.fa-google-plus:before{content:"\f2b3"}.fa-google-plus-g:before{content:"\f0d5"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-wallet:before{content:"\f1ee"}.fa-gopuram:before{content:"\f664"}.fa-graduation-cap:before{content:"\f19d"}.fa-gratipay:before{content:"\f184"}.fa-grav:before{content:"\f2d6"}.fa-greater-than:before{content:"\f531"}.fa-greater-than-equal:before{content:"\f532"}.fa-grimace:before{content:"\f57f"}.fa-grin:before{content:"\f580"}.fa-grin-alt:before{content:"\f581"}.fa-grin-beam:before{content:"\f582"}.fa-grin-beam-sweat:before{content:"\f583"}.fa-grin-hearts:before{content:"\f584"}.fa-grin-squint:before{content:"\f585"}.fa-grin-squint-tears:before{content:"\f586"}.fa-grin-stars:before{content:"\f587"}.fa-grin-tears:before{content:"\f588"}.fa-grin-tongue:before{content:"\f589"}.fa-grin-tongue-squint:before{content:"\f58a"}.fa-grin-tongue-wink:before{content:"\f58b"}.fa-grin-wink:before{content:"\f58c"}.fa-grip-horizontal:before{content:"\f58d"}.fa-grip-lines:before{content:"\f7a4"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-grip-vertical:before{content:"\f58e"}.fa-gripfire:before{content:"\f3ac"}.fa-grunt:before{content:"\f3ad"}.fa-guitar:before{content:"\f7a6"}.fa-gulp:before{content:"\f3ae"}.fa-h-square:before{content:"\f0fd"}.fa-hacker-news:before{content:"\f1d4"}.fa-hacker-news-square:before{content:"\f3af"}.fa-hackerrank:before{content:"\f5f7"}.fa-hamburger:before{content:"\f805"}.fa-hammer:before{content:"\f6e3"}.fa-hamsa:before{content:"\f665"}.fa-hand-holding:before{content:"\f4bd"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-hand-holding-usd:before{content:"\f4c0"}.fa-hand-lizard:before{content:"\f258"}.fa-hand-middle-finger:before{content:"\f806"}.fa-hand-paper:before{content:"\f256"}.fa-hand-peace:before{content:"\f25b"}.fa-hand-point-down:before{content:"\f0a7"}.fa-hand-point-left:before{content:"\f0a5"}.fa-hand-point-right:before{content:"\f0a4"}.fa-hand-point-up:before{content:"\f0a6"}.fa-hand-pointer:before{content:"\f25a"}.fa-hand-rock:before{content:"\f255"}.fa-hand-scissors:before{content:"\f257"}.fa-hand-spock:before{content:"\f259"}.fa-hands:before{content:"\f4c2"}.fa-hands-helping:before{content:"\f4c4"}.fa-handshake:before{content:"\f2b5"}.fa-hanukiah:before{content:"\f6e6"}.fa-hard-hat:before{content:"\f807"}.fa-hashtag:before{content:"\f292"}.fa-hat-cowboy:before{content:"\f8c0"}.fa-hat-cowboy-side:before{content:"\f8c1"}.fa-hat-wizard:before{content:"\f6e8"}.fa-hdd:before{content:"\f0a0"}.fa-heading:before{content:"\f1dc"}.fa-headphones:before{content:"\f025"}.fa-headphones-alt:before{content:"\f58f"}.fa-headset:before{content:"\f590"}.fa-heart:before{content:"\f004"}.fa-heart-broken:before{content:"\f7a9"}.fa-heartbeat:before{content:"\f21e"}.fa-helicopter:before{content:"\f533"}.fa-highlighter:before{content:"\f591"}.fa-hiking:before{content:"\f6ec"}.fa-hippo:before{content:"\f6ed"}.fa-hips:before{content:"\f452"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-history:before{content:"\f1da"}.fa-hockey-puck:before{content:"\f453"}.fa-holly-berry:before{content:"\f7aa"}.fa-home:before{content:"\f015"}.fa-hooli:before{content:"\f427"}.fa-hornbill:before{content:"\f592"}.fa-horse:before{content:"\f6f0"}.fa-horse-head:before{content:"\f7ab"}.fa-hospital:before{content:"\f0f8"}.fa-hospital-alt:before{content:"\f47d"}.fa-hospital-symbol:before{content:"\f47e"}.fa-hot-tub:before{content:"\f593"}.fa-hotdog:before{content:"\f80f"}.fa-hotel:before{content:"\f594"}.fa-hotjar:before{content:"\f3b1"}.fa-hourglass:before{content:"\f254"}.fa-hourglass-end:before{content:"\f253"}.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-start:before{content:"\f251"}.fa-house-damage:before{content:"\f6f1"}.fa-houzz:before{content:"\f27c"}.fa-hryvnia:before{content:"\f6f2"}.fa-html5:before{content:"\f13b"}.fa-hubspot:before{content:"\f3b2"}.fa-i-cursor:before{content:"\f246"}.fa-ice-cream:before{content:"\f810"}.fa-icicles:before{content:"\f7ad"}.fa-icons:before{content:"\f86d"}.fa-id-badge:before{content:"\f2c1"}.fa-id-card:before{content:"\f2c2"}.fa-id-card-alt:before{content:"\f47f"}.fa-ideal:before{content:"\f913"}.fa-igloo:before{content:"\f7ae"}.fa-image:before{content:"\f03e"}.fa-images:before{content:"\f302"}.fa-imdb:before{content:"\f2d8"}.fa-inbox:before{content:"\f01c"}.fa-indent:before{content:"\f03c"}.fa-industry:before{content:"\f275"}.fa-infinity:before{content:"\f534"}.fa-info:before{content:"\f129"}.fa-info-circle:before{content:"\f05a"}.fa-instagram:before{content:"\f16d"}.fa-intercom:before{content:"\f7af"}.fa-internet-explorer:before{content:"\f26b"}.fa-invision:before{content:"\f7b0"}.fa-ioxhost:before{content:"\f208"}.fa-italic:before{content:"\f033"}.fa-itch-io:before{content:"\f83a"}.fa-itunes:before{content:"\f3b4"}.fa-itunes-note:before{content:"\f3b5"}.fa-java:before{content:"\f4e4"}.fa-jedi:before{content:"\f669"}.fa-jedi-order:before{content:"\f50e"}.fa-jenkins:before{content:"\f3b6"}.fa-jira:before{content:"\f7b1"}.fa-joget:before{content:"\f3b7"}.fa-joint:before{content:"\f595"}.fa-joomla:before{content:"\f1aa"}.fa-journal-whills:before{content:"\f66a"}.fa-js:before{content:"\f3b8"}.fa-js-square:before{content:"\f3b9"}.fa-jsfiddle:before{content:"\f1cc"}.fa-kaaba:before{content:"\f66b"}.fa-kaggle:before{content:"\f5fa"}.fa-key:before{content:"\f084"}.fa-keybase:before{content:"\f4f5"}.fa-keyboard:before{content:"\f11c"}.fa-keycdn:before{content:"\f3ba"}.fa-khanda:before{content:"\f66d"}.fa-kickstarter:before{content:"\f3bb"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-kiss:before{content:"\f596"}.fa-kiss-beam:before{content:"\f597"}.fa-kiss-wink-heart:before{content:"\f598"}.fa-kiwi-bird:before{content:"\f535"}.fa-korvue:before{content:"\f42f"}.fa-landmark:before{content:"\f66f"}.fa-language:before{content:"\f1ab"}.fa-laptop:before{content:"\f109"}.fa-laptop-code:before{content:"\f5fc"}.fa-laptop-medical:before{content:"\f812"}.fa-laravel:before{content:"\f3bd"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-laugh:before{content:"\f599"}.fa-laugh-beam:before{content:"\f59a"}.fa-laugh-squint:before{content:"\f59b"}.fa-laugh-wink:before{content:"\f59c"}.fa-layer-group:before{content:"\f5fd"}.fa-leaf:before{content:"\f06c"}.fa-leanpub:before{content:"\f212"}.fa-lemon:before{content:"\f094"}.fa-less:before{content:"\f41d"}.fa-less-than:before{content:"\f536"}.fa-less-than-equal:before{content:"\f537"}.fa-level-down-alt:before{content:"\f3be"}.fa-level-up-alt:before{content:"\f3bf"}.fa-life-ring:before{content:"\f1cd"}.fa-lightbulb:before{content:"\f0eb"}.fa-line:before{content:"\f3c0"}.fa-link:before{content:"\f0c1"}.fa-linkedin:before{content:"\f08c"}.fa-linkedin-in:before{content:"\f0e1"}.fa-linode:before{content:"\f2b8"}.fa-linux:before{content:"\f17c"}.fa-lira-sign:before{content:"\f195"}.fa-list:before{content:"\f03a"}.fa-list-alt:before{content:"\f022"}.fa-list-ol:before{content:"\f0cb"}.fa-list-ul:before{content:"\f0ca"}.fa-location-arrow:before{content:"\f124"}.fa-lock:before{content:"\f023"}.fa-lock-open:before{content:"\f3c1"}.fa-long-arrow-alt-down:before{content:"\f309"}.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-long-arrow-alt-right:before{content:"\f30b"}.fa-long-arrow-alt-up:before{content:"\f30c"}.fa-low-vision:before{content:"\f2a8"}.fa-luggage-cart:before{content:"\f59d"}.fa-lyft:before{content:"\f3c3"}.fa-magento:before{content:"\f3c4"}.fa-magic:before{content:"\f0d0"}.fa-magnet:before{content:"\f076"}.fa-mail-bulk:before{content:"\f674"}.fa-mailchimp:before{content:"\f59e"}.fa-male:before{content:"\f183"}.fa-mandalorian:before{content:"\f50f"}.fa-map:before{content:"\f279"}.fa-map-marked:before{content:"\f59f"}.fa-map-marked-alt:before{content:"\f5a0"}.fa-map-marker:before{content:"\f041"}.fa-map-marker-alt:before{content:"\f3c5"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-markdown:before{content:"\f60f"}.fa-marker:before{content:"\f5a1"}.fa-mars:before{content:"\f222"}.fa-mars-double:before{content:"\f227"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mask:before{content:"\f6fa"}.fa-mastodon:before{content:"\f4f6"}.fa-maxcdn:before{content:"\f136"}.fa-mdb:before{content:"\f8ca"}.fa-medal:before{content:"\f5a2"}.fa-medapps:before{content:"\f3c6"}.fa-medium:before{content:"\f23a"}.fa-medium-m:before{content:"\f3c7"}.fa-medkit:before{content:"\f0fa"}.fa-medrt:before{content:"\f3c8"}.fa-meetup:before{content:"\f2e0"}.fa-megaport:before{content:"\f5a3"}.fa-meh:before{content:"\f11a"}.fa-meh-blank:before{content:"\f5a4"}.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-memory:before{content:"\f538"}.fa-mendeley:before{content:"\f7b3"}.fa-menorah:before{content:"\f676"}.fa-mercury:before{content:"\f223"}.fa-meteor:before{content:"\f753"}.fa-microblog:before{content:"\f91a"}.fa-microchip:before{content:"\f2db"}.fa-microphone:before{content:"\f130"}.fa-microphone-alt:before{content:"\f3c9"}.fa-microphone-alt-slash:before{content:"\f539"}.fa-microphone-slash:before{content:"\f131"}.fa-microscope:before{content:"\f610"}.fa-microsoft:before{content:"\f3ca"}.fa-minus:before{content:"\f068"}.fa-minus-circle:before{content:"\f056"}.fa-minus-square:before{content:"\f146"}.fa-mitten:before{content:"\f7b5"}.fa-mix:before{content:"\f3cb"}.fa-mixcloud:before{content:"\f289"}.fa-mizuni:before{content:"\f3cc"}.fa-mobile:before{content:"\f10b"}.fa-mobile-alt:before{content:"\f3cd"}.fa-modx:before{content:"\f285"}.fa-monero:before{content:"\f3d0"}.fa-money-bill:before{content:"\f0d6"}.fa-money-bill-alt:before{content:"\f3d1"}.fa-money-bill-wave:before{content:"\f53a"}.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-money-check:before{content:"\f53c"}.fa-money-check-alt:before{content:"\f53d"}.fa-monument:before{content:"\f5a6"}.fa-moon:before{content:"\f186"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-mosque:before{content:"\f678"}.fa-motorcycle:before{content:"\f21c"}.fa-mountain:before{content:"\f6fc"}.fa-mouse:before{content:"\f8cc"}.fa-mouse-pointer:before{content:"\f245"}.fa-mug-hot:before{content:"\f7b6"}.fa-music:before{content:"\f001"}.fa-napster:before{content:"\f3d2"}.fa-neos:before{content:"\f612"}.fa-network-wired:before{content:"\f6ff"}.fa-neuter:before{content:"\f22c"}.fa-newspaper:before{content:"\f1ea"}.fa-nimblr:before{content:"\f5a8"}.fa-node:before{content:"\f419"}.fa-node-js:before{content:"\f3d3"}.fa-not-equal:before{content:"\f53e"}.fa-notes-medical:before{content:"\f481"}.fa-npm:before{content:"\f3d4"}.fa-ns8:before{content:"\f3d5"}.fa-nutritionix:before{content:"\f3d6"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-oil-can:before{content:"\f613"}.fa-old-republic:before{content:"\f510"}.fa-om:before{content:"\f679"}.fa-opencart:before{content:"\f23d"}.fa-openid:before{content:"\f19b"}.fa-opera:before{content:"\f26a"}.fa-optin-monster:before{content:"\f23c"}.fa-orcid:before{content:"\f8d2"}.fa-osi:before{content:"\f41a"}.fa-otter:before{content:"\f700"}.fa-outdent:before{content:"\f03b"}.fa-page4:before{content:"\f3d7"}.fa-pagelines:before{content:"\f18c"}.fa-pager:before{content:"\f815"}.fa-paint-brush:before{content:"\f1fc"}.fa-paint-roller:before{content:"\f5aa"}.fa-palette:before{content:"\f53f"}.fa-palfed:before{content:"\f3d8"}.fa-pallet:before{content:"\f482"}.fa-paper-plane:before{content:"\f1d8"}.fa-paperclip:before{content:"\f0c6"}.fa-parachute-box:before{content:"\f4cd"}.fa-paragraph:before{content:"\f1dd"}.fa-parking:before{content:"\f540"}.fa-passport:before{content:"\f5ab"}.fa-pastafarianism:before{content:"\f67b"}.fa-paste:before{content:"\f0ea"}.fa-patreon:before{content:"\f3d9"}.fa-pause:before{content:"\f04c"}.fa-pause-circle:before{content:"\f28b"}.fa-paw:before{content:"\f1b0"}.fa-paypal:before{content:"\f1ed"}.fa-peace:before{content:"\f67c"}.fa-pen:before{content:"\f304"}.fa-pen-alt:before{content:"\f305"}.fa-pen-fancy:before{content:"\f5ac"}.fa-pen-nib:before{content:"\f5ad"}.fa-pen-square:before{content:"\f14b"}.fa-pencil-alt:before{content:"\f303"}.fa-pencil-ruler:before{content:"\f5ae"}.fa-penny-arcade:before{content:"\f704"}.fa-people-carry:before{content:"\f4ce"}.fa-pepper-hot:before{content:"\f816"}.fa-percent:before{content:"\f295"}.fa-percentage:before{content:"\f541"}.fa-periscope:before{content:"\f3da"}.fa-person-booth:before{content:"\f756"}.fa-phabricator:before{content:"\f3db"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-phoenix-squadron:before{content:"\f511"}.fa-phone:before{content:"\f095"}.fa-phone-alt:before{content:"\f879"}.fa-phone-slash:before{content:"\f3dd"}.fa-phone-square:before{content:"\f098"}.fa-phone-square-alt:before{content:"\f87b"}.fa-phone-volume:before{content:"\f2a0"}.fa-photo-video:before{content:"\f87c"}.fa-php:before{content:"\f457"}.fa-pied-piper:before{content:"\f2ae"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-square:before{content:"\f91e"}.fa-piggy-bank:before{content:"\f4d3"}.fa-pills:before{content:"\f484"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-p:before{content:"\f231"}.fa-pinterest-square:before{content:"\f0d3"}.fa-pizza-slice:before{content:"\f818"}.fa-place-of-worship:before{content:"\f67f"}.fa-plane:before{content:"\f072"}.fa-plane-arrival:before{content:"\f5af"}.fa-plane-departure:before{content:"\f5b0"}.fa-play:before{content:"\f04b"}.fa-play-circle:before{content:"\f144"}.fa-playstation:before{content:"\f3df"}.fa-plug:before{content:"\f1e6"}.fa-plus:before{content:"\f067"}.fa-plus-circle:before{content:"\f055"}.fa-plus-square:before{content:"\f0fe"}.fa-podcast:before{content:"\f2ce"}.fa-poll:before{content:"\f681"}.fa-poll-h:before{content:"\f682"}.fa-poo:before{content:"\f2fe"}.fa-poo-storm:before{content:"\f75a"}.fa-poop:before{content:"\f619"}.fa-portrait:before{content:"\f3e0"}.fa-pound-sign:before{content:"\f154"}.fa-power-off:before{content:"\f011"}.fa-pray:before{content:"\f683"}.fa-praying-hands:before{content:"\f684"}.fa-prescription:before{content:"\f5b1"}.fa-prescription-bottle:before{content:"\f485"}.fa-prescription-bottle-alt:before{content:"\f486"}.fa-print:before{content:"\f02f"}.fa-procedures:before{content:"\f487"}.fa-product-hunt:before{content:"\f288"}.fa-project-diagram:before{content:"\f542"}.fa-pushed:before{content:"\f3e1"}.fa-puzzle-piece:before{content:"\f12e"}.fa-python:before{content:"\f3e2"}.fa-qq:before{content:"\f1d6"}.fa-qrcode:before{content:"\f029"}.fa-question:before{content:"\f128"}.fa-question-circle:before{content:"\f059"}.fa-quidditch:before{content:"\f458"}.fa-quinscape:before{content:"\f459"}.fa-quora:before{content:"\f2c4"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-quran:before{content:"\f687"}.fa-r-project:before{content:"\f4f7"}.fa-radiation:before{content:"\f7b9"}.fa-radiation-alt:before{content:"\f7ba"}.fa-rainbow:before{content:"\f75b"}.fa-random:before{content:"\f074"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-ravelry:before{content:"\f2d9"}.fa-react:before{content:"\f41b"}.fa-reacteurope:before{content:"\f75d"}.fa-readme:before{content:"\f4d5"}.fa-rebel:before{content:"\f1d0"}.fa-receipt:before{content:"\f543"}.fa-record-vinyl:before{content:"\f8d9"}.fa-recycle:before{content:"\f1b8"}.fa-red-river:before{content:"\f3e3"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-alien:before{content:"\f281"}.fa-reddit-square:before{content:"\f1a2"}.fa-redhat:before{content:"\f7bc"}.fa-redo:before{content:"\f01e"}.fa-redo-alt:before{content:"\f2f9"}.fa-registered:before{content:"\f25d"}.fa-remove-format:before{content:"\f87d"}.fa-renren:before{content:"\f18b"}.fa-reply:before{content:"\f3e5"}.fa-reply-all:before{content:"\f122"}.fa-replyd:before{content:"\f3e6"}.fa-republican:before{content:"\f75e"}.fa-researchgate:before{content:"\f4f8"}.fa-resolving:before{content:"\f3e7"}.fa-restroom:before{content:"\f7bd"}.fa-retweet:before{content:"\f079"}.fa-rev:before{content:"\f5b2"}.fa-ribbon:before{content:"\f4d6"}.fa-ring:before{content:"\f70b"}.fa-road:before{content:"\f018"}.fa-robot:before{content:"\f544"}.fa-rocket:before{content:"\f135"}.fa-rocketchat:before{content:"\f3e8"}.fa-rockrms:before{content:"\f3e9"}.fa-route:before{content:"\f4d7"}.fa-rss:before{content:"\f09e"}.fa-rss-square:before{content:"\f143"}.fa-ruble-sign:before{content:"\f158"}.fa-ruler:before{content:"\f545"}.fa-ruler-combined:before{content:"\f546"}.fa-ruler-horizontal:before{content:"\f547"}.fa-ruler-vertical:before{content:"\f548"}.fa-running:before{content:"\f70c"}.fa-rupee-sign:before{content:"\f156"}.fa-sad-cry:before{content:"\f5b3"}.fa-sad-tear:before{content:"\f5b4"}.fa-safari:before{content:"\f267"}.fa-salesforce:before{content:"\f83b"}.fa-sass:before{content:"\f41e"}.fa-satellite:before{content:"\f7bf"}.fa-satellite-dish:before{content:"\f7c0"}.fa-save:before{content:"\f0c7"}.fa-schlix:before{content:"\f3ea"}.fa-school:before{content:"\f549"}.fa-screwdriver:before{content:"\f54a"}.fa-scribd:before{content:"\f28a"}.fa-scroll:before{content:"\f70e"}.fa-sd-card:before{content:"\f7c2"}.fa-search:before{content:"\f002"}.fa-search-dollar:before{content:"\f688"}.fa-search-location:before{content:"\f689"}.fa-search-minus:before{content:"\f010"}.fa-search-plus:before{content:"\f00e"}.fa-searchengin:before{content:"\f3eb"}.fa-seedling:before{content:"\f4d8"}.fa-sellcast:before{content:"\f2da"}.fa-sellsy:before{content:"\f213"}.fa-server:before{content:"\f233"}.fa-servicestack:before{content:"\f3ec"}.fa-shapes:before{content:"\f61f"}.fa-share:before{content:"\f064"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-share-square:before{content:"\f14d"}.fa-shekel-sign:before{content:"\f20b"}.fa-shield-alt:before{content:"\f3ed"}.fa-ship:before{content:"\f21a"}.fa-shipping-fast:before{content:"\f48b"}.fa-shirtsinbulk:before{content:"\f214"}.fa-shoe-prints:before{content:"\f54b"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-shopping-cart:before{content:"\f07a"}.fa-shopware:before{content:"\f5b5"}.fa-shower:before{content:"\f2cc"}.fa-shuttle-van:before{content:"\f5b6"}.fa-sign:before{content:"\f4d9"}.fa-sign-in-alt:before{content:"\f2f6"}.fa-sign-language:before{content:"\f2a7"}.fa-sign-out-alt:before{content:"\f2f5"}.fa-signal:before{content:"\f012"}.fa-signature:before{content:"\f5b7"}.fa-sim-card:before{content:"\f7c4"}.fa-simplybuilt:before{content:"\f215"}.fa-sistrix:before{content:"\f3ee"}.fa-sitemap:before{content:"\f0e8"}.fa-sith:before{content:"\f512"}.fa-skating:before{content:"\f7c5"}.fa-sketch:before{content:"\f7c6"}.fa-skiing:before{content:"\f7c9"}.fa-skiing-nordic:before{content:"\f7ca"}.fa-skull:before{content:"\f54c"}.fa-skull-crossbones:before{content:"\f714"}.fa-skyatlas:before{content:"\f216"}.fa-skype:before{content:"\f17e"}.fa-slack:before{content:"\f198"}.fa-slack-hash:before{content:"\f3ef"}.fa-slash:before{content:"\f715"}.fa-sleigh:before{content:"\f7cc"}.fa-sliders-h:before{content:"\f1de"}.fa-slideshare:before{content:"\f1e7"}.fa-smile:before{content:"\f118"}.fa-smile-beam:before{content:"\f5b8"}.fa-smile-wink:before{content:"\f4da"}.fa-smog:before{content:"\f75f"}.fa-smoking:before{content:"\f48d"}.fa-smoking-ban:before{content:"\f54d"}.fa-sms:before{content:"\f7cd"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-snowboarding:before{content:"\f7ce"}.fa-snowflake:before{content:"\f2dc"}.fa-snowman:before{content:"\f7d0"}.fa-snowplow:before{content:"\f7d2"}.fa-socks:before{content:"\f696"}.fa-solar-panel:before{content:"\f5ba"}.fa-sort:before{content:"\f0dc"}.fa-sort-alpha-down:before{content:"\f15d"}.fa-sort-alpha-down-alt:before{content:"\f881"}.fa-sort-alpha-up:before{content:"\f15e"}.fa-sort-alpha-up-alt:before{content:"\f882"}.fa-sort-amount-down:before{content:"\f160"}.fa-sort-amount-down-alt:before{content:"\f884"}.fa-sort-amount-up:before{content:"\f161"}.fa-sort-amount-up-alt:before{content:"\f885"}.fa-sort-down:before{content:"\f0dd"}.fa-sort-numeric-down:before{content:"\f162"}.fa-sort-numeric-down-alt:before{content:"\f886"}.fa-sort-numeric-up:before{content:"\f163"}.fa-sort-numeric-up-alt:before{content:"\f887"}.fa-sort-up:before{content:"\f0de"}.fa-soundcloud:before{content:"\f1be"}.fa-sourcetree:before{content:"\f7d3"}.fa-spa:before{content:"\f5bb"}.fa-space-shuttle:before{content:"\f197"}.fa-speakap:before{content:"\f3f3"}.fa-speaker-deck:before{content:"\f83c"}.fa-spell-check:before{content:"\f891"}.fa-spider:before{content:"\f717"}.fa-spinner:before{content:"\f110"}.fa-splotch:before{content:"\f5bc"}.fa-spotify:before{content:"\f1bc"}.fa-spray-can:before{content:"\f5bd"}.fa-square:before{content:"\f0c8"}.fa-square-full:before{content:"\f45c"}.fa-square-root-alt:before{content:"\f698"}.fa-squarespace:before{content:"\f5be"}.fa-stack-exchange:before{content:"\f18d"}.fa-stack-overflow:before{content:"\f16c"}.fa-stackpath:before{content:"\f842"}.fa-stamp:before{content:"\f5bf"}.fa-star:before{content:"\f005"}.fa-star-and-crescent:before{content:"\f699"}.fa-star-half:before{content:"\f089"}.fa-star-half-alt:before{content:"\f5c0"}.fa-star-of-david:before{content:"\f69a"}.fa-star-of-life:before{content:"\f621"}.fa-staylinked:before{content:"\f3f5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-steam-symbol:before{content:"\f3f6"}.fa-step-backward:before{content:"\f048"}.fa-step-forward:before{content:"\f051"}.fa-stethoscope:before{content:"\f0f1"}.fa-sticker-mule:before{content:"\f3f7"}.fa-sticky-note:before{content:"\f249"}.fa-stop:before{content:"\f04d"}.fa-stop-circle:before{content:"\f28d"}.fa-stopwatch:before{content:"\f2f2"}.fa-store:before{content:"\f54e"}.fa-store-alt:before{content:"\f54f"}.fa-strava:before{content:"\f428"}.fa-stream:before{content:"\f550"}.fa-street-view:before{content:"\f21d"}.fa-strikethrough:before{content:"\f0cc"}.fa-stripe:before{content:"\f429"}.fa-stripe-s:before{content:"\f42a"}.fa-stroopwafel:before{content:"\f551"}.fa-studiovinari:before{content:"\f3f8"}.fa-stumbleupon:before{content:"\f1a4"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-subscript:before{content:"\f12c"}.fa-subway:before{content:"\f239"}.fa-suitcase:before{content:"\f0f2"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-sun:before{content:"\f185"}.fa-superpowers:before{content:"\f2dd"}.fa-superscript:before{content:"\f12b"}.fa-supple:before{content:"\f3f9"}.fa-surprise:before{content:"\f5c2"}.fa-suse:before{content:"\f7d6"}.fa-swatchbook:before{content:"\f5c3"}.fa-swift:before{content:"\f8e1"}.fa-swimmer:before{content:"\f5c4"}.fa-swimming-pool:before{content:"\f5c5"}.fa-symfony:before{content:"\f83d"}.fa-synagogue:before{content:"\f69b"}.fa-sync:before{content:"\f021"}.fa-sync-alt:before{content:"\f2f1"}.fa-syringe:before{content:"\f48e"}.fa-table:before{content:"\f0ce"}.fa-table-tennis:before{content:"\f45d"}.fa-tablet:before{content:"\f10a"}.fa-tablet-alt:before{content:"\f3fa"}.fa-tablets:before{content:"\f490"}.fa-tachometer-alt:before{content:"\f3fd"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-tape:before{content:"\f4db"}.fa-tasks:before{content:"\f0ae"}.fa-taxi:before{content:"\f1ba"}.fa-teamspeak:before{content:"\f4f9"}.fa-teeth:before{content:"\f62e"}.fa-teeth-open:before{content:"\f62f"}.fa-telegram:before{content:"\f2c6"}.fa-telegram-plane:before{content:"\f3fe"}.fa-temperature-high:before{content:"\f769"}.fa-temperature-low:before{content:"\f76b"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-tenge:before{content:"\f7d7"}.fa-terminal:before{content:"\f120"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-th:before{content:"\f00a"}.fa-th-large:before{content:"\f009"}.fa-th-list:before{content:"\f00b"}.fa-the-red-yeti:before{content:"\f69d"}.fa-theater-masks:before{content:"\f630"}.fa-themeco:before{content:"\f5c6"}.fa-themeisle:before{content:"\f2b2"}.fa-thermometer:before{content:"\f491"}.fa-thermometer-empty:before{content:"\f2cb"}.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-think-peaks:before{content:"\f731"}.fa-thumbs-down:before{content:"\f165"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbtack:before{content:"\f08d"}.fa-ticket-alt:before{content:"\f3ff"}.fa-times:before{content:"\f00d"}.fa-times-circle:before{content:"\f057"}.fa-tint:before{content:"\f043"}.fa-tint-slash:before{content:"\f5c7"}.fa-tired:before{content:"\f5c8"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-toilet:before{content:"\f7d8"}.fa-toilet-paper:before{content:"\f71e"}.fa-toolbox:before{content:"\f552"}.fa-tools:before{content:"\f7d9"}.fa-tooth:before{content:"\f5c9"}.fa-torah:before{content:"\f6a0"}.fa-torii-gate:before{content:"\f6a1"}.fa-tractor:before{content:"\f722"}.fa-trade-federation:before{content:"\f513"}.fa-trademark:before{content:"\f25c"}.fa-traffic-light:before{content:"\f637"}.fa-trailer:before{content:"\f941"}.fa-train:before{content:"\f238"}.fa-tram:before{content:"\f7da"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-trash:before{content:"\f1f8"}.fa-trash-alt:before{content:"\f2ed"}.fa-trash-restore:before{content:"\f829"}.fa-trash-restore-alt:before{content:"\f82a"}.fa-tree:before{content:"\f1bb"}.fa-trello:before{content:"\f181"}.fa-tripadvisor:before{content:"\f262"}.fa-trophy:before{content:"\f091"}.fa-truck:before{content:"\f0d1"}.fa-truck-loading:before{content:"\f4de"}.fa-truck-monster:before{content:"\f63b"}.fa-truck-moving:before{content:"\f4df"}.fa-truck-pickup:before{content:"\f63c"}.fa-tshirt:before{content:"\f553"}.fa-tty:before{content:"\f1e4"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-tv:before{content:"\f26c"}.fa-twitch:before{content:"\f1e8"}.fa-twitter:before{content:"\f099"}.fa-twitter-square:before{content:"\f081"}.fa-typo3:before{content:"\f42b"}.fa-uber:before{content:"\f402"}.fa-ubuntu:before{content:"\f7df"}.fa-uikit:before{content:"\f403"}.fa-umbraco:before{content:"\f8e8"}.fa-umbrella:before{content:"\f0e9"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-underline:before{content:"\f0cd"}.fa-undo:before{content:"\f0e2"}.fa-undo-alt:before{content:"\f2ea"}.fa-uniregistry:before{content:"\f404"}.fa-unity:before{content:"\f949"}.fa-universal-access:before{content:"\f29a"}.fa-university:before{content:"\f19c"}.fa-unlink:before{content:"\f127"}.fa-unlock:before{content:"\f09c"}.fa-unlock-alt:before{content:"\f13e"}.fa-untappd:before{content:"\f405"}.fa-upload:before{content:"\f093"}.fa-ups:before{content:"\f7e0"}.fa-usb:before{content:"\f287"}.fa-user:before{content:"\f007"}.fa-user-alt:before{content:"\f406"}.fa-user-alt-slash:before{content:"\f4fa"}.fa-user-astronaut:before{content:"\f4fb"}.fa-user-check:before{content:"\f4fc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-clock:before{content:"\f4fd"}.fa-user-cog:before{content:"\f4fe"}.fa-user-edit:before{content:"\f4ff"}.fa-user-friends:before{content:"\f500"}.fa-user-graduate:before{content:"\f501"}.fa-user-injured:before{content:"\f728"}.fa-user-lock:before{content:"\f502"}.fa-user-md:before{content:"\f0f0"}.fa-user-minus:before{content:"\f503"}.fa-user-ninja:before{content:"\f504"}.fa-user-nurse:before{content:"\f82f"}.fa-user-plus:before{content:"\f234"}.fa-user-secret:before{content:"\f21b"}.fa-user-shield:before{content:"\f505"}.fa-user-slash:before{content:"\f506"}.fa-user-tag:before{content:"\f507"}.fa-user-tie:before{content:"\f508"}.fa-user-times:before{content:"\f235"}.fa-users:before{content:"\f0c0"}.fa-users-cog:before{content:"\f509"}.fa-usps:before{content:"\f7e1"}.fa-ussunnah:before{content:"\f407"}.fa-utensil-spoon:before{content:"\f2e5"}.fa-utensils:before{content:"\f2e7"}.fa-vaadin:before{content:"\f408"}.fa-vector-square:before{content:"\f5cb"}.fa-venus:before{content:"\f221"}.fa-venus-double:before{content:"\f226"}.fa-venus-mars:before{content:"\f228"}.fa-viacoin:before{content:"\f237"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-vial:before{content:"\f492"}.fa-vials:before{content:"\f493"}.fa-viber:before{content:"\f409"}.fa-video:before{content:"\f03d"}.fa-video-slash:before{content:"\f4e2"}.fa-vihara:before{content:"\f6a7"}.fa-vimeo:before{content:"\f40a"}.fa-vimeo-square:before{content:"\f194"}.fa-vimeo-v:before{content:"\f27d"}.fa-vine:before{content:"\f1ca"}.fa-vk:before{content:"\f189"}.fa-vnv:before{content:"\f40b"}.fa-voicemail:before{content:"\f897"}.fa-volleyball-ball:before{content:"\f45f"}.fa-volume-down:before{content:"\f027"}.fa-volume-mute:before{content:"\f6a9"}.fa-volume-off:before{content:"\f026"}.fa-volume-up:before{content:"\f028"}.fa-vote-yea:before{content:"\f772"}.fa-vr-cardboard:before{content:"\f729"}.fa-vuejs:before{content:"\f41f"}.fa-walking:before{content:"\f554"}.fa-wallet:before{content:"\f555"}.fa-warehouse:before{content:"\f494"}.fa-water:before{content:"\f773"}.fa-wave-square:before{content:"\f83e"}.fa-waze:before{content:"\f83f"}.fa-weebly:before{content:"\f5cc"}.fa-weibo:before{content:"\f18a"}.fa-weight:before{content:"\f496"}.fa-weight-hanging:before{content:"\f5cd"}.fa-weixin:before{content:"\f1d7"}.fa-whatsapp:before{content:"\f232"}.fa-whatsapp-square:before{content:"\f40c"}.fa-wheelchair:before{content:"\f193"}.fa-whmcs:before{content:"\f40d"}.fa-wifi:before{content:"\f1eb"}.fa-wikipedia-w:before{content:"\f266"}.fa-wind:before{content:"\f72e"}.fa-window-close:before{content:"\f410"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-windows:before{content:"\f17a"}.fa-wine-bottle:before{content:"\f72f"}.fa-wine-glass:before{content:"\f4e3"}.fa-wine-glass-alt:before{content:"\f5ce"}.fa-wix:before{content:"\f5cf"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-won-sign:before{content:"\f159"}.fa-wordpress:before{content:"\f19a"}.fa-wordpress-simple:before{content:"\f411"}.fa-wpbeginner:before{content:"\f297"}.fa-wpexplorer:before{content:"\f2de"}.fa-wpforms:before{content:"\f298"}.fa-wpressr:before{content:"\f3e4"}.fa-wrench:before{content:"\f0ad"}.fa-x-ray:before{content:"\f497"}.fa-xbox:before{content:"\f412"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-y-combinator:before{content:"\f23b"}.fa-yahoo:before{content:"\f19e"}.fa-yammer:before{content:"\f840"}.fa-yandex:before{content:"\f413"}.fa-yandex-international:before{content:"\f414"}.fa-yarn:before{content:"\f7e3"}.fa-yelp:before{content:"\f1e9"}.fa-yen-sign:before{content:"\f157"}.fa-yin-yang:before{content:"\f6ad"}.fa-yoast:before{content:"\f2b1"}.fa-youtube:before{content:"\f167"}.fa-youtube-square:before{content:"\f431"}.fa-zhihu:before{content:"\f63f"}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}@font-face{font-family:"Font Awesome 5 Brands";font-style:normal;font-weight:normal;font-display:auto;src:url(../fonts/fa-brands-400.eot);src:url(../fonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../fonts/fa-brands-400.woff2) format("woff2"),url(../fonts/fa-brands-400.woff) format("woff"),url(../fonts/fa-brands-400.ttf) format("truetype"),url(../fonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:"Font Awesome 5 Brands"}@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:400;font-display:auto;src:url(../fonts/fa-regular-400.eot);src:url(../fonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../fonts/fa-regular-400.woff2) format("woff2"),url(../fonts/fa-regular-400.woff) format("woff"),url(../fonts/fa-regular-400.ttf) format("truetype"),url(../fonts/fa-regular-400.svg#fontawesome) format("svg")}.far{font-weight:400}@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:auto;src:url(../fonts/fa-solid-900.eot);src:url(../fonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../fonts/fa-solid-900.woff2) format("woff2"),url(../fonts/fa-solid-900.woff) format("woff"),url(../fonts/fa-solid-900.ttf) format("truetype"),url(../fonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.far,.fas{font-family:"Font Awesome 5 Free"}.fa,.fas{font-weight:900}
\ No newline at end of file
+.fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adobe:before{content:"\f778"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asterisk:before{content:"\f069"}.fa-asymmetrik:before{content:"\f372"}.fa-at:before{content:"\f1fa"}.fa-atlas:before{content:"\f558"}.fa-atlassian:before{content:"\f77b"}.fa-atom:before{content:"\f5d2"}.fa-audible:before{content:"\f373"}.fa-audio-description:before{content:"\f29e"}.fa-autoprefixer:before{content:"\f41c"}.fa-avianex:before{content:"\f374"}.fa-aviato:before{content:"\f421"}.fa-award:before{content:"\f559"}.fa-aws:before{content:"\f375"}.fa-baby:before{content:"\f77c"}.fa-baby-carriage:before{content:"\f77d"}.fa-backspace:before{content:"\f55a"}.fa-backward:before{content:"\f04a"}.fa-bacon:before{content:"\f7e5"}.fa-bahai:before{content:"\f666"}.fa-balance-scale:before{content:"\f24e"}.fa-balance-scale-left:before{content:"\f515"}.fa-balance-scale-right:before{content:"\f516"}.fa-ban:before{content:"\f05e"}.fa-band-aid:before{content:"\f462"}.fa-bandcamp:before{content:"\f2d5"}.fa-barcode:before{content:"\f02a"}.fa-bars:before{content:"\f0c9"}.fa-baseball-ball:before{content:"\f433"}.fa-basketball-ball:before{content:"\f434"}.fa-bath:before{content:"\f2cd"}.fa-battery-empty:before{content:"\f244"}.fa-battery-full:before{content:"\f240"}.fa-battery-half:before{content:"\f242"}.fa-battery-quarter:before{content:"\f243"}.fa-battery-three-quarters:before{content:"\f241"}.fa-battle-net:before{content:"\f835"}.fa-bed:before{content:"\f236"}.fa-beer:before{content:"\f0fc"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-bell:before{content:"\f0f3"}.fa-bell-slash:before{content:"\f1f6"}.fa-bezier-curve:before{content:"\f55b"}.fa-bible:before{content:"\f647"}.fa-bicycle:before{content:"\f206"}.fa-biking:before{content:"\f84a"}.fa-bimobject:before{content:"\f378"}.fa-binoculars:before{content:"\f1e5"}.fa-biohazard:before{content:"\f780"}.fa-birthday-cake:before{content:"\f1fd"}.fa-bitbucket:before{content:"\f171"}.fa-bitcoin:before{content:"\f379"}.fa-bity:before{content:"\f37a"}.fa-black-tie:before{content:"\f27e"}.fa-blackberry:before{content:"\f37b"}.fa-blender:before{content:"\f517"}.fa-blender-phone:before{content:"\f6b6"}.fa-blind:before{content:"\f29d"}.fa-blog:before{content:"\f781"}.fa-blogger:before{content:"\f37c"}.fa-blogger-b:before{content:"\f37d"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-bold:before{content:"\f032"}.fa-bolt:before{content:"\f0e7"}.fa-bomb:before{content:"\f1e2"}.fa-bone:before{content:"\f5d7"}.fa-bong:before{content:"\f55c"}.fa-book:before{content:"\f02d"}.fa-book-dead:before{content:"\f6b7"}.fa-book-medical:before{content:"\f7e6"}.fa-book-open:before{content:"\f518"}.fa-book-reader:before{content:"\f5da"}.fa-bookmark:before{content:"\f02e"}.fa-bootstrap:before{content:"\f836"}.fa-border-all:before{content:"\f84c"}.fa-border-none:before{content:"\f850"}.fa-border-style:before{content:"\f853"}.fa-bowling-ball:before{content:"\f436"}.fa-box:before{content:"\f466"}.fa-box-open:before{content:"\f49e"}.fa-boxes:before{content:"\f468"}.fa-braille:before{content:"\f2a1"}.fa-brain:before{content:"\f5dc"}.fa-bread-slice:before{content:"\f7ec"}.fa-briefcase:before{content:"\f0b1"}.fa-briefcase-medical:before{content:"\f469"}.fa-broadcast-tower:before{content:"\f519"}.fa-broom:before{content:"\f51a"}.fa-brush:before{content:"\f55d"}.fa-btc:before{content:"\f15a"}.fa-buffer:before{content:"\f837"}.fa-bug:before{content:"\f188"}.fa-building:before{content:"\f1ad"}.fa-bullhorn:before{content:"\f0a1"}.fa-bullseye:before{content:"\f140"}.fa-burn:before{content:"\f46a"}.fa-buromobelexperte:before{content:"\f37f"}.fa-bus:before{content:"\f207"}.fa-bus-alt:before{content:"\f55e"}.fa-business-time:before{content:"\f64a"}.fa-buy-n-large:before{content:"\f8a6"}.fa-buysellads:before{content:"\f20d"}.fa-calculator:before{content:"\f1ec"}.fa-calendar:before{content:"\f133"}.fa-calendar-alt:before{content:"\f073"}.fa-calendar-check:before{content:"\f274"}.fa-calendar-day:before{content:"\f783"}.fa-calendar-minus:before{content:"\f272"}.fa-calendar-plus:before{content:"\f271"}.fa-calendar-times:before{content:"\f273"}.fa-calendar-week:before{content:"\f784"}.fa-camera:before{content:"\f030"}.fa-camera-retro:before{content:"\f083"}.fa-campground:before{content:"\f6bb"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-candy-cane:before{content:"\f786"}.fa-cannabis:before{content:"\f55f"}.fa-capsules:before{content:"\f46b"}.fa-car:before{content:"\f1b9"}.fa-car-alt:before{content:"\f5de"}.fa-car-battery:before{content:"\f5df"}.fa-car-crash:before{content:"\f5e1"}.fa-car-side:before{content:"\f5e4"}.fa-caravan:before{content:"\f8ff"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-caret-square-down:before{content:"\f150"}.fa-caret-square-left:before{content:"\f191"}.fa-caret-square-right:before{content:"\f152"}.fa-caret-square-up:before{content:"\f151"}.fa-caret-up:before{content:"\f0d8"}.fa-carrot:before{content:"\f787"}.fa-cart-arrow-down:before{content:"\f218"}.fa-cart-plus:before{content:"\f217"}.fa-cash-register:before{content:"\f788"}.fa-cat:before{content:"\f6be"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-apple-pay:before{content:"\f416"}.fa-cc-diners-club:before{content:"\f24c"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-cc-visa:before{content:"\f1f0"}.fa-centercode:before{content:"\f380"}.fa-centos:before{content:"\f789"}.fa-certificate:before{content:"\f0a3"}.fa-chair:before{content:"\f6c0"}.fa-chalkboard:before{content:"\f51b"}.fa-chalkboard-teacher:before{content:"\f51c"}.fa-charging-station:before{content:"\f5e7"}.fa-chart-area:before{content:"\f1fe"}.fa-chart-bar:before{content:"\f080"}.fa-chart-line:before{content:"\f201"}.fa-chart-pie:before{content:"\f200"}.fa-check:before{content:"\f00c"}.fa-check-circle:before{content:"\f058"}.fa-check-double:before{content:"\f560"}.fa-check-square:before{content:"\f14a"}.fa-cheese:before{content:"\f7ef"}.fa-chess:before{content:"\f439"}.fa-chess-bishop:before{content:"\f43a"}.fa-chess-board:before{content:"\f43c"}.fa-chess-king:before{content:"\f43f"}.fa-chess-knight:before{content:"\f441"}.fa-chess-pawn:before{content:"\f443"}.fa-chess-queen:before{content:"\f445"}.fa-chess-rook:before{content:"\f447"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-down:before{content:"\f078"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-chevron-up:before{content:"\f077"}.fa-child:before{content:"\f1ae"}.fa-chrome:before{content:"\f268"}.fa-chromecast:before{content:"\f838"}.fa-church:before{content:"\f51d"}.fa-circle:before{content:"\f111"}.fa-circle-notch:before{content:"\f1ce"}.fa-city:before{content:"\f64f"}.fa-clinic-medical:before{content:"\f7f2"}.fa-clipboard:before{content:"\f328"}.fa-clipboard-check:before{content:"\f46c"}.fa-clipboard-list:before{content:"\f46d"}.fa-clock:before{content:"\f017"}.fa-clone:before{content:"\f24d"}.fa-closed-captioning:before{content:"\f20a"}.fa-cloud:before{content:"\f0c2"}.fa-cloud-download-alt:before{content:"\f381"}.fa-cloud-meatball:before{content:"\f73b"}.fa-cloud-moon:before{content:"\f6c3"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-cloud-rain:before{content:"\f73d"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-cloud-sun:before{content:"\f6c4"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-cloud-upload-alt:before{content:"\f382"}.fa-cloudscale:before{content:"\f383"}.fa-cloudsmith:before{content:"\f384"}.fa-cloudversify:before{content:"\f385"}.fa-cocktail:before{content:"\f561"}.fa-code:before{content:"\f121"}.fa-code-branch:before{content:"\f126"}.fa-codepen:before{content:"\f1cb"}.fa-codiepie:before{content:"\f284"}.fa-coffee:before{content:"\f0f4"}.fa-cog:before{content:"\f013"}.fa-cogs:before{content:"\f085"}.fa-coins:before{content:"\f51e"}.fa-columns:before{content:"\f0db"}.fa-comment:before{content:"\f075"}.fa-comment-alt:before{content:"\f27a"}.fa-comment-dollar:before{content:"\f651"}.fa-comment-dots:before{content:"\f4ad"}.fa-comment-medical:before{content:"\f7f5"}.fa-comment-slash:before{content:"\f4b3"}.fa-comments:before{content:"\f086"}.fa-comments-dollar:before{content:"\f653"}.fa-compact-disc:before{content:"\f51f"}.fa-compass:before{content:"\f14e"}.fa-compress:before{content:"\f066"}.fa-compress-alt:before{content:"\f422"}.fa-compress-arrows-alt:before{content:"\f78c"}.fa-concierge-bell:before{content:"\f562"}.fa-confluence:before{content:"\f78d"}.fa-connectdevelop:before{content:"\f20e"}.fa-contao:before{content:"\f26d"}.fa-cookie:before{content:"\f563"}.fa-cookie-bite:before{content:"\f564"}.fa-copy:before{content:"\f0c5"}.fa-copyright:before{content:"\f1f9"}.fa-cotton-bureau:before{content:"\f89e"}.fa-couch:before{content:"\f4b8"}.fa-cpanel:before{content:"\f388"}.fa-creative-commons:before{content:"\f25e"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-credit-card:before{content:"\f09d"}.fa-critical-role:before{content:"\f6c9"}.fa-crop:before{content:"\f125"}.fa-crop-alt:before{content:"\f565"}.fa-cross:before{content:"\f654"}.fa-crosshairs:before{content:"\f05b"}.fa-crow:before{content:"\f520"}.fa-crown:before{content:"\f521"}.fa-crutch:before{content:"\f7f7"}.fa-css3:before{content:"\f13c"}.fa-css3-alt:before{content:"\f38b"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-cut:before{content:"\f0c4"}.fa-cuttlefish:before{content:"\f38c"}.fa-d-and-d:before{content:"\f38d"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-dashcube:before{content:"\f210"}.fa-database:before{content:"\f1c0"}.fa-deaf:before{content:"\f2a4"}.fa-delicious:before{content:"\f1a5"}.fa-democrat:before{content:"\f747"}.fa-deploydog:before{content:"\f38e"}.fa-deskpro:before{content:"\f38f"}.fa-desktop:before{content:"\f108"}.fa-dev:before{content:"\f6cc"}.fa-deviantart:before{content:"\f1bd"}.fa-dharmachakra:before{content:"\f655"}.fa-dhl:before{content:"\f790"}.fa-diagnoses:before{content:"\f470"}.fa-diaspora:before{content:"\f791"}.fa-dice:before{content:"\f522"}.fa-dice-d20:before{content:"\f6cf"}.fa-dice-d6:before{content:"\f6d1"}.fa-dice-five:before{content:"\f523"}.fa-dice-four:before{content:"\f524"}.fa-dice-one:before{content:"\f525"}.fa-dice-six:before{content:"\f526"}.fa-dice-three:before{content:"\f527"}.fa-dice-two:before{content:"\f528"}.fa-digg:before{content:"\f1a6"}.fa-digital-ocean:before{content:"\f391"}.fa-digital-tachograph:before{content:"\f566"}.fa-directions:before{content:"\f5eb"}.fa-discord:before{content:"\f392"}.fa-discourse:before{content:"\f393"}.fa-divide:before{content:"\f529"}.fa-dizzy:before{content:"\f567"}.fa-dna:before{content:"\f471"}.fa-dochub:before{content:"\f394"}.fa-docker:before{content:"\f395"}.fa-dog:before{content:"\f6d3"}.fa-dollar-sign:before{content:"\f155"}.fa-dolly:before{content:"\f472"}.fa-dolly-flatbed:before{content:"\f474"}.fa-donate:before{content:"\f4b9"}.fa-door-closed:before{content:"\f52a"}.fa-door-open:before{content:"\f52b"}.fa-dot-circle:before{content:"\f192"}.fa-dove:before{content:"\f4ba"}.fa-download:before{content:"\f019"}.fa-draft2digital:before{content:"\f396"}.fa-drafting-compass:before{content:"\f568"}.fa-dragon:before{content:"\f6d5"}.fa-draw-polygon:before{content:"\f5ee"}.fa-dribbble:before{content:"\f17d"}.fa-dribbble-square:before{content:"\f397"}.fa-dropbox:before{content:"\f16b"}.fa-drum:before{content:"\f569"}.fa-drum-steelpan:before{content:"\f56a"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-drupal:before{content:"\f1a9"}.fa-dumbbell:before{content:"\f44b"}.fa-dumpster:before{content:"\f793"}.fa-dumpster-fire:before{content:"\f794"}.fa-dungeon:before{content:"\f6d9"}.fa-dyalog:before{content:"\f399"}.fa-earlybirds:before{content:"\f39a"}.fa-ebay:before{content:"\f4f4"}.fa-edge:before{content:"\f282"}.fa-edit:before{content:"\f044"}.fa-egg:before{content:"\f7fb"}.fa-eject:before{content:"\f052"}.fa-elementor:before{content:"\f430"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-ello:before{content:"\f5f1"}.fa-ember:before{content:"\f423"}.fa-empire:before{content:"\f1d1"}.fa-envelope:before{content:"\f0e0"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-text:before{content:"\f658"}.fa-envelope-square:before{content:"\f199"}.fa-envira:before{content:"\f299"}.fa-equals:before{content:"\f52c"}.fa-eraser:before{content:"\f12d"}.fa-erlang:before{content:"\f39d"}.fa-ethereum:before{content:"\f42e"}.fa-ethernet:before{content:"\f796"}.fa-etsy:before{content:"\f2d7"}.fa-euro-sign:before{content:"\f153"}.fa-evernote:before{content:"\f839"}.fa-exchange-alt:before{content:"\f362"}.fa-exclamation:before{content:"\f12a"}.fa-exclamation-circle:before{content:"\f06a"}.fa-exclamation-triangle:before{content:"\f071"}.fa-expand:before{content:"\f065"}.fa-expand-alt:before{content:"\f424"}.fa-expand-arrows-alt:before{content:"\f31e"}.fa-expeditedssl:before{content:"\f23e"}.fa-external-link-alt:before{content:"\f35d"}.fa-external-link-square-alt:before{content:"\f360"}.fa-eye:before{content:"\f06e"}.fa-eye-dropper:before{content:"\f1fb"}.fa-eye-slash:before{content:"\f070"}.fa-facebook:before{content:"\f09a"}.fa-facebook-f:before{content:"\f39e"}.fa-facebook-messenger:before{content:"\f39f"}.fa-facebook-square:before{content:"\f082"}.fa-fan:before{content:"\f863"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-fast-backward:before{content:"\f049"}.fa-fast-forward:before{content:"\f050"}.fa-fax:before{content:"\f1ac"}.fa-feather:before{content:"\f52d"}.fa-feather-alt:before{content:"\f56b"}.fa-fedex:before{content:"\f797"}.fa-fedora:before{content:"\f798"}.fa-female:before{content:"\f182"}.fa-fighter-jet:before{content:"\f0fb"}.fa-figma:before{content:"\f799"}.fa-file:before{content:"\f15b"}.fa-file-alt:before{content:"\f15c"}.fa-file-archive:before{content:"\f1c6"}.fa-file-audio:before{content:"\f1c7"}.fa-file-code:before{content:"\f1c9"}.fa-file-contract:before{content:"\f56c"}.fa-file-csv:before{content:"\f6dd"}.fa-file-download:before{content:"\f56d"}.fa-file-excel:before{content:"\f1c3"}.fa-file-export:before{content:"\f56e"}.fa-file-image:before{content:"\f1c5"}.fa-file-import:before{content:"\f56f"}.fa-file-invoice:before{content:"\f570"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-file-medical:before{content:"\f477"}.fa-file-medical-alt:before{content:"\f478"}.fa-file-pdf:before{content:"\f1c1"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-file-prescription:before{content:"\f572"}.fa-file-signature:before{content:"\f573"}.fa-file-upload:before{content:"\f574"}.fa-file-video:before{content:"\f1c8"}.fa-file-word:before{content:"\f1c2"}.fa-fill:before{content:"\f575"}.fa-fill-drip:before{content:"\f576"}.fa-film:before{content:"\f008"}.fa-filter:before{content:"\f0b0"}.fa-fingerprint:before{content:"\f577"}.fa-fire:before{content:"\f06d"}.fa-fire-alt:before{content:"\f7e4"}.fa-fire-extinguisher:before{content:"\f134"}.fa-firefox:before{content:"\f269"}.fa-firefox-browser:before{content:"\f907"}.fa-first-aid:before{content:"\f479"}.fa-first-order:before{content:"\f2b0"}.fa-first-order-alt:before{content:"\f50a"}.fa-firstdraft:before{content:"\f3a1"}.fa-fish:before{content:"\f578"}.fa-fist-raised:before{content:"\f6de"}.fa-flag:before{content:"\f024"}.fa-flag-checkered:before{content:"\f11e"}.fa-flag-usa:before{content:"\f74d"}.fa-flask:before{content:"\f0c3"}.fa-flickr:before{content:"\f16e"}.fa-flipboard:before{content:"\f44d"}.fa-flushed:before{content:"\f579"}.fa-fly:before{content:"\f417"}.fa-folder:before{content:"\f07b"}.fa-folder-minus:before{content:"\f65d"}.fa-folder-open:before{content:"\f07c"}.fa-folder-plus:before{content:"\f65e"}.fa-font:before{content:"\f031"}.fa-font-awesome:before{content:"\f2b4"}.fa-font-awesome-alt:before{content:"\f35c"}.fa-font-awesome-flag:before{content:"\f425"}.fa-font-awesome-logo-full:before{content:"\f4e6"}.fa-fonticons:before{content:"\f280"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-football-ball:before{content:"\f44e"}.fa-fort-awesome:before{content:"\f286"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-forumbee:before{content:"\f211"}.fa-forward:before{content:"\f04e"}.fa-foursquare:before{content:"\f180"}.fa-free-code-camp:before{content:"\f2c5"}.fa-freebsd:before{content:"\f3a4"}.fa-frog:before{content:"\f52e"}.fa-frown:before{content:"\f119"}.fa-frown-open:before{content:"\f57a"}.fa-fulcrum:before{content:"\f50b"}.fa-funnel-dollar:before{content:"\f662"}.fa-futbol:before{content:"\f1e3"}.fa-galactic-republic:before{content:"\f50c"}.fa-galactic-senate:before{content:"\f50d"}.fa-gamepad:before{content:"\f11b"}.fa-gas-pump:before{content:"\f52f"}.fa-gavel:before{content:"\f0e3"}.fa-gem:before{content:"\f3a5"}.fa-genderless:before{content:"\f22d"}.fa-get-pocket:before{content:"\f265"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-ghost:before{content:"\f6e2"}.fa-gift:before{content:"\f06b"}.fa-gifts:before{content:"\f79c"}.fa-git:before{content:"\f1d3"}.fa-git-alt:before{content:"\f841"}.fa-git-square:before{content:"\f1d2"}.fa-github:before{content:"\f09b"}.fa-github-alt:before{content:"\f113"}.fa-github-square:before{content:"\f092"}.fa-gitkraken:before{content:"\f3a6"}.fa-gitlab:before{content:"\f296"}.fa-gitter:before{content:"\f426"}.fa-glass-cheers:before{content:"\f79f"}.fa-glass-martini:before{content:"\f000"}.fa-glass-martini-alt:before{content:"\f57b"}.fa-glass-whiskey:before{content:"\f7a0"}.fa-glasses:before{content:"\f530"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-globe:before{content:"\f0ac"}.fa-globe-africa:before{content:"\f57c"}.fa-globe-americas:before{content:"\f57d"}.fa-globe-asia:before{content:"\f57e"}.fa-globe-europe:before{content:"\f7a2"}.fa-gofore:before{content:"\f3a7"}.fa-golf-ball:before{content:"\f450"}.fa-goodreads:before{content:"\f3a8"}.fa-goodreads-g:before{content:"\f3a9"}.fa-google:before{content:"\f1a0"}.fa-google-drive:before{content:"\f3aa"}.fa-google-play:before{content:"\f3ab"}.fa-google-plus:before{content:"\f2b3"}.fa-google-plus-g:before{content:"\f0d5"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-wallet:before{content:"\f1ee"}.fa-gopuram:before{content:"\f664"}.fa-graduation-cap:before{content:"\f19d"}.fa-gratipay:before{content:"\f184"}.fa-grav:before{content:"\f2d6"}.fa-greater-than:before{content:"\f531"}.fa-greater-than-equal:before{content:"\f532"}.fa-grimace:before{content:"\f57f"}.fa-grin:before{content:"\f580"}.fa-grin-alt:before{content:"\f581"}.fa-grin-beam:before{content:"\f582"}.fa-grin-beam-sweat:before{content:"\f583"}.fa-grin-hearts:before{content:"\f584"}.fa-grin-squint:before{content:"\f585"}.fa-grin-squint-tears:before{content:"\f586"}.fa-grin-stars:before{content:"\f587"}.fa-grin-tears:before{content:"\f588"}.fa-grin-tongue:before{content:"\f589"}.fa-grin-tongue-squint:before{content:"\f58a"}.fa-grin-tongue-wink:before{content:"\f58b"}.fa-grin-wink:before{content:"\f58c"}.fa-grip-horizontal:before{content:"\f58d"}.fa-grip-lines:before{content:"\f7a4"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-grip-vertical:before{content:"\f58e"}.fa-gripfire:before{content:"\f3ac"}.fa-grunt:before{content:"\f3ad"}.fa-guitar:before{content:"\f7a6"}.fa-gulp:before{content:"\f3ae"}.fa-h-square:before{content:"\f0fd"}.fa-hacker-news:before{content:"\f1d4"}.fa-hacker-news-square:before{content:"\f3af"}.fa-hackerrank:before{content:"\f5f7"}.fa-hamburger:before{content:"\f805"}.fa-hammer:before{content:"\f6e3"}.fa-hamsa:before{content:"\f665"}.fa-hand-holding:before{content:"\f4bd"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-hand-holding-usd:before{content:"\f4c0"}.fa-hand-lizard:before{content:"\f258"}.fa-hand-middle-finger:before{content:"\f806"}.fa-hand-paper:before{content:"\f256"}.fa-hand-peace:before{content:"\f25b"}.fa-hand-point-down:before{content:"\f0a7"}.fa-hand-point-left:before{content:"\f0a5"}.fa-hand-point-right:before{content:"\f0a4"}.fa-hand-point-up:before{content:"\f0a6"}.fa-hand-pointer:before{content:"\f25a"}.fa-hand-rock:before{content:"\f255"}.fa-hand-scissors:before{content:"\f257"}.fa-hand-spock:before{content:"\f259"}.fa-hands:before{content:"\f4c2"}.fa-hands-helping:before{content:"\f4c4"}.fa-handshake:before{content:"\f2b5"}.fa-hanukiah:before{content:"\f6e6"}.fa-hard-hat:before{content:"\f807"}.fa-hashtag:before{content:"\f292"}.fa-hat-cowboy:before{content:"\f8c0"}.fa-hat-cowboy-side:before{content:"\f8c1"}.fa-hat-wizard:before{content:"\f6e8"}.fa-hdd:before{content:"\f0a0"}.fa-heading:before{content:"\f1dc"}.fa-headphones:before{content:"\f025"}.fa-headphones-alt:before{content:"\f58f"}.fa-headset:before{content:"\f590"}.fa-heart:before{content:"\f004"}.fa-heart-broken:before{content:"\f7a9"}.fa-heartbeat:before{content:"\f21e"}.fa-helicopter:before{content:"\f533"}.fa-highlighter:before{content:"\f591"}.fa-hiking:before{content:"\f6ec"}.fa-hippo:before{content:"\f6ed"}.fa-hips:before{content:"\f452"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-history:before{content:"\f1da"}.fa-hockey-puck:before{content:"\f453"}.fa-holly-berry:before{content:"\f7aa"}.fa-home:before{content:"\f015"}.fa-hooli:before{content:"\f427"}.fa-hornbill:before{content:"\f592"}.fa-horse:before{content:"\f6f0"}.fa-horse-head:before{content:"\f7ab"}.fa-hospital:before{content:"\f0f8"}.fa-hospital-alt:before{content:"\f47d"}.fa-hospital-symbol:before{content:"\f47e"}.fa-hot-tub:before{content:"\f593"}.fa-hotdog:before{content:"\f80f"}.fa-hotel:before{content:"\f594"}.fa-hotjar:before{content:"\f3b1"}.fa-hourglass:before{content:"\f254"}.fa-hourglass-end:before{content:"\f253"}.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-start:before{content:"\f251"}.fa-house-damage:before{content:"\f6f1"}.fa-houzz:before{content:"\f27c"}.fa-hryvnia:before{content:"\f6f2"}.fa-html5:before{content:"\f13b"}.fa-hubspot:before{content:"\f3b2"}.fa-i-cursor:before{content:"\f246"}.fa-ice-cream:before{content:"\f810"}.fa-icicles:before{content:"\f7ad"}.fa-icons:before{content:"\f86d"}.fa-id-badge:before{content:"\f2c1"}.fa-id-card:before{content:"\f2c2"}.fa-id-card-alt:before{content:"\f47f"}.fa-ideal:before{content:"\f913"}.fa-igloo:before{content:"\f7ae"}.fa-image:before{content:"\f03e"}.fa-images:before{content:"\f302"}.fa-imdb:before{content:"\f2d8"}.fa-inbox:before{content:"\f01c"}.fa-indent:before{content:"\f03c"}.fa-industry:before{content:"\f275"}.fa-infinity:before{content:"\f534"}.fa-info:before{content:"\f129"}.fa-info-circle:before{content:"\f05a"}.fa-instagram:before{content:"\f16d"}.fa-intercom:before{content:"\f7af"}.fa-internet-explorer:before{content:"\f26b"}.fa-invision:before{content:"\f7b0"}.fa-ioxhost:before{content:"\f208"}.fa-italic:before{content:"\f033"}.fa-itch-io:before{content:"\f83a"}.fa-itunes:before{content:"\f3b4"}.fa-itunes-note:before{content:"\f3b5"}.fa-java:before{content:"\f4e4"}.fa-jedi:before{content:"\f669"}.fa-jedi-order:before{content:"\f50e"}.fa-jenkins:before{content:"\f3b6"}.fa-jira:before{content:"\f7b1"}.fa-joget:before{content:"\f3b7"}.fa-joint:before{content:"\f595"}.fa-joomla:before{content:"\f1aa"}.fa-journal-whills:before{content:"\f66a"}.fa-js:before{content:"\f3b8"}.fa-js-square:before{content:"\f3b9"}.fa-jsfiddle:before{content:"\f1cc"}.fa-kaaba:before{content:"\f66b"}.fa-kaggle:before{content:"\f5fa"}.fa-key:before{content:"\f084"}.fa-keybase:before{content:"\f4f5"}.fa-keyboard:before{content:"\f11c"}.fa-keycdn:before{content:"\f3ba"}.fa-khanda:before{content:"\f66d"}.fa-kickstarter:before{content:"\f3bb"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-kiss:before{content:"\f596"}.fa-kiss-beam:before{content:"\f597"}.fa-kiss-wink-heart:before{content:"\f598"}.fa-kiwi-bird:before{content:"\f535"}.fa-korvue:before{content:"\f42f"}.fa-landmark:before{content:"\f66f"}.fa-language:before{content:"\f1ab"}.fa-laptop:before{content:"\f109"}.fa-laptop-code:before{content:"\f5fc"}.fa-laptop-medical:before{content:"\f812"}.fa-laravel:before{content:"\f3bd"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-laugh:before{content:"\f599"}.fa-laugh-beam:before{content:"\f59a"}.fa-laugh-squint:before{content:"\f59b"}.fa-laugh-wink:before{content:"\f59c"}.fa-layer-group:before{content:"\f5fd"}.fa-leaf:before{content:"\f06c"}.fa-leanpub:before{content:"\f212"}.fa-lemon:before{content:"\f094"}.fa-less:before{content:"\f41d"}.fa-less-than:before{content:"\f536"}.fa-less-than-equal:before{content:"\f537"}.fa-level-down-alt:before{content:"\f3be"}.fa-level-up-alt:before{content:"\f3bf"}.fa-life-ring:before{content:"\f1cd"}.fa-lightbulb:before{content:"\f0eb"}.fa-line:before{content:"\f3c0"}.fa-link:before{content:"\f0c1"}.fa-linkedin:before{content:"\f08c"}.fa-linkedin-in:before{content:"\f0e1"}.fa-linode:before{content:"\f2b8"}.fa-linux:before{content:"\f17c"}.fa-lira-sign:before{content:"\f195"}.fa-list:before{content:"\f03a"}.fa-list-alt:before{content:"\f022"}.fa-list-ol:before{content:"\f0cb"}.fa-list-ul:before{content:"\f0ca"}.fa-location-arrow:before{content:"\f124"}.fa-lock:before{content:"\f023"}.fa-lock-open:before{content:"\f3c1"}.fa-long-arrow-alt-down:before{content:"\f309"}.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-long-arrow-alt-right:before{content:"\f30b"}.fa-long-arrow-alt-up:before{content:"\f30c"}.fa-low-vision:before{content:"\f2a8"}.fa-luggage-cart:before{content:"\f59d"}.fa-lyft:before{content:"\f3c3"}.fa-magento:before{content:"\f3c4"}.fa-magic:before{content:"\f0d0"}.fa-magnet:before{content:"\f076"}.fa-mail-bulk:before{content:"\f674"}.fa-mailchimp:before{content:"\f59e"}.fa-male:before{content:"\f183"}.fa-mandalorian:before{content:"\f50f"}.fa-map:before{content:"\f279"}.fa-map-marked:before{content:"\f59f"}.fa-map-marked-alt:before{content:"\f5a0"}.fa-map-marker:before{content:"\f041"}.fa-map-marker-alt:before{content:"\f3c5"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-markdown:before{content:"\f60f"}.fa-marker:before{content:"\f5a1"}.fa-mars:before{content:"\f222"}.fa-mars-double:before{content:"\f227"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mask:before{content:"\f6fa"}.fa-mastodon:before{content:"\f4f6"}.fa-maxcdn:before{content:"\f136"}.fa-mdb:before{content:"\f8ca"}.fa-medal:before{content:"\f5a2"}.fa-medapps:before{content:"\f3c6"}.fa-medium:before{content:"\f23a"}.fa-medium-m:before{content:"\f3c7"}.fa-medkit:before{content:"\f0fa"}.fa-medrt:before{content:"\f3c8"}.fa-meetup:before{content:"\f2e0"}.fa-megaport:before{content:"\f5a3"}.fa-meh:before{content:"\f11a"}.fa-meh-blank:before{content:"\f5a4"}.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-memory:before{content:"\f538"}.fa-mendeley:before{content:"\f7b3"}.fa-menorah:before{content:"\f676"}.fa-mercury:before{content:"\f223"}.fa-meteor:before{content:"\f753"}.fa-microblog:before{content:"\f91a"}.fa-microchip:before{content:"\f2db"}.fa-microphone:before{content:"\f130"}.fa-microphone-alt:before{content:"\f3c9"}.fa-microphone-alt-slash:before{content:"\f539"}.fa-microphone-slash:before{content:"\f131"}.fa-microscope:before{content:"\f610"}.fa-microsoft:before{content:"\f3ca"}.fa-minus:before{content:"\f068"}.fa-minus-circle:before{content:"\f056"}.fa-minus-square:before{content:"\f146"}.fa-mitten:before{content:"\f7b5"}.fa-mix:before{content:"\f3cb"}.fa-mixcloud:before{content:"\f289"}.fa-mizuni:before{content:"\f3cc"}.fa-mobile:before{content:"\f10b"}.fa-mobile-alt:before{content:"\f3cd"}.fa-modx:before{content:"\f285"}.fa-monero:before{content:"\f3d0"}.fa-money-bill:before{content:"\f0d6"}.fa-money-bill-alt:before{content:"\f3d1"}.fa-money-bill-wave:before{content:"\f53a"}.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-money-check:before{content:"\f53c"}.fa-money-check-alt:before{content:"\f53d"}.fa-monument:before{content:"\f5a6"}.fa-moon:before{content:"\f186"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-mosque:before{content:"\f678"}.fa-motorcycle:before{content:"\f21c"}.fa-mountain:before{content:"\f6fc"}.fa-mouse:before{content:"\f8cc"}.fa-mouse-pointer:before{content:"\f245"}.fa-mug-hot:before{content:"\f7b6"}.fa-music:before{content:"\f001"}.fa-napster:before{content:"\f3d2"}.fa-neos:before{content:"\f612"}.fa-network-wired:before{content:"\f6ff"}.fa-neuter:before{content:"\f22c"}.fa-newspaper:before{content:"\f1ea"}.fa-nimblr:before{content:"\f5a8"}.fa-node:before{content:"\f419"}.fa-node-js:before{content:"\f3d3"}.fa-not-equal:before{content:"\f53e"}.fa-notes-medical:before{content:"\f481"}.fa-npm:before{content:"\f3d4"}.fa-ns8:before{content:"\f3d5"}.fa-nutritionix:before{content:"\f3d6"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-oil-can:before{content:"\f613"}.fa-old-republic:before{content:"\f510"}.fa-om:before{content:"\f679"}.fa-opencart:before{content:"\f23d"}.fa-openid:before{content:"\f19b"}.fa-opera:before{content:"\f26a"}.fa-optin-monster:before{content:"\f23c"}.fa-orcid:before{content:"\f8d2"}.fa-osi:before{content:"\f41a"}.fa-otter:before{content:"\f700"}.fa-outdent:before{content:"\f03b"}.fa-page4:before{content:"\f3d7"}.fa-pagelines:before{content:"\f18c"}.fa-pager:before{content:"\f815"}.fa-paint-brush:before{content:"\f1fc"}.fa-paint-roller:before{content:"\f5aa"}.fa-palette:before{content:"\f53f"}.fa-palfed:before{content:"\f3d8"}.fa-pallet:before{content:"\f482"}.fa-paper-plane:before{content:"\f1d8"}.fa-paperclip:before{content:"\f0c6"}.fa-parachute-box:before{content:"\f4cd"}.fa-paragraph:before{content:"\f1dd"}.fa-parking:before{content:"\f540"}.fa-passport:before{content:"\f5ab"}.fa-pastafarianism:before{content:"\f67b"}.fa-paste:before{content:"\f0ea"}.fa-patreon:before{content:"\f3d9"}.fa-pause:before{content:"\f04c"}.fa-pause-circle:before{content:"\f28b"}.fa-paw:before{content:"\f1b0"}.fa-paypal:before{content:"\f1ed"}.fa-peace:before{content:"\f67c"}.fa-pen:before{content:"\f304"}.fa-pen-alt:before{content:"\f305"}.fa-pen-fancy:before{content:"\f5ac"}.fa-pen-nib:before{content:"\f5ad"}.fa-pen-square:before{content:"\f14b"}.fa-pencil-alt:before{content:"\f303"}.fa-pencil-ruler:before{content:"\f5ae"}.fa-penny-arcade:before{content:"\f704"}.fa-people-carry:before{content:"\f4ce"}.fa-pepper-hot:before{content:"\f816"}.fa-percent:before{content:"\f295"}.fa-percentage:before{content:"\f541"}.fa-periscope:before{content:"\f3da"}.fa-person-booth:before{content:"\f756"}.fa-phabricator:before{content:"\f3db"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-phoenix-squadron:before{content:"\f511"}.fa-phone:before{content:"\f095"}.fa-phone-alt:before{content:"\f879"}.fa-phone-slash:before{content:"\f3dd"}.fa-phone-square:before{content:"\f098"}.fa-phone-square-alt:before{content:"\f87b"}.fa-phone-volume:before{content:"\f2a0"}.fa-photo-video:before{content:"\f87c"}.fa-php:before{content:"\f457"}.fa-pied-piper:before{content:"\f2ae"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-square:before{content:"\f91e"}.fa-piggy-bank:before{content:"\f4d3"}.fa-pills:before{content:"\f484"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-p:before{content:"\f231"}.fa-pinterest-square:before{content:"\f0d3"}.fa-pizza-slice:before{content:"\f818"}.fa-place-of-worship:before{content:"\f67f"}.fa-plane:before{content:"\f072"}.fa-plane-arrival:before{content:"\f5af"}.fa-plane-departure:before{content:"\f5b0"}.fa-play:before{content:"\f04b"}.fa-play-circle:before{content:"\f144"}.fa-playstation:before{content:"\f3df"}.fa-plug:before{content:"\f1e6"}.fa-plus:before{content:"\f067"}.fa-plus-circle:before{content:"\f055"}.fa-plus-square:before{content:"\f0fe"}.fa-podcast:before{content:"\f2ce"}.fa-poll:before{content:"\f681"}.fa-poll-h:before{content:"\f682"}.fa-poo:before{content:"\f2fe"}.fa-poo-storm:before{content:"\f75a"}.fa-poop:before{content:"\f619"}.fa-portrait:before{content:"\f3e0"}.fa-pound-sign:before{content:"\f154"}.fa-power-off:before{content:"\f011"}.fa-pray:before{content:"\f683"}.fa-praying-hands:before{content:"\f684"}.fa-prescription:before{content:"\f5b1"}.fa-prescription-bottle:before{content:"\f485"}.fa-prescription-bottle-alt:before{content:"\f486"}.fa-print:before{content:"\f02f"}.fa-procedures:before{content:"\f487"}.fa-product-hunt:before{content:"\f288"}.fa-project-diagram:before{content:"\f542"}.fa-pushed:before{content:"\f3e1"}.fa-puzzle-piece:before{content:"\f12e"}.fa-python:before{content:"\f3e2"}.fa-qq:before{content:"\f1d6"}.fa-qrcode:before{content:"\f029"}.fa-question:before{content:"\f128"}.fa-question-circle:before{content:"\f059"}.fa-quidditch:before{content:"\f458"}.fa-quinscape:before{content:"\f459"}.fa-quora:before{content:"\f2c4"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-quran:before{content:"\f687"}.fa-r-project:before{content:"\f4f7"}.fa-radiation:before{content:"\f7b9"}.fa-radiation-alt:before{content:"\f7ba"}.fa-rainbow:before{content:"\f75b"}.fa-random:before{content:"\f074"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-ravelry:before{content:"\f2d9"}.fa-react:before{content:"\f41b"}.fa-reacteurope:before{content:"\f75d"}.fa-readme:before{content:"\f4d5"}.fa-rebel:before{content:"\f1d0"}.fa-receipt:before{content:"\f543"}.fa-record-vinyl:before{content:"\f8d9"}.fa-recycle:before{content:"\f1b8"}.fa-red-river:before{content:"\f3e3"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-alien:before{content:"\f281"}.fa-reddit-square:before{content:"\f1a2"}.fa-redhat:before{content:"\f7bc"}.fa-redo:before{content:"\f01e"}.fa-redo-alt:before{content:"\f2f9"}.fa-registered:before{content:"\f25d"}.fa-remove-format:before{content:"\f87d"}.fa-renren:before{content:"\f18b"}.fa-reply:before{content:"\f3e5"}.fa-reply-all:before{content:"\f122"}.fa-replyd:before{content:"\f3e6"}.fa-republican:before{content:"\f75e"}.fa-researchgate:before{content:"\f4f8"}.fa-resolving:before{content:"\f3e7"}.fa-restroom:before{content:"\f7bd"}.fa-retweet:before{content:"\f079"}.fa-rev:before{content:"\f5b2"}.fa-ribbon:before{content:"\f4d6"}.fa-ring:before{content:"\f70b"}.fa-road:before{content:"\f018"}.fa-robot:before{content:"\f544"}.fa-rocket:before{content:"\f135"}.fa-rocketchat:before{content:"\f3e8"}.fa-rockrms:before{content:"\f3e9"}.fa-route:before{content:"\f4d7"}.fa-rss:before{content:"\f09e"}.fa-rss-square:before{content:"\f143"}.fa-ruble-sign:before{content:"\f158"}.fa-ruler:before{content:"\f545"}.fa-ruler-combined:before{content:"\f546"}.fa-ruler-horizontal:before{content:"\f547"}.fa-ruler-vertical:before{content:"\f548"}.fa-running:before{content:"\f70c"}.fa-rupee-sign:before{content:"\f156"}.fa-sad-cry:before{content:"\f5b3"}.fa-sad-tear:before{content:"\f5b4"}.fa-safari:before{content:"\f267"}.fa-salesforce:before{content:"\f83b"}.fa-sass:before{content:"\f41e"}.fa-satellite:before{content:"\f7bf"}.fa-satellite-dish:before{content:"\f7c0"}.fa-save:before{content:"\f0c7"}.fa-schlix:before{content:"\f3ea"}.fa-school:before{content:"\f549"}.fa-screwdriver:before{content:"\f54a"}.fa-scribd:before{content:"\f28a"}.fa-scroll:before{content:"\f70e"}.fa-sd-card:before{content:"\f7c2"}.fa-search:before{content:"\f002"}.fa-search-dollar:before{content:"\f688"}.fa-search-location:before{content:"\f689"}.fa-search-minus:before{content:"\f010"}.fa-search-plus:before{content:"\f00e"}.fa-searchengin:before{content:"\f3eb"}.fa-seedling:before{content:"\f4d8"}.fa-sellcast:before{content:"\f2da"}.fa-sellsy:before{content:"\f213"}.fa-server:before{content:"\f233"}.fa-servicestack:before{content:"\f3ec"}.fa-shapes:before{content:"\f61f"}.fa-share:before{content:"\f064"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-share-square:before{content:"\f14d"}.fa-shekel-sign:before{content:"\f20b"}.fa-shield-alt:before{content:"\f3ed"}.fa-ship:before{content:"\f21a"}.fa-shipping-fast:before{content:"\f48b"}.fa-shirtsinbulk:before{content:"\f214"}.fa-shoe-prints:before{content:"\f54b"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-shopping-cart:before{content:"\f07a"}.fa-shopware:before{content:"\f5b5"}.fa-shower:before{content:"\f2cc"}.fa-shuttle-van:before{content:"\f5b6"}.fa-sign:before{content:"\f4d9"}.fa-sign-in-alt:before{content:"\f2f6"}.fa-sign-language:before{content:"\f2a7"}.fa-sign-out-alt:before{content:"\f2f5"}.fa-signal:before{content:"\f012"}.fa-signature:before{content:"\f5b7"}.fa-sim-card:before{content:"\f7c4"}.fa-simplybuilt:before{content:"\f215"}.fa-sistrix:before{content:"\f3ee"}.fa-sitemap:before{content:"\f0e8"}.fa-sith:before{content:"\f512"}.fa-skating:before{content:"\f7c5"}.fa-sketch:before{content:"\f7c6"}.fa-skiing:before{content:"\f7c9"}.fa-skiing-nordic:before{content:"\f7ca"}.fa-skull:before{content:"\f54c"}.fa-skull-crossbones:before{content:"\f714"}.fa-skyatlas:before{content:"\f216"}.fa-skype:before{content:"\f17e"}.fa-slack:before{content:"\f198"}.fa-slack-hash:before{content:"\f3ef"}.fa-slash:before{content:"\f715"}.fa-sleigh:before{content:"\f7cc"}.fa-sliders-h:before{content:"\f1de"}.fa-slideshare:before{content:"\f1e7"}.fa-smile:before{content:"\f118"}.fa-smile-beam:before{content:"\f5b8"}.fa-smile-wink:before{content:"\f4da"}.fa-smog:before{content:"\f75f"}.fa-smoking:before{content:"\f48d"}.fa-smoking-ban:before{content:"\f54d"}.fa-sms:before{content:"\f7cd"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-snowboarding:before{content:"\f7ce"}.fa-snowflake:before{content:"\f2dc"}.fa-snowman:before{content:"\f7d0"}.fa-snowplow:before{content:"\f7d2"}.fa-socks:before{content:"\f696"}.fa-solar-panel:before{content:"\f5ba"}.fa-sort:before{content:"\f0dc"}.fa-sort-alpha-down:before{content:"\f15d"}.fa-sort-alpha-down-alt:before{content:"\f881"}.fa-sort-alpha-up:before{content:"\f15e"}.fa-sort-alpha-up-alt:before{content:"\f882"}.fa-sort-amount-down:before{content:"\f160"}.fa-sort-amount-down-alt:before{content:"\f884"}.fa-sort-amount-up:before{content:"\f161"}.fa-sort-amount-up-alt:before{content:"\f885"}.fa-sort-down:before{content:"\f0dd"}.fa-sort-numeric-down:before{content:"\f162"}.fa-sort-numeric-down-alt:before{content:"\f886"}.fa-sort-numeric-up:before{content:"\f163"}.fa-sort-numeric-up-alt:before{content:"\f887"}.fa-sort-up:before{content:"\f0de"}.fa-soundcloud:before{content:"\f1be"}.fa-sourcetree:before{content:"\f7d3"}.fa-spa:before{content:"\f5bb"}.fa-space-shuttle:before{content:"\f197"}.fa-speakap:before{content:"\f3f3"}.fa-speaker-deck:before{content:"\f83c"}.fa-spell-check:before{content:"\f891"}.fa-spider:before{content:"\f717"}.fa-spinner:before{content:"\f110"}.fa-splotch:before{content:"\f5bc"}.fa-spotify:before{content:"\f1bc"}.fa-spray-can:before{content:"\f5bd"}.fa-square:before{content:"\f0c8"}.fa-square-full:before{content:"\f45c"}.fa-square-root-alt:before{content:"\f698"}.fa-squarespace:before{content:"\f5be"}.fa-stack-exchange:before{content:"\f18d"}.fa-stack-overflow:before{content:"\f16c"}.fa-stackpath:before{content:"\f842"}.fa-stamp:before{content:"\f5bf"}.fa-star:before{content:"\f005"}.fa-star-and-crescent:before{content:"\f699"}.fa-star-half:before{content:"\f089"}.fa-star-half-alt:before{content:"\f5c0"}.fa-star-of-david:before{content:"\f69a"}.fa-star-of-life:before{content:"\f621"}.fa-staylinked:before{content:"\f3f5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-steam-symbol:before{content:"\f3f6"}.fa-step-backward:before{content:"\f048"}.fa-step-forward:before{content:"\f051"}.fa-stethoscope:before{content:"\f0f1"}.fa-sticker-mule:before{content:"\f3f7"}.fa-sticky-note:before{content:"\f249"}.fa-stop:before{content:"\f04d"}.fa-stop-circle:before{content:"\f28d"}.fa-stopwatch:before{content:"\f2f2"}.fa-store:before{content:"\f54e"}.fa-store-alt:before{content:"\f54f"}.fa-strava:before{content:"\f428"}.fa-stream:before{content:"\f550"}.fa-street-view:before{content:"\f21d"}.fa-strikethrough:before{content:"\f0cc"}.fa-stripe:before{content:"\f429"}.fa-stripe-s:before{content:"\f42a"}.fa-stroopwafel:before{content:"\f551"}.fa-studiovinari:before{content:"\f3f8"}.fa-stumbleupon:before{content:"\f1a4"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-subscript:before{content:"\f12c"}.fa-subway:before{content:"\f239"}.fa-suitcase:before{content:"\f0f2"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-sun:before{content:"\f185"}.fa-superpowers:before{content:"\f2dd"}.fa-superscript:before{content:"\f12b"}.fa-supple:before{content:"\f3f9"}.fa-surprise:before{content:"\f5c2"}.fa-suse:before{content:"\f7d6"}.fa-swatchbook:before{content:"\f5c3"}.fa-swift:before{content:"\f8e1"}.fa-swimmer:before{content:"\f5c4"}.fa-swimming-pool:before{content:"\f5c5"}.fa-symfony:before{content:"\f83d"}.fa-synagogue:before{content:"\f69b"}.fa-sync:before{content:"\f021"}.fa-sync-alt:before{content:"\f2f1"}.fa-syringe:before{content:"\f48e"}.fa-table:before{content:"\f0ce"}.fa-table-tennis:before{content:"\f45d"}.fa-tablet:before{content:"\f10a"}.fa-tablet-alt:before{content:"\f3fa"}.fa-tablets:before{content:"\f490"}.fa-tachometer-alt:before{content:"\f3fd"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-tape:before{content:"\f4db"}.fa-tasks:before{content:"\f0ae"}.fa-taxi:before{content:"\f1ba"}.fa-teamspeak:before{content:"\f4f9"}.fa-teeth:before{content:"\f62e"}.fa-teeth-open:before{content:"\f62f"}.fa-telegram:before{content:"\f2c6"}.fa-telegram-plane:before{content:"\f3fe"}.fa-temperature-high:before{content:"\f769"}.fa-temperature-low:before{content:"\f76b"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-tenge:before{content:"\f7d7"}.fa-terminal:before{content:"\f120"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-th:before{content:"\f00a"}.fa-th-large:before{content:"\f009"}.fa-th-list:before{content:"\f00b"}.fa-the-red-yeti:before{content:"\f69d"}.fa-theater-masks:before{content:"\f630"}.fa-themeco:before{content:"\f5c6"}.fa-themeisle:before{content:"\f2b2"}.fa-thermometer:before{content:"\f491"}.fa-thermometer-empty:before{content:"\f2cb"}.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-think-peaks:before{content:"\f731"}.fa-thumbs-down:before{content:"\f165"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbtack:before{content:"\f08d"}.fa-ticket-alt:before{content:"\f3ff"}.fa-times:before{content:"\f00d"}.fa-times-circle:before{content:"\f057"}.fa-tint:before{content:"\f043"}.fa-tint-slash:before{content:"\f5c7"}.fa-tired:before{content:"\f5c8"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-toilet:before{content:"\f7d8"}.fa-toilet-paper:before{content:"\f71e"}.fa-toolbox:before{content:"\f552"}.fa-tools:before{content:"\f7d9"}.fa-tooth:before{content:"\f5c9"}.fa-torah:before{content:"\f6a0"}.fa-torii-gate:before{content:"\f6a1"}.fa-tractor:before{content:"\f722"}.fa-trade-federation:before{content:"\f513"}.fa-trademark:before{content:"\f25c"}.fa-traffic-light:before{content:"\f637"}.fa-trailer:before{content:"\f941"}.fa-train:before{content:"\f238"}.fa-tram:before{content:"\f7da"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-trash:before{content:"\f1f8"}.fa-trash-alt:before{content:"\f2ed"}.fa-trash-restore:before{content:"\f829"}.fa-trash-restore-alt:before{content:"\f82a"}.fa-tree:before{content:"\f1bb"}.fa-trello:before{content:"\f181"}.fa-tripadvisor:before{content:"\f262"}.fa-trophy:before{content:"\f091"}.fa-truck:before{content:"\f0d1"}.fa-truck-loading:before{content:"\f4de"}.fa-truck-monster:before{content:"\f63b"}.fa-truck-moving:before{content:"\f4df"}.fa-truck-pickup:before{content:"\f63c"}.fa-tshirt:before{content:"\f553"}.fa-tty:before{content:"\f1e4"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-tv:before{content:"\f26c"}.fa-twitch:before{content:"\f1e8"}.fa-twitter:before{content:"\f099"}.fa-twitter-square:before{content:"\f081"}.fa-typo3:before{content:"\f42b"}.fa-uber:before{content:"\f402"}.fa-ubuntu:before{content:"\f7df"}.fa-uikit:before{content:"\f403"}.fa-umbraco:before{content:"\f8e8"}.fa-umbrella:before{content:"\f0e9"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-underline:before{content:"\f0cd"}.fa-undo:before{content:"\f0e2"}.fa-undo-alt:before{content:"\f2ea"}.fa-uniregistry:before{content:"\f404"}.fa-unity:before{content:"\f949"}.fa-universal-access:before{content:"\f29a"}.fa-university:before{content:"\f19c"}.fa-unlink:before{content:"\f127"}.fa-unlock:before{content:"\f09c"}.fa-unlock-alt:before{content:"\f13e"}.fa-untappd:before{content:"\f405"}.fa-upload:before{content:"\f093"}.fa-ups:before{content:"\f7e0"}.fa-usb:before{content:"\f287"}.fa-user:before{content:"\f007"}.fa-user-alt:before{content:"\f406"}.fa-user-alt-slash:before{content:"\f4fa"}.fa-user-astronaut:before{content:"\f4fb"}.fa-user-check:before{content:"\f4fc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-clock:before{content:"\f4fd"}.fa-user-cog:before{content:"\f4fe"}.fa-user-edit:before{content:"\f4ff"}.fa-user-friends:before{content:"\f500"}.fa-user-graduate:before{content:"\f501"}.fa-user-injured:before{content:"\f728"}.fa-user-lock:before{content:"\f502"}.fa-user-md:before{content:"\f0f0"}.fa-user-minus:before{content:"\f503"}.fa-user-ninja:before{content:"\f504"}.fa-user-nurse:before{content:"\f82f"}.fa-user-plus:before{content:"\f234"}.fa-user-secret:before{content:"\f21b"}.fa-user-shield:before{content:"\f505"}.fa-user-slash:before{content:"\f506"}.fa-user-tag:before{content:"\f507"}.fa-user-tie:before{content:"\f508"}.fa-user-times:before{content:"\f235"}.fa-users:before{content:"\f0c0"}.fa-users-cog:before{content:"\f509"}.fa-usps:before{content:"\f7e1"}.fa-ussunnah:before{content:"\f407"}.fa-utensil-spoon:before{content:"\f2e5"}.fa-utensils:before{content:"\f2e7"}.fa-vaadin:before{content:"\f408"}.fa-vector-square:before{content:"\f5cb"}.fa-venus:before{content:"\f221"}.fa-venus-double:before{content:"\f226"}.fa-venus-mars:before{content:"\f228"}.fa-viacoin:before{content:"\f237"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-vial:before{content:"\f492"}.fa-vials:before{content:"\f493"}.fa-viber:before{content:"\f409"}.fa-video:before{content:"\f03d"}.fa-video-slash:before{content:"\f4e2"}.fa-vihara:before{content:"\f6a7"}.fa-vimeo:before{content:"\f40a"}.fa-vimeo-square:before{content:"\f194"}.fa-vimeo-v:before{content:"\f27d"}.fa-vine:before{content:"\f1ca"}.fa-vk:before{content:"\f189"}.fa-vnv:before{content:"\f40b"}.fa-voicemail:before{content:"\f897"}.fa-volleyball-ball:before{content:"\f45f"}.fa-volume-down:before{content:"\f027"}.fa-volume-mute:before{content:"\f6a9"}.fa-volume-off:before{content:"\f026"}.fa-volume-up:before{content:"\f028"}.fa-vote-yea:before{content:"\f772"}.fa-vr-cardboard:before{content:"\f729"}.fa-vuejs:before{content:"\f41f"}.fa-walking:before{content:"\f554"}.fa-wallet:before{content:"\f555"}.fa-warehouse:before{content:"\f494"}.fa-water:before{content:"\f773"}.fa-wave-square:before{content:"\f83e"}.fa-waze:before{content:"\f83f"}.fa-weebly:before{content:"\f5cc"}.fa-weibo:before{content:"\f18a"}.fa-weight:before{content:"\f496"}.fa-weight-hanging:before{content:"\f5cd"}.fa-weixin:before{content:"\f1d7"}.fa-whatsapp:before{content:"\f232"}.fa-whatsapp-square:before{content:"\f40c"}.fa-wheelchair:before{content:"\f193"}.fa-whmcs:before{content:"\f40d"}.fa-wifi:before{content:"\f1eb"}.fa-wikipedia-w:before{content:"\f266"}.fa-wind:before{content:"\f72e"}.fa-window-close:before{content:"\f410"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-windows:before{content:"\f17a"}.fa-wine-bottle:before{content:"\f72f"}.fa-wine-glass:before{content:"\f4e3"}.fa-wine-glass-alt:before{content:"\f5ce"}.fa-wix:before{content:"\f5cf"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-won-sign:before{content:"\f159"}.fa-wordpress:before{content:"\f19a"}.fa-wordpress-simple:before{content:"\f411"}.fa-wpbeginner:before{content:"\f297"}.fa-wpexplorer:before{content:"\f2de"}.fa-wpforms:before{content:"\f298"}.fa-wpressr:before{content:"\f3e4"}.fa-wrench:before{content:"\f0ad"}.fa-x-ray:before{content:"\f497"}.fa-xbox:before{content:"\f412"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-y-combinator:before{content:"\f23b"}.fa-yahoo:before{content:"\f19e"}.fa-yammer:before{content:"\f840"}.fa-yandex:before{content:"\f413"}.fa-yandex-international:before{content:"\f414"}.fa-yarn:before{content:"\f7e3"}.fa-yelp:before{content:"\f1e9"}.fa-yen-sign:before{content:"\f157"}.fa-yin-yang:before{content:"\f6ad"}.fa-yoast:before{content:"\f2b1"}.fa-youtube:before{content:"\f167"}.fa-youtube-square:before{content:"\f431"}.fa-zhihu:before{content:"\f63f"}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}@font-face{font-family:"Font Awesome 5 Brands";font-style:normal;font-weight:normal;font-display:auto;src:url(fa-brands-400.eot);src:url(fa-brands-400.eot?#iefix) format("embedded-opentype"),url(fa-brands-400.woff2) format("woff2"),url(fa-brands-400.woff) format("woff"),url(fa-brands-400.ttf) format("truetype"),url(fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:"Font Awesome 5 Brands"}@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:400;font-display:auto;src:url(fa-regular-400.eot);src:url(fa-regular-400.eot?#iefix) format("embedded-opentype"),url(fa-regular-400.woff2) format("woff2"),url(fa-regular-400.woff) format("woff"),url(fa-regular-400.ttf) format("truetype"),url(fa-regular-400.svg#fontawesome) format("svg")}.far{font-weight:400}@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:auto;src:url(fa-solid-900.eot);src:url(fa-solid-900.eot?#iefix) format("embedded-opentype"),url(fa-solid-900.woff2) format("woff2"),url(fa-solid-900.woff) format("woff"),url(fa-solid-900.ttf) format("truetype"),url(fa-solid-900.svg#fontawesome) format("svg")}.fa,.far,.fas{font-family:"Font Awesome 5 Free"}.fa,.fas{font-weight:900}
\ No newline at end of file
diff --git a/assets/fonts/fontawesome-webfont.eot b/internal/app/api/core/assets/fonts/fontawesome-webfont.eot
similarity index 100%
rename from assets/fonts/fontawesome-webfont.eot
rename to internal/app/api/core/assets/fonts/fontawesome-webfont.eot
diff --git a/assets/fonts/fontawesome-webfont.svg b/internal/app/api/core/assets/fonts/fontawesome-webfont.svg
similarity index 100%
rename from assets/fonts/fontawesome-webfont.svg
rename to internal/app/api/core/assets/fonts/fontawesome-webfont.svg
diff --git a/assets/fonts/fontawesome-webfont.ttf b/internal/app/api/core/assets/fonts/fontawesome-webfont.ttf
similarity index 100%
rename from assets/fonts/fontawesome-webfont.ttf
rename to internal/app/api/core/assets/fonts/fontawesome-webfont.ttf
diff --git a/assets/fonts/fontawesome-webfont.woff b/internal/app/api/core/assets/fonts/fontawesome-webfont.woff
similarity index 100%
rename from assets/fonts/fontawesome-webfont.woff
rename to internal/app/api/core/assets/fonts/fontawesome-webfont.woff
diff --git a/assets/fonts/fontawesome-webfont.woff2 b/internal/app/api/core/assets/fonts/fontawesome-webfont.woff2
similarity index 100%
rename from assets/fonts/fontawesome-webfont.woff2
rename to internal/app/api/core/assets/fonts/fontawesome-webfont.woff2
diff --git a/assets/fonts/fontawesome5-overrides.min.css b/internal/app/api/core/assets/fonts/fontawesome5-overrides.min.css
similarity index 100%
rename from assets/fonts/fontawesome5-overrides.min.css
rename to internal/app/api/core/assets/fonts/fontawesome5-overrides.min.css
diff --git a/internal/app/api/core/assets/img/header-logo-small.png b/internal/app/api/core/assets/img/header-logo-small.png
new file mode 100644
index 0000000..4743535
Binary files /dev/null and b/internal/app/api/core/assets/img/header-logo-small.png differ
diff --git a/internal/app/api/core/assets/img/header-logo.png b/internal/app/api/core/assets/img/header-logo.png
new file mode 100644
index 0000000..7df31d7
Binary files /dev/null and b/internal/app/api/core/assets/img/header-logo.png differ
diff --git a/assets/js/bootstrap.bundle.min.js b/internal/app/api/core/assets/js/bootstrap.bundle.min.js
similarity index 100%
rename from assets/js/bootstrap.bundle.min.js
rename to internal/app/api/core/assets/js/bootstrap.bundle.min.js
diff --git a/assets/js/jquery.easing.js b/internal/app/api/core/assets/js/jquery.easing.js
similarity index 100%
rename from assets/js/jquery.easing.js
rename to internal/app/api/core/assets/js/jquery.easing.js
diff --git a/assets/js/jquery.min.js b/internal/app/api/core/assets/js/jquery.min.js
similarity index 100%
rename from assets/js/jquery.min.js
rename to internal/app/api/core/assets/js/jquery.min.js
diff --git a/assets/js/popper.min.js b/internal/app/api/core/assets/js/popper.min.js
similarity index 100%
rename from assets/js/popper.min.js
rename to internal/app/api/core/assets/js/popper.min.js
diff --git a/internal/app/api/core/assets/js/rapidoc-min.js b/internal/app/api/core/assets/js/rapidoc-min.js
new file mode 100644
index 0000000..6a28927
--- /dev/null
+++ b/internal/app/api/core/assets/js/rapidoc-min.js
@@ -0,0 +1,2 @@
+/*! RapiDoc 9.1.8 | Author - Mrinmoy Majumdar | License information can be found in rapidoc-min.js.LICENSE.txt */
+(()=>{var e,t,r={310:(e,t,r)=>{"use strict";const n=new WeakMap,a=e=>(...t)=>{const r=e(...t);return n.set(r,!0),r},o=e=>"function"==typeof e&&n.has(e),i="undefined"!=typeof window&&null!=window.customElements&&void 0!==window.customElements.polyfillWrapFlushCallback,s=(e,t,r=null)=>{for(;t!==r;){const r=t.nextSibling;e.removeChild(t),t=r}},l={},c={},p=`{{lit-${String(Math.random()).slice(2)}}}`,u=`\x3c!--${p}--\x3e`,d=new RegExp(`${p}|${u}`),h="$lit$";class f{constructor(e,t){this.parts=[],this.element=t;const r=[],n=[],a=document.createTreeWalker(t.content,133,null,!1);let o=0,i=-1,s=0;const{strings:l,values:{length:c}}=e;for(;s
0;){const t=l[s],r=v.exec(t)[2],n=r.toLowerCase()+h,a=e.getAttribute(n);e.removeAttribute(n);const o=a.split(d);this.parts.push({type:"attribute",index:i,name:r,strings:o}),s+=o.length-1}}"TEMPLATE"===e.tagName&&(n.push(e),a.currentNode=e.content)}else if(3===e.nodeType){const t=e.data;if(t.indexOf(p)>=0){const n=e.parentNode,a=t.split(d),o=a.length-1;for(let t=0;t{const r=e.length-t.length;return r>=0&&e.slice(r)===t},y=e=>-1!==e.index,g=()=>document.createComment(""),v=/([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/;class b{constructor(e,t,r){this.__parts=[],this.template=e,this.processor=t,this.options=r}update(e){let t=0;for(const r of this.__parts)void 0!==r&&r.setValue(e[t]),t++;for(const e of this.__parts)void 0!==e&&e.commit()}_clone(){const e=i?this.template.element.content.cloneNode(!0):document.importNode(this.template.element.content,!0),t=[],r=this.template.parts,n=document.createTreeWalker(e,133,null,!1);let a,o=0,s=0,l=n.nextNode();for(;oe}),w=` ${p} `;class S{constructor(e,t,r,n){this.strings=e,this.values=t,this.type=r,this.processor=n}getHTML(){const e=this.strings.length-1;let t="",r=!1;for(let n=0;n-1||r)&&-1===e.indexOf("--\x3e",a+1);const o=v.exec(e);t+=null===o?e+(r?w:u):e.substr(0,o.index)+o[1]+o[2]+h+o[3]+p}return t+=this.strings[e],t}getTemplateElement(){const e=document.createElement("template");let t=this.getHTML();return void 0!==x&&(t=x.createHTML(t)),e.innerHTML=t,e}}const k=e=>null===e||!("object"==typeof e||"function"==typeof e),$=e=>Array.isArray(e)||!(!e||!e[Symbol.iterator]);class A{constructor(e,t,r){this.dirty=!0,this.element=e,this.name=t,this.strings=r,this.parts=[];for(let e=0;e{try{const e={get capture(){return j=!0,!1}};window.addEventListener("test",e,e),window.removeEventListener("test",e,e)}catch(e){}})();class I{constructor(e,t,r){this.value=void 0,this.__pendingValue=void 0,this.element=e,this.eventName=t,this.eventContext=r,this.__boundHandleEvent=e=>this.handleEvent(e)}setValue(e){this.__pendingValue=e}commit(){for(;o(this.__pendingValue);){const e=this.__pendingValue;this.__pendingValue=l,e(this)}if(this.__pendingValue===l)return;const e=this.__pendingValue,t=this.value,r=null==e||null!=t&&(e.capture!==t.capture||e.once!==t.once||e.passive!==t.passive),n=null!=e&&(null==t||r);r&&this.element.removeEventListener(this.eventName,this.__boundHandleEvent,this.__options),n&&(this.__options=P(e),this.element.addEventListener(this.eventName,this.__boundHandleEvent,this.__options)),this.value=e,this.__pendingValue=l}handleEvent(e){"function"==typeof this.value?this.value.call(this.eventContext||this.element,e):this.value.handleEvent(e)}}const P=e=>e&&(j?{capture:e.capture,passive:e.passive,once:e.once}:e.capture);const R=new class{handleAttributeExpressions(e,t,r,n){const a=t[0];if("."===a){return new _(e,t.slice(1),r).parts}if("@"===a)return[new I(e,t.slice(1),n.eventContext)];if("?"===a)return[new T(e,t.slice(1),r)];return new A(e,t,r).parts}handleTextExpression(e){return new O(e)}};function L(e){let t=N.get(e.type);void 0===t&&(t={stringsArray:new WeakMap,keyString:new Map},N.set(e.type,t));let r=t.stringsArray.get(e.strings);if(void 0!==r)return r;const n=e.strings.join(p);return r=t.keyString.get(n),void 0===r&&(r=new f(e,e.getTemplateElement()),t.keyString.set(n,r)),t.stringsArray.set(e.strings,r),r}const N=new Map,F=new WeakMap;"undefined"!=typeof window&&(window.litHtmlVersions||(window.litHtmlVersions=[])).push("1.4.1");const D=(e,...t)=>new S(e,t,"html",R);window.JSCompiler_renameProperty=(e,t)=>e;const B={toAttribute(e,t){switch(t){case Boolean:return e?"":null;case Object:case Array:return null==e?e:JSON.stringify(e)}return e},fromAttribute(e,t){switch(t){case Boolean:return null!==e;case Number:return null===e?null:Number(e);case Object:case Array:return JSON.parse(e)}return e}},z=(e,t)=>t!==e&&(t==t||e==e),q={attribute:!0,type:String,converter:B,reflect:!1,hasChanged:z},U="finalized";class M extends HTMLElement{constructor(){super(),this.initialize()}static get observedAttributes(){this.finalize();const e=[];return this._classProperties.forEach(((t,r)=>{const n=this._attributeNameForProperty(r,t);void 0!==n&&(this._attributeToPropertyMap.set(n,r),e.push(n))})),e}static _ensureClassProperties(){if(!this.hasOwnProperty(JSCompiler_renameProperty("_classProperties",this))){this._classProperties=new Map;const e=Object.getPrototypeOf(this)._classProperties;void 0!==e&&e.forEach(((e,t)=>this._classProperties.set(t,e)))}}static createProperty(e,t=q){if(this._ensureClassProperties(),this._classProperties.set(e,t),t.noAccessor||this.prototype.hasOwnProperty(e))return;const r="symbol"==typeof e?Symbol():`__${e}`,n=this.getPropertyDescriptor(e,r,t);void 0!==n&&Object.defineProperty(this.prototype,e,n)}static getPropertyDescriptor(e,t,r){return{get(){return this[t]},set(n){const a=this[e];this[t]=n,this.requestUpdateInternal(e,a,r)},configurable:!0,enumerable:!0}}static getPropertyOptions(e){return this._classProperties&&this._classProperties.get(e)||q}static finalize(){const e=Object.getPrototypeOf(this);if(e.hasOwnProperty(U)||e.finalize(),this.finalized=!0,this._ensureClassProperties(),this._attributeToPropertyMap=new Map,this.hasOwnProperty(JSCompiler_renameProperty("properties",this))){const e=this.properties,t=[...Object.getOwnPropertyNames(e),..."function"==typeof Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(e):[]];for(const r of t)this.createProperty(r,e[r])}}static _attributeNameForProperty(e,t){const r=t.attribute;return!1===r?void 0:"string"==typeof r?r:"string"==typeof e?e.toLowerCase():void 0}static _valueHasChanged(e,t,r=z){return r(e,t)}static _propertyValueFromAttribute(e,t){const r=t.type,n=t.converter||B,a="function"==typeof n?n:n.fromAttribute;return a?a(e,r):e}static _propertyValueToAttribute(e,t){if(void 0===t.reflect)return;const r=t.type,n=t.converter;return(n&&n.toAttribute||B.toAttribute)(e,r)}initialize(){this._updateState=0,this._updatePromise=new Promise((e=>this._enableUpdatingResolver=e)),this._changedProperties=new Map,this._saveInstanceProperties(),this.requestUpdateInternal()}_saveInstanceProperties(){this.constructor._classProperties.forEach(((e,t)=>{if(this.hasOwnProperty(t)){const e=this[t];delete this[t],this._instanceProperties||(this._instanceProperties=new Map),this._instanceProperties.set(t,e)}}))}_applyInstanceProperties(){this._instanceProperties.forEach(((e,t)=>this[t]=e)),this._instanceProperties=void 0}connectedCallback(){this.enableUpdating()}enableUpdating(){void 0!==this._enableUpdatingResolver&&(this._enableUpdatingResolver(),this._enableUpdatingResolver=void 0)}disconnectedCallback(){}attributeChangedCallback(e,t,r){t!==r&&this._attributeToProperty(e,r)}_propertyToAttribute(e,t,r=q){const n=this.constructor,a=n._attributeNameForProperty(e,r);if(void 0!==a){const e=n._propertyValueToAttribute(t,r);if(void 0===e)return;this._updateState=8|this._updateState,null==e?this.removeAttribute(a):this.setAttribute(a,e),this._updateState=-9&this._updateState}}_attributeToProperty(e,t){if(8&this._updateState)return;const r=this.constructor,n=r._attributeToPropertyMap.get(e);if(void 0!==n){const e=r.getPropertyOptions(n);this._updateState=16|this._updateState,this[n]=r._propertyValueFromAttribute(t,e),this._updateState=-17&this._updateState}}requestUpdateInternal(e,t,r){let n=!0;if(void 0!==e){const a=this.constructor;r=r||a.getPropertyOptions(e),a._valueHasChanged(this[e],t,r.hasChanged)?(this._changedProperties.has(e)||this._changedProperties.set(e,t),!0!==r.reflect||16&this._updateState||(void 0===this._reflectingProperties&&(this._reflectingProperties=new Map),this._reflectingProperties.set(e,r))):n=!1}!this._hasRequestedUpdate&&n&&(this._updatePromise=this._enqueueUpdate())}requestUpdate(e,t){return this.requestUpdateInternal(e,t),this.updateComplete}async _enqueueUpdate(){this._updateState=4|this._updateState;try{await this._updatePromise}catch(e){}const e=this.performUpdate();return null!=e&&await e,!this._hasRequestedUpdate}get _hasRequestedUpdate(){return 4&this._updateState}get hasUpdated(){return 1&this._updateState}performUpdate(){if(!this._hasRequestedUpdate)return;this._instanceProperties&&this._applyInstanceProperties();let e=!1;const t=this._changedProperties;try{e=this.shouldUpdate(t),e?this.update(t):this._markUpdated()}catch(t){throw e=!1,this._markUpdated(),t}e&&(1&this._updateState||(this._updateState=1|this._updateState,this.firstUpdated(t)),this.updated(t))}_markUpdated(){this._changedProperties=new Map,this._updateState=-5&this._updateState}get updateComplete(){return this._getUpdateComplete()}_getUpdateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._updatePromise}shouldUpdate(e){return!0}update(e){void 0!==this._reflectingProperties&&this._reflectingProperties.size>0&&(this._reflectingProperties.forEach(((e,t)=>this._propertyToAttribute(t,this[t],e))),this._reflectingProperties=void 0),this._markUpdated()}updated(e){}firstUpdated(e){}}M.finalized=!0;const H=Element.prototype;H.msMatchesSelector||H.webkitMatchesSelector;const V=window.ShadowRoot&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,W=Symbol();class G{constructor(e,t){if(t!==W)throw new Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=e}get styleSheet(){return void 0===this._styleSheet&&(V?(this._styleSheet=new CSSStyleSheet,this._styleSheet.replaceSync(this.cssText)):this._styleSheet=null),this._styleSheet}toString(){return this.cssText}}const K=e=>new G(String(e),W),J=(e,...t)=>{const r=t.reduce(((t,r,n)=>t+(e=>{if(e instanceof G)return e.cssText;if("number"==typeof e)return e;throw new Error(`Value passed to 'css' function must be a 'css' function result: ${e}. Use 'unsafeCSS' to pass non-literal values, but\n take care to ensure page security.`)})(r)+e[n+1]),e[0]);return new G(r,W)};(window.litElementVersions||(window.litElementVersions=[])).push("2.5.1");const Y={};class Z extends M{static getStyles(){return this.styles}static _getUniqueStyles(){if(this.hasOwnProperty(JSCompiler_renameProperty("_styles",this)))return;const e=this.getStyles();if(Array.isArray(e)){const t=(e,r)=>e.reduceRight(((e,r)=>Array.isArray(r)?t(r,e):(e.add(r),e)),r),r=t(e,new Set),n=[];r.forEach((e=>n.unshift(e))),this._styles=n}else this._styles=void 0===e?[]:[e];this._styles=this._styles.map((e=>{if(e instanceof CSSStyleSheet&&!V){const t=Array.prototype.slice.call(e.cssRules).reduce(((e,t)=>e+t.cssText),"");return K(t)}return e}))}initialize(){super.initialize(),this.constructor._getUniqueStyles(),this.renderRoot=this.createRenderRoot(),window.ShadowRoot&&this.renderRoot instanceof window.ShadowRoot&&this.adoptStyles()}createRenderRoot(){return this.attachShadow(this.constructor.shadowRootOptions)}adoptStyles(){const e=this.constructor._styles;0!==e.length&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow?V?this.renderRoot.adoptedStyleSheets=e.map((e=>e instanceof CSSStyleSheet?e:e.styleSheet)):this._needsShimAdoptedStyleSheets=!0:window.ShadyCSS.ScopingShim.prepareAdoptedCssText(e.map((e=>e.cssText)),this.localName))}connectedCallback(){super.connectedCallback(),this.hasUpdated&&void 0!==window.ShadyCSS&&window.ShadyCSS.styleElement(this)}update(e){const t=this.render();super.update(e),t!==Y&&this.constructor.render(t,this.renderRoot,{scopeName:this.localName,eventContext:this}),this._needsShimAdoptedStyleSheets&&(this._needsShimAdoptedStyleSheets=!1,this.constructor._styles.forEach((e=>{const t=document.createElement("style");t.textContent=e.cssText,this.renderRoot.appendChild(t)})))}render(){return Y}}function Q(){return{baseUrl:null,breaks:!1,extensions:null,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:null,sanitize:!1,sanitizer:null,silent:!1,smartLists:!1,smartypants:!1,tokenizer:null,walkTokens:null,xhtml:!1}}Z.finalized=!0,Z.render=(e,t,r)=>{let n=F.get(t);void 0===n&&(s(t,t.firstChild),F.set(t,n=new O(Object.assign({templateFactory:L},r))),n.appendInto(t)),n.setValue(e),n.commit()},Z.shadowRootOptions={mode:"open"};let X={baseUrl:null,breaks:!1,extensions:null,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:null,sanitize:!1,sanitizer:null,silent:!1,smartLists:!1,smartypants:!1,tokenizer:null,walkTokens:null,xhtml:!1};const ee=/[&<>"']/,te=/[&<>"']/g,re=/[<>"']|&(?!#?\w+;)/,ne=/[<>"']|&(?!#?\w+;)/g,ae={"&":"&","<":"<",">":">",'"':""","'":"'"},oe=e=>ae[e];function ie(e,t){if(t){if(ee.test(e))return e.replace(te,oe)}else if(re.test(e))return e.replace(ne,oe);return e}const se=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;function le(e){return e.replace(se,((e,t)=>"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""))}const ce=/(^|[^\[])\^/g;function pe(e,t){e=e.source||e,t=t||"";const r={replace:(t,n)=>(n=(n=n.source||n).replace(ce,"$1"),e=e.replace(t,n),r),getRegex:()=>new RegExp(e,t)};return r}const ue=/[^\w:]/g,de=/^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;function he(e,t,r){if(e){let e;try{e=decodeURIComponent(le(r)).replace(ue,"").toLowerCase()}catch(e){return null}if(0===e.indexOf("javascript:")||0===e.indexOf("vbscript:")||0===e.indexOf("data:"))return null}t&&!de.test(r)&&(r=function(e,t){fe[" "+e]||(me.test(e)?fe[" "+e]=e+"/":fe[" "+e]=we(e,"/",!0));const r=-1===(e=fe[" "+e]).indexOf(":");return"//"===t.substring(0,2)?r?t:e.replace(ye,"$1")+t:"/"===t.charAt(0)?r?t:e.replace(ge,"$1")+t:e+t}(t,r));try{r=encodeURI(r).replace(/%25/g,"%")}catch(e){return null}return r}const fe={},me=/^[^:]+:\/*[^/]*$/,ye=/^([^:]+:)[\s\S]*$/,ge=/^([^:]+:\/*[^/]*)[\s\S]*$/;const ve={exec:function(){}};function be(e){let t,r,n=1;for(;n{let n=!1,a=t;for(;--a>=0&&"\\"===r[a];)n=!n;return n?"|":" |"})).split(/ \|/);let n=0;if(r[0].trim()||r.shift(),r.length>0&&!r[r.length-1].trim()&&r.pop(),r.length>t)r.splice(t);else for(;r.length1;)1&t&&(r+=e),t>>=1,e+=e;return r+e}function $e(e,t,r,n){const a=t.href,o=t.title?ie(t.title):null,i=e[1].replace(/\\([\[\]])/g,"$1");if("!"!==e[0].charAt(0)){n.state.inLink=!0;const e={type:"link",raw:r,href:a,title:o,text:i,tokens:n.inlineTokens(i,[])};return n.state.inLink=!1,e}return{type:"image",raw:r,href:a,title:o,text:ie(i)}}class Ae{constructor(e){this.options=e||X}space(e){const t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const e=t[0].replace(/^ {1,4}/gm,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?e:we(e,"\n")}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const e=t[0],r=function(e,t){const r=e.match(/^(\s+)(?:```)/);if(null===r)return t;const n=r[1];return t.split("\n").map((e=>{const t=e.match(/^\s+/);if(null===t)return e;const[r]=t;return r.length>=n.length?e.slice(n.length):e})).join("\n")}(e,t[3]||"");return{type:"code",raw:e,lang:t[2]?t[2].trim():t[2],text:r}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(/#$/.test(e)){const t=we(e,"#");this.options.pedantic?e=t.trim():t&&!/ $/.test(t)||(e=t.trim())}const r={type:"heading",raw:t[0],depth:t[1].length,text:e,tokens:[]};return this.lexer.inline(r.text,r.tokens),r}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:t[0]}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){const e=t[0].replace(/^ *> ?/gm,"");return{type:"blockquote",raw:t[0],tokens:this.lexer.blockTokens(e,[]),text:e}}}list(e){let t=this.rules.block.list.exec(e);if(t){let r,n,a,o,i,s,l,c,p,u,d,h,f=t[1].trim();const m=f.length>1,y={type:"list",raw:"",ordered:m,start:m?+f.slice(0,-1):"",loose:!1,items:[]};f=m?`\\d{1,9}\\${f.slice(-1)}`:`\\${f}`,this.options.pedantic&&(f=m?f:"[*+-]");const g=new RegExp(`^( {0,3}${f})((?: [^\\n]*)?(?:\\n|$))`);for(;e&&(h=!1,t=g.exec(e))&&!this.rules.block.hr.test(e);){if(r=t[0],e=e.substring(r.length),c=t[2].split("\n",1)[0],p=e.split("\n",1)[0],this.options.pedantic?(o=2,d=c.trimLeft()):(o=t[2].search(/[^ ]/),o=o>4?1:o,d=c.slice(o),o+=t[1].length),s=!1,!c&&/^ *$/.test(p)&&(r+=p+"\n",e=e.substring(p.length+1),h=!0),!h){const t=new RegExp(`^ {0,${Math.min(3,o-1)}}(?:[*+-]|\\d{1,9}[.)])`);for(;e&&(u=e.split("\n",1)[0],c=u,this.options.pedantic&&(c=c.replace(/^ {1,4}(?=( {4})*[^ ])/g," ")),!t.test(c));){if(c.search(/[^ ]/)>=o||!c.trim())d+="\n"+c.slice(o);else{if(s)break;d+="\n"+c}s||c.trim()||(s=!0),r+=u+"\n",e=e.substring(u.length+1)}}y.loose||(l?y.loose=!0:/\n *\n *$/.test(r)&&(l=!0)),this.options.gfm&&(n=/^\[[ xX]\] /.exec(d),n&&(a="[ ] "!==n[0],d=d.replace(/^\[[ xX]\] +/,""))),y.items.push({type:"list_item",raw:r,task:!!n,checked:a,loose:!1,text:d}),y.raw+=r}y.items[y.items.length-1].raw=r.trimRight(),y.items[y.items.length-1].text=d.trimRight(),y.raw=y.raw.trimRight();const v=y.items.length;for(i=0;i"space"===e.type)),t=e.every((e=>{const t=e.raw.split("");let r=0;for(const e of t)if("\n"===e&&(r+=1),r>1)return!0;return!1}));!y.loose&&e.length&&t&&(y.loose=!0,y.items[i].loose=!0)}return y}}html(e){const t=this.rules.block.html.exec(e);if(t){const e={type:"html",raw:t[0],pre:!this.options.sanitizer&&("pre"===t[1]||"script"===t[1]||"style"===t[1]),text:t[0]};return this.options.sanitize&&(e.type="paragraph",e.text=this.options.sanitizer?this.options.sanitizer(t[0]):ie(t[0]),e.tokens=[],this.lexer.inline(e.text,e.tokens)),e}}def(e){const t=this.rules.block.def.exec(e);if(t){t[3]&&(t[3]=t[3].substring(1,t[3].length-1));return{type:"def",tag:t[1].toLowerCase().replace(/\s+/g," "),raw:t[0],href:t[2],title:t[3]}}}table(e){const t=this.rules.block.table.exec(e);if(t){const e={type:"table",header:xe(t[1]).map((e=>({text:e}))),align:t[2].replace(/^ *|\| *$/g,"").split(/ *\| */),rows:t[3]&&t[3].trim()?t[3].replace(/\n[ \t]*$/,"").split("\n"):[]};if(e.header.length===e.align.length){e.raw=t[0];let r,n,a,o,i=e.align.length;for(r=0;r({text:e})));for(i=e.header.length,n=0;n/i.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:this.options.sanitize?"text":"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,text:this.options.sanitize?this.options.sanitizer?this.options.sanitizer(t[0]):ie(t[0]):t[0]}}link(e){const t=this.rules.inline.link.exec(e);if(t){const e=t[2].trim();if(!this.options.pedantic&&/^$/.test(e))return;const t=we(e.slice(0,-1),"\\");if((e.length-t.length)%2==0)return}else{const e=function(e,t){if(-1===e.indexOf(t[1]))return-1;const r=e.length;let n=0,a=0;for(;a-1){const r=(0===t[0].indexOf("!")?5:4)+t[1].length+e;t[2]=t[2].substring(0,e),t[0]=t[0].substring(0,r).trim(),t[3]=""}}let r=t[2],n="";if(this.options.pedantic){const e=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(r);e&&(r=e[1],n=e[3])}else n=t[3]?t[3].slice(1,-1):"";return r=r.trim(),/^$/.test(e)?r.slice(1):r.slice(1,-1)),$e(t,{href:r?r.replace(this.rules.inline._escapes,"$1"):r,title:n?n.replace(this.rules.inline._escapes,"$1"):n},t[0],this.lexer)}}reflink(e,t){let r;if((r=this.rules.inline.reflink.exec(e))||(r=this.rules.inline.nolink.exec(e))){let e=(r[2]||r[1]).replace(/\s+/g," ");if(e=t[e.toLowerCase()],!e||!e.href){const e=r[0].charAt(0);return{type:"text",raw:e,text:e}}return $e(r,e,r[0],this.lexer)}}emStrong(e,t,r=""){let n=this.rules.inline.emStrong.lDelim.exec(e);if(!n)return;if(n[3]&&r.match(/[\p{L}\p{N}]/u))return;const a=n[1]||n[2]||"";if(!a||a&&(""===r||this.rules.inline.punctuation.exec(r))){const r=n[0].length-1;let a,o,i=r,s=0;const l="*"===n[0][0]?this.rules.inline.emStrong.rDelimAst:this.rules.inline.emStrong.rDelimUnd;for(l.lastIndex=0,t=t.slice(-1*e.length+r);null!=(n=l.exec(t));){if(a=n[1]||n[2]||n[3]||n[4]||n[5]||n[6],!a)continue;if(o=a.length,n[3]||n[4]){i+=o;continue}if((n[5]||n[6])&&r%3&&!((r+o)%3)){s+=o;continue}if(i-=o,i>0)continue;if(o=Math.min(o,o+i+s),Math.min(r,o)%2){const t=e.slice(1,r+n.index+o);return{type:"em",raw:e.slice(0,r+n.index+o+1),text:t,tokens:this.lexer.inlineTokens(t,[])}}const t=e.slice(2,r+n.index+o-1);return{type:"strong",raw:e.slice(0,r+n.index+o+1),text:t,tokens:this.lexer.inlineTokens(t,[])}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let e=t[2].replace(/\n/g," ");const r=/[^ ]/.test(e),n=/^ /.test(e)&&/ $/.test(e);return r&&n&&(e=e.substring(1,e.length-1)),e=ie(e,!0),{type:"codespan",raw:t[0],text:e}}}br(e){const t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){const t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2],[])}}autolink(e,t){const r=this.rules.inline.autolink.exec(e);if(r){let e,n;return"@"===r[2]?(e=ie(this.options.mangle?t(r[1]):r[1]),n="mailto:"+e):(e=ie(r[1]),n=e),{type:"link",raw:r[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}url(e,t){let r;if(r=this.rules.inline.url.exec(e)){let e,n;if("@"===r[2])e=ie(this.options.mangle?t(r[0]):r[0]),n="mailto:"+e;else{let t;do{t=r[0],r[0]=this.rules.inline._backpedal.exec(r[0])[0]}while(t!==r[0]);e=ie(r[0]),n="www."===r[1]?"http://"+e:e}return{type:"link",raw:r[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e,t){const r=this.rules.inline.text.exec(e);if(r){let e;return e=this.lexer.state.inRawBlock?this.options.sanitize?this.options.sanitizer?this.options.sanitizer(r[0]):ie(r[0]):r[0]:ie(this.options.smartypants?t(r[0]):r[0]),{type:"text",raw:r[0],text:e}}}}const Ee={newline:/^(?: *(?:\n|$))+/,code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,fences:/^ {0,3}(`{3,}(?=[^`\n]*\n)|~{3,})([^\n]*)\n(?:|([\s\S]*?)\n)(?: {0,3}\1[~`]* *(?=\n|$)|$)/,hr:/^ {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\* *){3,})(?:\n+|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,blockquote:/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/,list:/^( {0,3}bull)( [^\n]+?)?(?:\n|$)/,html:"^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:\\1>[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?!script|pre|style|textarea)[a-z][\\w-]*\\s*>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))",def:/^ {0,3}\[(label)\]: *(?:\n *)?([^\s>]+)>?(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/,table:ve,lheading:/^([^\n]+)\n {0,3}(=+|-+) *(?:\n+|$)/,_paragraph:/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,text:/^[^\n]+/,_label:/(?!\s*\])(?:\\.|[^\[\]\\])+/,_title:/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/};Ee.def=pe(Ee.def).replace("label",Ee._label).replace("title",Ee._title).getRegex(),Ee.bullet=/(?:[*+-]|\d{1,9}[.)])/,Ee.listItemStart=pe(/^( *)(bull) */).replace("bull",Ee.bullet).getRegex(),Ee.list=pe(Ee.list).replace(/bull/g,Ee.bullet).replace("hr","\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))").replace("def","\\n+(?="+Ee.def.source+")").getRegex(),Ee._tag="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",Ee._comment=/|$)/,Ee.html=pe(Ee.html,"i").replace("comment",Ee._comment).replace("tag",Ee._tag).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),Ee.paragraph=pe(Ee._paragraph).replace("hr",Ee.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",Ee._tag).getRegex(),Ee.blockquote=pe(Ee.blockquote).replace("paragraph",Ee.paragraph).getRegex(),Ee.normal=be({},Ee),Ee.gfm=be({},Ee.normal,{table:"^ *([^\\n ].*\\|.*)\\n {0,3}(?:\\| *)?(:?-+:? *(?:\\| *:?-+:? *)*)(?:\\| *)?(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)"}),Ee.gfm.table=pe(Ee.gfm.table).replace("hr",Ee.hr).replace("heading"," {0,3}#{1,6} ").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",Ee._tag).getRegex(),Ee.gfm.paragraph=pe(Ee._paragraph).replace("hr",Ee.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("table",Ee.gfm.table).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",Ee._tag).getRegex(),Ee.pedantic=be({},Ee.normal,{html:pe("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+?\\1> *(?:\\n{2,}|\\s*$)| \\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",Ee._comment).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *([^\s>]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:ve,paragraph:pe(Ee.normal._paragraph).replace("hr",Ee.hr).replace("heading"," *#{1,6} *[^\n]").replace("lheading",Ee.lheading).replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").getRegex()});const Oe={escape:/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,autolink:/^<(scheme:[^\s\x00-\x1f<>]*|email)>/,url:ve,tag:"^comment|^[a-zA-Z][\\w:-]*\\s*>|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^",link:/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/,reflink:/^!?\[(label)\]\[(ref)\]/,nolink:/^!?\[(ref)\](?:\[\])?/,reflinkSearch:"reflink|nolink(?!\\()",emStrong:{lDelim:/^(?:\*+(?:([punct_])|[^\s*]))|^_+(?:([punct*])|([^\s_]))/,rDelimAst:/^[^_*]*?\_\_[^_*]*?\*[^_*]*?(?=\_\_)|[punct_](\*+)(?=[\s]|$)|[^punct*_\s](\*+)(?=[punct_\s]|$)|[punct_\s](\*+)(?=[^punct*_\s])|[\s](\*+)(?=[punct_])|[punct_](\*+)(?=[punct_])|[^punct*_\s](\*+)(?=[^punct*_\s])/,rDelimUnd:/^[^_*]*?\*\*[^_*]*?\_[^_*]*?(?=\*\*)|[punct*](\_+)(?=[\s]|$)|[^punct*_\s](\_+)(?=[punct*\s]|$)|[punct*\s](\_+)(?=[^punct*_\s])|[\s](\_+)(?=[punct*])|[punct*](\_+)(?=[punct*])/},code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,br:/^( {2,}|\\)\n(?!\s*$)/,del:ve,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\.5&&(r="x"+r.toString(16)),n+=""+r+";";return n}Oe._punctuation="!\"#$%&'()+\\-.,/:;<=>?@\\[\\]`^{|}~",Oe.punctuation=pe(Oe.punctuation).replace(/punctuation/g,Oe._punctuation).getRegex(),Oe.blockSkip=/\[[^\]]*?\]\([^\)]*?\)|`[^`]*?`|<[^>]*?>/g,Oe.escapedEmSt=/\\\*|\\_/g,Oe._comment=pe(Ee._comment).replace("(?:--\x3e|$)","--\x3e").getRegex(),Oe.emStrong.lDelim=pe(Oe.emStrong.lDelim).replace(/punct/g,Oe._punctuation).getRegex(),Oe.emStrong.rDelimAst=pe(Oe.emStrong.rDelimAst,"g").replace(/punct/g,Oe._punctuation).getRegex(),Oe.emStrong.rDelimUnd=pe(Oe.emStrong.rDelimUnd,"g").replace(/punct/g,Oe._punctuation).getRegex(),Oe._escapes=/\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g,Oe._scheme=/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/,Oe._email=/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/,Oe.autolink=pe(Oe.autolink).replace("scheme",Oe._scheme).replace("email",Oe._email).getRegex(),Oe._attribute=/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/,Oe.tag=pe(Oe.tag).replace("comment",Oe._comment).replace("attribute",Oe._attribute).getRegex(),Oe._label=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,Oe._href=/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/,Oe._title=/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/,Oe.link=pe(Oe.link).replace("label",Oe._label).replace("href",Oe._href).replace("title",Oe._title).getRegex(),Oe.reflink=pe(Oe.reflink).replace("label",Oe._label).replace("ref",Ee._label).getRegex(),Oe.nolink=pe(Oe.nolink).replace("ref",Ee._label).getRegex(),Oe.reflinkSearch=pe(Oe.reflinkSearch,"g").replace("reflink",Oe.reflink).replace("nolink",Oe.nolink).getRegex(),Oe.normal=be({},Oe),Oe.pedantic=be({},Oe.normal,{strong:{start:/^__|\*\*/,middle:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,endAst:/\*\*(?!\*)/g,endUnd:/__(?!_)/g},em:{start:/^_|\*/,middle:/^()\*(?=\S)([\s\S]*?\S)\*(?!\*)|^_(?=\S)([\s\S]*?\S)_(?!_)/,endAst:/\*(?!\*)/g,endUnd:/_(?!_)/g},link:pe(/^!?\[(label)\]\((.*?)\)/).replace("label",Oe._label).getRegex(),reflink:pe(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",Oe._label).getRegex()}),Oe.gfm=be({},Oe.normal,{escape:pe(Oe.escape).replace("])","~|])").getRegex(),_extended_email:/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/,url:/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,_backpedal:/(?:[^?!.,:;*_~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\!!(r=n.call({lexer:this},e,t))&&(e=e.substring(r.raw.length),t.push(r),!0)))))if(r=this.tokenizer.space(e))e=e.substring(r.raw.length),1===r.raw.length&&t.length>0?t[t.length-1].raw+="\n":t.push(r);else if(r=this.tokenizer.code(e))e=e.substring(r.raw.length),n=t[t.length-1],!n||"paragraph"!==n.type&&"text"!==n.type?t.push(r):(n.raw+="\n"+r.raw,n.text+="\n"+r.text,this.inlineQueue[this.inlineQueue.length-1].src=n.text);else if(r=this.tokenizer.fences(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.heading(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.hr(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.blockquote(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.list(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.html(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.def(e))e=e.substring(r.raw.length),n=t[t.length-1],!n||"paragraph"!==n.type&&"text"!==n.type?this.tokens.links[r.tag]||(this.tokens.links[r.tag]={href:r.href,title:r.title}):(n.raw+="\n"+r.raw,n.text+="\n"+r.raw,this.inlineQueue[this.inlineQueue.length-1].src=n.text);else if(r=this.tokenizer.table(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.lheading(e))e=e.substring(r.raw.length),t.push(r);else{if(a=e,this.options.extensions&&this.options.extensions.startBlock){let t=1/0;const r=e.slice(1);let n;this.options.extensions.startBlock.forEach((function(e){n=e.call({lexer:this},r),"number"==typeof n&&n>=0&&(t=Math.min(t,n))})),t<1/0&&t>=0&&(a=e.substring(0,t+1))}if(this.state.top&&(r=this.tokenizer.paragraph(a)))n=t[t.length-1],o&&"paragraph"===n.type?(n.raw+="\n"+r.raw,n.text+="\n"+r.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=n.text):t.push(r),o=a.length!==e.length,e=e.substring(r.raw.length);else if(r=this.tokenizer.text(e))e=e.substring(r.raw.length),n=t[t.length-1],n&&"text"===n.type?(n.raw+="\n"+r.raw,n.text+="\n"+r.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=n.text):t.push(r);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return this.state.top=!0,t}inline(e,t){this.inlineQueue.push({src:e,tokens:t})}inlineTokens(e,t=[]){let r,n,a,o,i,s,l=e;if(this.tokens.links){const e=Object.keys(this.tokens.links);if(e.length>0)for(;null!=(o=this.tokenizer.rules.inline.reflinkSearch.exec(l));)e.includes(o[0].slice(o[0].lastIndexOf("[")+1,-1))&&(l=l.slice(0,o.index)+"["+ke("a",o[0].length-2)+"]"+l.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!=(o=this.tokenizer.rules.inline.blockSkip.exec(l));)l=l.slice(0,o.index)+"["+ke("a",o[0].length-2)+"]"+l.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;null!=(o=this.tokenizer.rules.inline.escapedEmSt.exec(l));)l=l.slice(0,o.index)+"++"+l.slice(this.tokenizer.rules.inline.escapedEmSt.lastIndex);for(;e;)if(i||(s=""),i=!1,!(this.options.extensions&&this.options.extensions.inline&&this.options.extensions.inline.some((n=>!!(r=n.call({lexer:this},e,t))&&(e=e.substring(r.raw.length),t.push(r),!0)))))if(r=this.tokenizer.escape(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.tag(e))e=e.substring(r.raw.length),n=t[t.length-1],n&&"text"===r.type&&"text"===n.type?(n.raw+=r.raw,n.text+=r.text):t.push(r);else if(r=this.tokenizer.link(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.reflink(e,this.tokens.links))e=e.substring(r.raw.length),n=t[t.length-1],n&&"text"===r.type&&"text"===n.type?(n.raw+=r.raw,n.text+=r.text):t.push(r);else if(r=this.tokenizer.emStrong(e,l,s))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.codespan(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.br(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.del(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.autolink(e,_e))e=e.substring(r.raw.length),t.push(r);else if(this.state.inLink||!(r=this.tokenizer.url(e,_e))){if(a=e,this.options.extensions&&this.options.extensions.startInline){let t=1/0;const r=e.slice(1);let n;this.options.extensions.startInline.forEach((function(e){n=e.call({lexer:this},r),"number"==typeof n&&n>=0&&(t=Math.min(t,n))})),t<1/0&&t>=0&&(a=e.substring(0,t+1))}if(r=this.tokenizer.inlineText(a,Te))e=e.substring(r.raw.length),"_"!==r.raw.slice(-1)&&(s=r.raw.slice(-1)),i=!0,n=t[t.length-1],n&&"text"===n.type?(n.raw+=r.raw,n.text+=r.text):t.push(r);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}else e=e.substring(r.raw.length),t.push(r);return t}}class je{constructor(e){this.options=e||X}code(e,t,r){const n=(t||"").match(/\S*/)[0];if(this.options.highlight){const t=this.options.highlight(e,n);null!=t&&t!==e&&(r=!0,e=t)}return e=e.replace(/\n$/,"")+"\n",n?''+(r?e:ie(e,!0))+" \n":""+(r?e:ie(e,!0))+" \n"}blockquote(e){return"\n"+e+" \n"}html(e){return e}heading(e,t,r,n){return this.options.headerIds?"\n":""+e+" \n"}hr(){return this.options.xhtml?" \n":" \n"}list(e,t,r){const n=t?"ol":"ul";return"<"+n+(t&&1!==r?' start="'+r+'"':"")+">\n"+e+""+n+">\n"}listitem(e){return""+e+" \n"}checkbox(e){return" "}paragraph(e){return""+e+"
\n"}table(e,t){return t&&(t=""+t+" "),"\n"}tablerow(e){return"\n"+e+" \n"}tablecell(e,t){const r=t.header?"th":"td";return(t.align?"<"+r+' align="'+t.align+'">':"<"+r+">")+e+""+r+">\n"}strong(e){return""+e+" "}em(e){return""+e+" "}codespan(e){return""+e+""}br(){return this.options.xhtml?" ":" "}del(e){return""+e+""}link(e,t,r){if(null===(e=he(this.options.sanitize,this.options.baseUrl,e)))return r;let n='"+r+" ",n}image(e,t,r){if(null===(e=he(this.options.sanitize,this.options.baseUrl,e)))return r;let n=' ":">",n}text(e){return e}}class Ie{strong(e){return e}em(e){return e}codespan(e){return e}del(e){return e}html(e){return e}text(e){return e}link(e,t,r){return""+r}image(e,t,r){return""+r}br(){return""}}class Pe{constructor(){this.seen={}}serialize(e){return e.toLowerCase().trim().replace(/<[!\/a-z].*?>/gi,"").replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g,"").replace(/\s/g,"-")}getNextSafeSlug(e,t){let r=e,n=0;if(this.seen.hasOwnProperty(r)){n=this.seen[e];do{n++,r=e+"-"+n}while(this.seen.hasOwnProperty(r))}return t||(this.seen[e]=n,this.seen[r]=0),r}slug(e,t={}){const r=this.serialize(e);return this.getNextSafeSlug(r,t.dryrun)}}class Re{constructor(e){this.options=e||X,this.options.renderer=this.options.renderer||new je,this.renderer=this.options.renderer,this.renderer.options=this.options,this.textRenderer=new Ie,this.slugger=new Pe}static parse(e,t){return new Re(t).parse(e)}static parseInline(e,t){return new Re(t).parseInline(e)}parse(e,t=!0){let r,n,a,o,i,s,l,c,p,u,d,h,f,m,y,g,v,b,x,w="";const S=e.length;for(r=0;r0&&"paragraph"===y.tokens[0].type?(y.tokens[0].text=b+" "+y.tokens[0].text,y.tokens[0].tokens&&y.tokens[0].tokens.length>0&&"text"===y.tokens[0].tokens[0].type&&(y.tokens[0].tokens[0].text=b+" "+y.tokens[0].tokens[0].text)):y.tokens.unshift({type:"text",text:b}):m+=b),m+=this.parse(y.tokens,f),p+=this.renderer.listitem(m,v,g);w+=this.renderer.list(p,d,h);continue;case"html":w+=this.renderer.html(u.text);continue;case"paragraph":w+=this.renderer.paragraph(this.parseInline(u.tokens));continue;case"text":for(p=u.tokens?this.parseInline(u.tokens):u.text;r+1{n(e.text,e.lang,(function(t,r){if(t)return o(t);null!=r&&r!==e.text&&(e.text=r,e.escaped=!0),i--,0===i&&o()}))}),0))})),void(0===i&&o())}try{const r=Ce.lex(e,t);return t.walkTokens&&Le.walkTokens(r,t.walkTokens),Re.parse(r,t)}catch(e){if(e.message+="\nPlease report this to https://github.com/markedjs/marked.",t.silent)return" An error occurred:
"+ie(e.message+"",!0)+" ";throw e}}Le.options=Le.setOptions=function(e){var t;return be(Le.defaults,e),t=Le.defaults,X=t,Le},Le.getDefaults=Q,Le.defaults=X,Le.use=function(...e){const t=be({},...e),r=Le.defaults.extensions||{renderers:{},childTokens:{}};let n;e.forEach((e=>{if(e.extensions&&(n=!0,e.extensions.forEach((e=>{if(!e.name)throw new Error("extension name required");if(e.renderer){const t=r.renderers?r.renderers[e.name]:null;r.renderers[e.name]=t?function(...r){let n=e.renderer.apply(this,r);return!1===n&&(n=t.apply(this,r)),n}:e.renderer}if(e.tokenizer){if(!e.level||"block"!==e.level&&"inline"!==e.level)throw new Error("extension level must be 'block' or 'inline'");r[e.level]?r[e.level].unshift(e.tokenizer):r[e.level]=[e.tokenizer],e.start&&("block"===e.level?r.startBlock?r.startBlock.push(e.start):r.startBlock=[e.start]:"inline"===e.level&&(r.startInline?r.startInline.push(e.start):r.startInline=[e.start]))}e.childTokens&&(r.childTokens[e.name]=e.childTokens)}))),e.renderer){const r=Le.defaults.renderer||new je;for(const t in e.renderer){const n=r[t];r[t]=(...a)=>{let o=e.renderer[t].apply(r,a);return!1===o&&(o=n.apply(r,a)),o}}t.renderer=r}if(e.tokenizer){const r=Le.defaults.tokenizer||new Ae;for(const t in e.tokenizer){const n=r[t];r[t]=(...a)=>{let o=e.tokenizer[t].apply(r,a);return!1===o&&(o=n.apply(r,a)),o}}t.tokenizer=r}if(e.walkTokens){const r=Le.defaults.walkTokens;t.walkTokens=function(t){e.walkTokens.call(this,t),r&&r.call(this,t)}}n&&(t.extensions=r),Le.setOptions(t)}))},Le.walkTokens=function(e,t){for(const r of e)switch(t.call(Le,r),r.type){case"table":for(const e of r.header)Le.walkTokens(e.tokens,t);for(const e of r.rows)for(const r of e)Le.walkTokens(r.tokens,t);break;case"list":Le.walkTokens(r.items,t);break;default:Le.defaults.extensions&&Le.defaults.extensions.childTokens&&Le.defaults.extensions.childTokens[r.type]?Le.defaults.extensions.childTokens[r.type].forEach((function(e){Le.walkTokens(r[e],t)})):r.tokens&&Le.walkTokens(r.tokens,t)}},Le.parseInline=function(e,t){if(null==e)throw new Error("marked.parseInline(): input parameter is undefined or null");if("string"!=typeof e)throw new Error("marked.parseInline(): input parameter is of type "+Object.prototype.toString.call(e)+", string expected");Se(t=be({},Le.defaults,t||{}));try{const r=Ce.lexInline(e,t);return t.walkTokens&&Le.walkTokens(r,t.walkTokens),Re.parseInline(r,t)}catch(e){if(e.message+="\nPlease report this to https://github.com/markedjs/marked.",t.silent)return"An error occurred:
"+ie(e.message+"",!0)+" ";throw e}},Le.Parser=Re,Le.parser=Re.parse,Le.Renderer=je,Le.TextRenderer=Ie,Le.Lexer=Ce,Le.lexer=Ce.lex,Le.Tokenizer=Ae,Le.Slugger=Pe,Le.parse=Le;Le.options,Le.setOptions,Le.use,Le.walkTokens,Le.parseInline,Re.parse,Ce.lex;var Ne=r(660),Fe=r.n(Ne);r(251),r(358),r(46),r(503),r(277),r(874),r(366),r(57),r(16);const De=J`.hover-bg:hover{background:var(--bg3)}::selection{background:var(--selection-bg);color:var(--selection-fg)}.regular-font{font-family:var(--font-regular)}.mono-font{font-family:var(--font-mono)}.title{font-size:calc(var(--font-size-small) + 18px);font-weight:400}.sub-title{font-size:20px}.req-res-title{font-family:var(--font-regular);font-size:calc(var(--font-size-small) + 4px);font-weight:700;margin-bottom:8px;text-align:left}.tiny-title{font-size:calc(var(--font-size-small) + 1px);font-weight:700}.regular-font-size{font-size:var(--font-size-regular)}.small-font-size{font-size:var(--font-size-small)}.upper{text-transform:uppercase}.primary-text{color:var(--primary-color)}.bold-text{font-weight:700}.gray-text{color:var(--light-fg)}.red-text{color:var(--red)}.blue-text{color:var(--blue)}.multiline{overflow:scroll;max-height:var(--resp-area-height,300px);color:var(--fg3)}.method-fg.put{color:var(--orange)}.method-fg.post{color:var(--green)}.method-fg.get{color:var(--blue)}.method-fg.delete{color:var(--red)}.method-fg.head,.method-fg.options,.method-fg.patch{color:var(--yellow)}h1{font-family:var(--font-regular);font-size:28px;padding-top:10px;letter-spacing:normal;font-weight:400}h2{font-family:var(--font-regular);font-size:24px;padding-top:10px;letter-spacing:normal;font-weight:400}h3{font-family:var(--font-regular);font-size:18px;padding-top:10px;letter-spacing:normal;font-weight:400}h4{font-family:var(--font-regular);font-size:16px;padding-top:10px;letter-spacing:normal;font-weight:400}h5{font-family:var(--font-regular);font-size:14px;padding-top:10px;letter-spacing:normal;font-weight:400}h6{font-family:var(--font-regular);font-size:14px;padding-top:10px;letter-spacing:normal;font-weight:400}h1,h2,h3,h4,h5{margin-block-end:.2em}p{margin-block-start:.5em}a{color:var(--blue);cursor:pointer}a.inactive-link{color:var(--fg);text-decoration:none;cursor:text}code,pre{margin:0;font-family:var(--font-mono);font-size:calc(var(--font-size-mono) - 1px)}.m-markdown,.m-markdown-small{display:block}.m-markdown p,.m-markdown span{font-size:var(--font-size-regular);line-height:calc(var(--font-size-regular) + 8px)}.m-markdown li{font-size:var(--font-size-regular);line-height:calc(var(--font-size-regular) + 10px)}.m-markdown-small li,.m-markdown-small p,.m-markdown-small span{font-size:var(--font-size-small);line-height:calc(var(--font-size-small) + 6px)}.m-markdown-small li{line-height:calc(var(--font-size-small) + 8px)}.m-markdown p:not(:first-child){margin-block-start:24px}.m-markdown-small p:not(:first-child){margin-block-start:12px}.m-markdown-small p:first-child{margin-block-start:0}.m-markdown p,.m-markdown-small p{margin-block-end:0}.m-markdown code span{font-size:var(--font-size-mono)}.m-markdown code,.m-markdown-small code{padding:1px 6px;border-radius:2px;color:var(--inline-code-fg);background-color:var(--bg3);font-size:calc(var(--font-size-mono));line-height:1.2}.m-markdown-small code{font-size:calc(var(--font-size-mono) - 1px)}.m-markdown pre,.m-markdown-small pre{white-space:pre-wrap;overflow-x:auto;line-height:normal;border-radius:2px;border:1px solid var(--code-border-color)}.m-markdown pre{padding:12px;background-color:var(--code-bg);color:var(--code-fg)}.m-markdown-small pre{margin-top:4px;padding:2px 4px;background-color:var(--bg3);color:var(--fg2)}.m-markdown pre code,.m-markdown-small pre code{border:none;padding:0}.m-markdown pre code{color:var(--code-fg);background-color:var(--code-bg);background-color:transparent}.m-markdown-small pre code{color:var(--fg2);background-color:var(--bg3)}.m-markdown ol,.m-markdown ul{padding-inline-start:30px}.m-markdown-small ol,.m-markdown-small ul{padding-inline-start:20px}.m-markdown a,.m-markdown-small a{color:var(--blue)}.m-markdown img,.m-markdown-small img{max-width:100%}.m-markdown table,.m-markdown-small table{border-spacing:0;margin:10px 0;border-collapse:separate;border:1px solid var(--border-color);border-radius:var(--border-radius);font-size:calc(var(--font-size-small) + 1px);line-height:calc(var(--font-size-small) + 4px);max-width:100%}.m-markdown-small table{font-size:var(--font-size-small);line-height:calc(var(--font-size-small) + 2px);margin:8px 0}.m-markdown td,.m-markdown th,.m-markdown-small td,.m-markdown-small th{vertical-align:top;border-top:1px solid var(--border-color);line-height:calc(var(--font-size-small) + 4px)}.m-markdown tr:first-child th,.m-markdown-small tr:first-child th{border-top:0 none}.m-markdown td,.m-markdown th{padding:10px 12px}.m-markdown-small td,.m-markdown-small th{padding:8px 8px}.m-markdown th,.m-markdown-small th{font-weight:600;background-color:var(--bg2);vertical-align:middle}.m-markdown-small table code{font-size:calc(var(--font-size-mono) - 2px)}.m-markdown table code{font-size:calc(var(--font-size-mono) - 1px)}.m-markdown blockquote,.m-markdown-small blockquote{margin-inline-start:0;margin-inline-end:0;border-left:3px solid var(--border-color);padding:6px 0 6px 6px}.m-markdown hr{border:1px solid var(--border-color)}`,Be=J`.m-btn{border-radius:var(--border-radius);font-weight:600;display:inline-block;padding:6px 16px;font-size:var(--font-size-small);outline:0;line-height:1;text-align:center;white-space:nowrap;border:2px solid var(--primary-color);background-color:transparent;transition:background-color .2s;user-select:none;cursor:pointer;box-shadow:0 1px 3px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.24)}.m-btn.primary{background-color:var(--primary-color);color:var(--primary-color-invert)}.m-btn.thin-border{border-width:1px}.m-btn.large{padding:8px 14px}.m-btn.small{padding:5px 12px}.m-btn.tiny{padding:5px 6px}.m-btn.circle{border-radius:50%}.m-btn:hover{background-color:var(--primary-color);color:var(--primary-color-invert)}.m-btn.nav{border:2px solid var(--nav-accent-color)}.m-btn.nav:hover{background-color:var(--nav-accent-color)}.m-btn:disabled{background-color:var(--bg3);color:var(--fg3);border-color:var(--fg3);cursor:not-allowed;opacity:.4}.toolbar-btn{cursor:pointer;padding:4px;margin:0 2px;font-size:var(--font-size-small);min-width:50px;color:var(--primary-color-invert);border-radius:2px;border:none;background-color:var(--primary-color)}button,input,pre,select,textarea{color:var(--fg);outline:0;background-color:var(--input-bg);border:1px solid var(--border-color);border-radius:var(--border-radius)}button{font-family:var(--font-regular)}input[type=file],input[type=password],input[type=text],pre,select,textarea{font-family:var(--font-mono);font-weight:400;font-size:var(--font-size-small);transition:border .2s;padding:6px 5px}select{font-family:var(--font-regular);padding:5px 30px 5px 5px;background-image:url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2212%22%20height%3D%2212%22%3E%3Cpath%20d%3D%22M10.3%203.3L6%207.6%201.7%203.3A1%201%200%2000.3%204.7l5%205a1%201%200%20001.4%200l5-5a1%201%200%2010-1.4-1.4z%22%20fill%3D%22%23777777%22%2F%3E%3C%2Fsvg%3E");background-position:calc(100% - 5px) center;background-repeat:no-repeat;background-size:10px;-webkit-appearance:none;-moz-appearance:none;appearance:none;cursor:pointer}select:hover{border-color:var(--primary-color)}input[type=password]::placeholder,input[type=text]::placeholder,textarea::placeholder{color:var(--placeholder-color);opacity:1}input[type=password]:active,input[type=password]:focus,input[type=text]:active,input[type=text]:focus,select:focus,textarea:active,textarea:focus{border:1px solid var(--primary-color)}input[type=file]{font-family:var(--font-regular);padding:2px;cursor:pointer;border:1px solid var(--primary-color);min-height:calc(var(--font-size-small) + 18px)}input[type=file]::-webkit-file-upload-button{font-family:var(--font-regular);font-size:var(--font-size-small);outline:0;cursor:pointer;padding:3px 8px;border:1px solid var(--primary-color);background-color:var(--primary-color);color:var(--primary-color-invert);border-radius:var(--border-radius);-webkit-appearance:none}pre,textarea{scrollbar-width:thin;scrollbar-color:var(--border-color) var(--input-bg)}pre::-webkit-scrollbar,textarea::-webkit-scrollbar{width:8px;height:8px}pre::-webkit-scrollbar-track,textarea::-webkit-scrollbar-track{background:var(--input-bg)}pre::-webkit-scrollbar-thumb,textarea::-webkit-scrollbar-thumb{border-radius:2px;background-color:var(--border-color)}.link{font-size:var(--font-size-small);text-decoration:underline;color:var(--blue);font-family:var(--font-mono);margin-bottom:2px}input[type=checkbox]:focus{outline:0}input[type=checkbox]{appearance:none;display:inline-block;background-color:var(--light-bg);border:1px solid var(--light-bg);border-radius:9px;cursor:pointer;height:18px;position:relative;transition:border .25s .15s,box-shadow .25s .3s,padding .25s;min-width:36px;width:36px;vertical-align:top}input[type=checkbox]:after{position:absolute;background-color:var(--bg);border:1px solid var(--light-bg);border-radius:8px;content:'';top:0;left:0;right:16px;display:block;height:16px;transition:border .25s .15s,left .25s .1s,right .15s .175s}input[type=checkbox]:checked{box-shadow:inset 0 0 0 13px var(--green);border-color:var(--green)}input[type=checkbox]:checked:after{border:1px solid var(--green);left:16px;right:1px;transition:border .25s,left .15s .25s,right .25s .175s}`,ze=J`.col,.row{display:flex}.row{align-items:center;flex-direction:row}.col{align-items:stretch;flex-direction:column}`,qe=J`.m-table{border-spacing:0;border-collapse:separate;border:1px solid var(--light-border-color);border-radius:var(--border-radius);margin:0;max-width:100%;direction:ltr}.m-table tr:first-child td,.m-table tr:first-child th{border-top:0 none}.m-table td,.m-table th{font-size:var(--font-size-small);line-height:calc(var(--font-size-small) + 4px);padding:4px 5px 4px;vertical-align:top}.m-table.padded-12 td,.m-table.padded-12 th{padding:12px}.m-table td:not([align]),.m-table th:not([align]){text-align:left}.m-table th{color:var(--fg2);font-size:var(--font-size-small);line-height:calc(var(--font-size-small) + 18px);font-weight:600;letter-spacing:normal;background-color:var(--bg2);vertical-align:bottom;border-bottom:1px solid var(--light-border-color)}.m-table>tbody>tr>td,.m-table>tr>td{border-top:1px solid var(--light-border-color);text-overflow:ellipsis;overflow:hidden}.table-title{font-size:var(--font-size-small);font-weight:700;vertical-align:middle;margin:12px 0 4px 0}`,Ue=J`.only-large-screen{display:none}.endpoint-head .path{display:flex;font-family:var(--font-mono);font-size:var(--font-size-small);align-items:center;overflow-wrap:break-word;word-break:break-all}.endpoint-head .descr{font-size:var(--font-size-small);color:var(--light-fg);font-weight:400;align-items:center;overflow-wrap:break-word;word-break:break-all;display:none}.m-endpoint.expanded{margin-bottom:16px}.m-endpoint>.endpoint-head{border-width:1px 1px 1px 5px;border-style:solid;border-color:transparent;border-top-color:var(--light-border-color);display:flex;padding:6px 16px;align-items:center;cursor:pointer}.m-endpoint>.endpoint-head.put.expanded,.m-endpoint>.endpoint-head.put:hover{border-color:var(--orange);background-color:var(--light-orange)}.m-endpoint>.endpoint-head.post.expanded,.m-endpoint>.endpoint-head.post:hover{border-color:var(--green);background-color:var(--light-green)}.m-endpoint>.endpoint-head.get.expanded,.m-endpoint>.endpoint-head.get:hover{border-color:var(--blue);background-color:var(--light-blue)}.m-endpoint>.endpoint-head.delete.expanded,.m-endpoint>.endpoint-head.delete:hover{border-color:var(--red);background-color:var(--light-red)}.m-endpoint>.endpoint-head.head.expanded,.m-endpoint>.endpoint-head.head:hover,.m-endpoint>.endpoint-head.options.expanded,.m-endpoint>.endpoint-head.options:hover,.m-endpoint>.endpoint-head.patch.expanded,.m-endpoint>.endpoint-head.patch:hover{border-color:var(--yellow);background-color:var(--light-yellow)}.m-endpoint>.endpoint-head.deprecated.expanded,.m-endpoint>.endpoint-head.deprecated:hover{border-color:var(--border-color);filter:opacity(.6)}.m-endpoint .endpoint-body{flex-wrap:wrap;padding:16px 0 0 0;border-width:0 1px 1px 5px;border-style:solid;box-shadow:0 4px 3px -3px rgba(0,0,0,.15)}.m-endpoint .endpoint-body.delete{border-color:var(--red)}.m-endpoint .endpoint-body.put{border-color:var(--orange)}.m-endpoint .endpoint-body.post{border-color:var(--green)}.m-endpoint .endpoint-body.get{border-color:var(--blue)}.m-endpoint .endpoint-body.head,.m-endpoint .endpoint-body.options,.m-endpoint .endpoint-body.patch{border-color:var(--yellow)}.m-endpoint .endpoint-body.deprecated{border-color:var(--border-color);filter:opacity(.6)}.endpoint-head .deprecated{color:var(--light-fg);filter:opacity(.6)}.summary{padding:8px 8px}.summary .title{font-size:calc(var(--font-size-regular) + 2px);margin-bottom:6px;word-break:break-all}.method{padding:2px 5px;vertical-align:middle;font-size:var(--font-size-small);height:calc(var(--font-size-small) + 16px);line-height:calc(var(--font-size-small) + 8px);width:60px;border-radius:2px;display:inline-block;text-align:center;font-weight:700;text-transform:uppercase;margin-right:5px}.method.delete{border:2px solid var(--red)}.method.put{border:2px solid var(--orange)}.method.post{border:2px solid var(--green)}.method.get{border:2px solid var(--blue)}.method.get.deprecated{border:2px solid var(--border-color)}.method.head,.method.options,.method.patch{border:2px solid var(--yellow)}.req-resp-container{display:flex;margin-top:16px;align-items:stretch;flex-wrap:wrap;flex-direction:column;border-top:1px solid var(--light-border-color)}.view-mode-request,api-response.view-mode{flex:1;min-height:100px;padding:16px 8px;overflow:hidden}.view-mode-request{border-width:0 0 1px 0;border-style:dashed}.head .view-mode-request,.options .view-mode-request,.patch .view-mode-request{border-color:var(--yellow)}.put .view-mode-request{border-color:var(--orange)}.post .view-mode-request{border-color:var(--green)}.get .view-mode-request{border-color:var(--blue)}.delete .view-mode-request{border-color:var(--red)}@media only screen and (min-width:1024px){.only-large-screen{display:block}.endpoint-head .path{font-size:var(--font-size-regular)}.endpoint-head .descr{display:flex}.descr .m-markdown-small,.endpoint-head .m-markdown-small{display:block}.req-resp-container{flex-direction:var(--layout,row);flex-wrap:nowrap}api-response.view-mode{padding:16px}.view-mode-request.row-layout{border-width:0 1px 0 0;padding:16px}.summary{padding:8px 16px}}`,Me=J`code[class*=language-],pre[class*=language-]{text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;tab-size:2;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-]{white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:var(--light-fg)}.token.punctuation{color:var(--fg)}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:var(--pink)}.token.function-name{color:var(--blue)}.token.boolean,.token.function,.token.number{color:var(--red)}.token.class-name,.token.constant,.token.property,.token.symbol{color:var(--code-property-color)}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:var(--code-keyword-color)}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:var(--green)}.token.entity,.token.operator,.token.url{color:var(--code-operator-color)}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green}`,He=J`.tab-panel{border:none}.tab-buttons{height:30px;border-bottom:1px solid var(--light-border-color);align-items:stretch;overflow-y:hidden;overflow-x:auto;scrollbar-width:thin}.tab-buttons::-webkit-scrollbar{height:1px;background-color:var(--border-color)}.tab-btn{border:none;border-bottom:3px solid transparent;color:var(--light-fg);background-color:transparent;white-space:nowrap;cursor:pointer;outline:0;font-family:var(--font-regular);font-size:var(--font-size-small);margin-right:16px;padding:1px}.tab-btn.active{border-bottom:3px solid var(--primary-color);font-weight:700;color:var(--primary-color)}.tab-btn:hover{color:var(--primary-color)}.tab-content{margin:-1px 0 0 0;position:relative}`,Ve=J`.nav-bar{width:0;height:100%;overflow:hidden;color:var(--nav-text-color);background-color:var(--nav-bg-color);background-blend-mode:multiply;line-height:calc(var(--font-size-small) + 4px);display:none;position:relative;flex-direction:column;flex-wrap:nowrap;word-break:break-word}::slotted([slot=nav-logo]){padding:16px 16px 0 16px}.nav-scroll{overflow-x:hidden;overflow-y:auto;overflow-y:overlay;scrollbar-width:thin;scrollbar-color:var(--nav-hover-bg-color) transparent}.nav-bar-tag{display:flex;align-items:center;justify-content:space-between;flex-direction:row}.nav-bar.read .nav-bar-tag-icon{display:none}.nav-bar-tag-icon{color:var(--nav-text-color);font-size:20px}.nav-bar-tag-icon:hover{color:var(--nav-hover-text-color)}.nav-bar.focused .nav-bar-tag-and-paths.collapsed .nav-bar-paths-under-tag{display:none}.nav-bar.focused .nav-bar-tag-and-paths.collapsed .nav-bar-tag-icon::after{content:'⌵';width:16px;height:16px;text-align:center;display:inline-block;transform:rotate(270deg)}.nav-bar.focused .nav-bar-tag-and-paths.expanded .nav-bar-tag-icon::after{content:'⌵';width:16px;height:16px;text-align:center;display:inline-block}.nav-scroll::-webkit-scrollbar{width:var(--scroll-bar-width,8px)}.nav-scroll::-webkit-scrollbar-track{background:0 0}.nav-scroll::-webkit-scrollbar-thumb{background-color:var(--nav-hover-bg-color)}.nav-bar-tag{font-size:var(--font-size-regular);color:var(--nav-accent-color);border-left:4px solid transparent;font-weight:700;padding:15px 15px 15px 10px;text-transform:capitalize}.nav-bar-components,.nav-bar-h1,.nav-bar-h2,.nav-bar-info,.nav-bar-path,.nav-bar-tag{display:flex;cursor:pointer;border-left:4px solid transparent}.nav-bar-h1,.nav-bar-h2,.nav-bar-path{font-size:calc(var(--font-size-small) + 1px);padding:var(--nav-item-padding)}.nav-bar-path.small-font{font-size:var(--font-size-small)}.nav-bar-info{font-size:var(--font-size-regular);padding:16px 10px;font-weight:700}.nav-bar-section{display:flex;flex-direction:row;justify-content:space-between;font-size:var(--font-size-small);color:var(--nav-text-color);padding:var(--nav-item-padding);font-weight:700}.nav-bar-section.operations{cursor:pointer}.nav-bar-section.operations:hover{color:var(--nav-hover-text-color);background-color:var(--nav-hover-bg-color)}.nav-bar-section:first-child{display:none}.nav-bar-h2{margin-left:12px}.nav-bar-h1.active,.nav-bar-h2.active,.nav-bar-info.active,.nav-bar-path.active,.nav-bar-section.operations.active,.nav-bar-tag.active{border-left:4px solid var(--nav-accent-color);color:var(--nav-hover-text-color)}.nav-bar-h1:hover,.nav-bar-h2:hover,.nav-bar-info:hover,.nav-bar-path:hover,.nav-bar-tag:hover{color:var(--nav-hover-text-color);background-color:var(--nav-hover-bg-color)}`,We=J`#api-info{font-size:calc(var(--font-size-regular) - 1px);margin-top:8px margin-left: -15px}#api-info span:before{content:"|";display:inline-block;opacity:.5;width:15px;text-align:center}#api-info span:first-child:before{content:"";width:0}`,Ge=J``;const Ke=/[\s#:?&={}]/g,Je="_rapidoc_api_key";function Ye(e){return new Promise((t=>setTimeout(t,e)))}function Ze(e,t){const r=t.currentTarget,n=document.createElement("textarea");n.value=e,n.style.position="fixed",document.body.appendChild(n),n.focus(),n.select();try{document.execCommand("copy"),r.innerText="Copied",setTimeout((()=>{r.innerText="Copy"}),5e3)}catch(e){console.error("Unable to copy",e)}document.body.removeChild(n)}function Qe(e,t,r="includes"){if("includes"===r){return`${t.method} ${t.path} ${t.summary||t.description||""} ${t.operationId||""}`.toLowerCase().includes(e.toLowerCase())}return new RegExp(e,"i").test(`${t.method} ${t.path}`)}function Xe(e,t=new Set){return e?(Object.keys(e).forEach((r=>{var n;if(t.add(r),e[r].properties)Xe(e[r].properties,t);else if(null!==(n=e[r].items)&&void 0!==n&&n.properties){var a;Xe(null===(a=e[r].items)||void 0===a?void 0:a.properties,t)}})),t):t}function et(e,t){if(e){const r=document.createElement("a");document.body.appendChild(r),r.style="display: none",r.href=e,r.download=t,r.click(),r.remove()}}function tt(e){if(e){const t=document.createElement("a");document.body.appendChild(t),t.style="display: none",t.href=e,t.target="_blank",t.click(),t.remove()}}var rt=r(764).Buffer;function nt(e){if(e.__esModule)return e;var t=Object.defineProperty({},"__esModule",{value:!0});return Object.keys(e).forEach((function(r){var n=Object.getOwnPropertyDescriptor(e,r);Object.defineProperty(t,r,n.get?n:{enumerable:!0,get:function(){return e[r]}})})),t}var at=function(e){return e&&e.Math==Math&&e},ot=at("object"==typeof globalThis&&globalThis)||at("object"==typeof window&&window)||at("object"==typeof self&&self)||at("object"==typeof ot&&ot)||function(){return this}()||Function("return this")(),it=function(e){try{return!!e()}catch(e){return!0}},st=!it((function(){var e=function(){}.bind();return"function"!=typeof e||e.hasOwnProperty("prototype")})),lt=st,ct=Function.prototype,pt=ct.apply,ut=ct.call,dt="object"==typeof Reflect&&Reflect.apply||(lt?ut.bind(pt):function(){return ut.apply(pt,arguments)}),ht=st,ft=Function.prototype,mt=ft.bind,yt=ft.call,gt=ht&&mt.bind(yt,yt),vt=ht?function(e){return e&>(e)}:function(e){return e&&function(){return yt.apply(e,arguments)}},bt=function(e){return"function"==typeof e},xt={},wt=!it((function(){return 7!=Object.defineProperty({},1,{get:function(){return 7}})[1]})),St=st,kt=Function.prototype.call,$t=St?kt.bind(kt):function(){return kt.apply(kt,arguments)},At={},Et={}.propertyIsEnumerable,Ot=Object.getOwnPropertyDescriptor,Tt=Ot&&!Et.call({1:2},1);At.f=Tt?function(e){var t=Ot(this,e);return!!t&&t.enumerable}:Et;var _t,Ct,jt=function(e,t){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:t}},It=vt,Pt=It({}.toString),Rt=It("".slice),Lt=function(e){return Rt(Pt(e),8,-1)},Nt=vt,Ft=it,Dt=Lt,Bt=ot.Object,zt=Nt("".split),qt=Ft((function(){return!Bt("z").propertyIsEnumerable(0)}))?function(e){return"String"==Dt(e)?zt(e,""):Bt(e)}:Bt,Ut=ot.TypeError,Mt=function(e){if(null==e)throw Ut("Can't call method on "+e);return e},Ht=qt,Vt=Mt,Wt=function(e){return Ht(Vt(e))},Gt=bt,Kt=function(e){return"object"==typeof e?null!==e:Gt(e)},Jt={},Yt=Jt,Zt=ot,Qt=bt,Xt=function(e){return Qt(e)?e:void 0},er=function(e,t){return arguments.length<2?Xt(Yt[e])||Xt(Zt[e]):Yt[e]&&Yt[e][t]||Zt[e]&&Zt[e][t]},tr=vt({}.isPrototypeOf),rr=er("navigator","userAgent")||"",nr=ot,ar=rr,or=nr.process,ir=nr.Deno,sr=or&&or.versions||ir&&ir.version,lr=sr&&sr.v8;lr&&(Ct=(_t=lr.split("."))[0]>0&&_t[0]<4?1:+(_t[0]+_t[1])),!Ct&&ar&&(!(_t=ar.match(/Edge\/(\d+)/))||_t[1]>=74)&&(_t=ar.match(/Chrome\/(\d+)/))&&(Ct=+_t[1]);var cr=Ct,pr=cr,ur=it,dr=!!Object.getOwnPropertySymbols&&!ur((function(){var e=Symbol();return!String(e)||!(Object(e)instanceof Symbol)||!Symbol.sham&&pr&&pr<41})),hr=dr&&!Symbol.sham&&"symbol"==typeof Symbol.iterator,fr=er,mr=bt,yr=tr,gr=hr,vr=ot.Object,br=gr?function(e){return"symbol"==typeof e}:function(e){var t=fr("Symbol");return mr(t)&&yr(t.prototype,vr(e))},xr=ot.String,wr=function(e){try{return xr(e)}catch(e){return"Object"}},Sr=bt,kr=wr,$r=ot.TypeError,Ar=function(e){if(Sr(e))return e;throw $r(kr(e)+" is not a function")},Er=Ar,Or=function(e,t){var r=e[t];return null==r?void 0:Er(r)},Tr=$t,_r=bt,Cr=Kt,jr=ot.TypeError,Ir={exports:{}},Pr=ot,Rr=Object.defineProperty,Lr=function(e,t){try{Rr(Pr,e,{value:t,configurable:!0,writable:!0})}catch(r){Pr[e]=t}return t},Nr="__core-js_shared__",Fr=ot[Nr]||Lr(Nr,{}),Dr=Fr;(Ir.exports=function(e,t){return Dr[e]||(Dr[e]=void 0!==t?t:{})})("versions",[]).push({version:"3.21.1",mode:"pure",copyright:"© 2014-2022 Denis Pushkarev (zloirock.ru)",license:"https://github.com/zloirock/core-js/blob/v3.21.1/LICENSE",source:"https://github.com/zloirock/core-js"});var Br=Mt,zr=ot.Object,qr=function(e){return zr(Br(e))},Ur=qr,Mr=vt({}.hasOwnProperty),Hr=Object.hasOwn||function(e,t){return Mr(Ur(e),t)},Vr=vt,Wr=0,Gr=Math.random(),Kr=Vr(1..toString),Jr=function(e){return"Symbol("+(void 0===e?"":e)+")_"+Kr(++Wr+Gr,36)},Yr=ot,Zr=Ir.exports,Qr=Hr,Xr=Jr,en=dr,tn=hr,rn=Zr("wks"),nn=Yr.Symbol,an=nn&&nn.for,on=tn?nn:nn&&nn.withoutSetter||Xr,sn=function(e){if(!Qr(rn,e)||!en&&"string"!=typeof rn[e]){var t="Symbol."+e;en&&Qr(nn,e)?rn[e]=nn[e]:rn[e]=tn&&an?an(t):on(t)}return rn[e]},ln=$t,cn=Kt,pn=br,un=Or,dn=function(e,t){var r,n;if("string"===t&&_r(r=e.toString)&&!Cr(n=Tr(r,e)))return n;if(_r(r=e.valueOf)&&!Cr(n=Tr(r,e)))return n;if("string"!==t&&_r(r=e.toString)&&!Cr(n=Tr(r,e)))return n;throw jr("Can't convert object to primitive value")},hn=sn,fn=ot.TypeError,mn=hn("toPrimitive"),yn=function(e,t){if(!cn(e)||pn(e))return e;var r,n=un(e,mn);if(n){if(void 0===t&&(t="default"),r=ln(n,e,t),!cn(r)||pn(r))return r;throw fn("Can't convert object to primitive value")}return void 0===t&&(t="number"),dn(e,t)},gn=br,vn=function(e){var t=yn(e,"string");return gn(t)?t:t+""},bn=Kt,xn=ot.document,wn=bn(xn)&&bn(xn.createElement),Sn=function(e){return wn?xn.createElement(e):{}},kn=Sn,$n=!wt&&!it((function(){return 7!=Object.defineProperty(kn("div"),"a",{get:function(){return 7}}).a})),An=wt,En=$t,On=At,Tn=jt,_n=Wt,Cn=vn,jn=Hr,In=$n,Pn=Object.getOwnPropertyDescriptor;xt.f=An?Pn:function(e,t){if(e=_n(e),t=Cn(t),In)try{return Pn(e,t)}catch(e){}if(jn(e,t))return Tn(!En(On.f,e,t),e[t])};var Rn=it,Ln=bt,Nn=/#|\.prototype\./,Fn=function(e,t){var r=Bn[Dn(e)];return r==qn||r!=zn&&(Ln(t)?Rn(t):!!t)},Dn=Fn.normalize=function(e){return String(e).replace(Nn,".").toLowerCase()},Bn=Fn.data={},zn=Fn.NATIVE="N",qn=Fn.POLYFILL="P",Un=Fn,Mn=Ar,Hn=st,Vn=vt(vt.bind),Wn=function(e,t){return Mn(e),void 0===t?e:Hn?Vn(e,t):function(){return e.apply(t,arguments)}},Gn={},Kn=wt&&it((function(){return 42!=Object.defineProperty((function(){}),"prototype",{value:42,writable:!1}).prototype})),Jn=ot,Yn=Kt,Zn=Jn.String,Qn=Jn.TypeError,Xn=function(e){if(Yn(e))return e;throw Qn(Zn(e)+" is not an object")},ea=wt,ta=$n,ra=Kn,na=Xn,aa=vn,oa=ot.TypeError,ia=Object.defineProperty,sa=Object.getOwnPropertyDescriptor,la="enumerable",ca="configurable",pa="writable";Gn.f=ea?ra?function(e,t,r){if(na(e),t=aa(t),na(r),"function"==typeof e&&"prototype"===t&&"value"in r&&pa in r&&!r.writable){var n=sa(e,t);n&&n.writable&&(e[t]=r.value,r={configurable:ca in r?r.configurable:n.configurable,enumerable:la in r?r.enumerable:n.enumerable,writable:!1})}return ia(e,t,r)}:ia:function(e,t,r){if(na(e),t=aa(t),na(r),ta)try{return ia(e,t,r)}catch(e){}if("get"in r||"set"in r)throw oa("Accessors not supported");return"value"in r&&(e[t]=r.value),e};var ua=Gn,da=jt,ha=wt?function(e,t,r){return ua.f(e,t,da(1,r))}:function(e,t,r){return e[t]=r,e},fa=ot,ma=dt,ya=vt,ga=bt,va=xt.f,ba=Un,xa=Jt,wa=Wn,Sa=ha,ka=Hr,$a=function(e){var t=function(r,n,a){if(this instanceof t){switch(arguments.length){case 0:return new e;case 1:return new e(r);case 2:return new e(r,n)}return new e(r,n,a)}return ma(e,this,arguments)};return t.prototype=e.prototype,t},Aa=function(e,t){var r,n,a,o,i,s,l,c,p=e.target,u=e.global,d=e.stat,h=e.proto,f=u?fa:d?fa[p]:(fa[p]||{}).prototype,m=u?xa:xa[p]||Sa(xa,p,{})[p],y=m.prototype;for(a in t)r=!ba(u?a:p+(d?".":"#")+a,e.forced)&&f&&ka(f,a),i=m[a],r&&(s=e.noTargetGet?(c=va(f,a))&&c.value:f[a]),o=r&&s?s:t[a],r&&typeof i==typeof o||(l=e.bind&&r?wa(o,fa):e.wrap&&r?$a(o):h&&ga(o)?ya(o):o,(e.sham||o&&o.sham||i&&i.sham)&&Sa(l,"sham",!0),Sa(m,a,l),h&&(ka(xa,n=p+"Prototype")||Sa(xa,n,{}),Sa(xa[n],a,o),e.real&&y&&!y[a]&&Sa(y,a,o)))},Ea=Math.ceil,Oa=Math.floor,Ta=function(e){var t=+e;return t!=t||0===t?0:(t>0?Oa:Ea)(t)},_a=Ta,Ca=Math.max,ja=Math.min,Ia=function(e,t){var r=_a(e);return r<0?Ca(r+t,0):ja(r,t)},Pa=Ta,Ra=Math.min,La=function(e){return e>0?Ra(Pa(e),9007199254740991):0},Na=La,Fa=function(e){return Na(e.length)},Da=Wt,Ba=Ia,za=Fa,qa=function(e){return function(t,r,n){var a,o=Da(t),i=za(o),s=Ba(n,i);if(e&&r!=r){for(;i>s;)if((a=o[s++])!=a)return!0}else for(;i>s;s++)if((e||s in o)&&o[s]===r)return e||s||0;return!e&&-1}},Ua={includes:qa(!0),indexOf:qa(!1)},Ma={},Ha=Hr,Va=Wt,Wa=Ua.indexOf,Ga=Ma,Ka=vt([].push),Ja=function(e,t){var r,n=Va(e),a=0,o=[];for(r in n)!Ha(Ga,r)&&Ha(n,r)&&Ka(o,r);for(;t.length>a;)Ha(n,r=t[a++])&&(~Wa(o,r)||Ka(o,r));return o},Ya=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"],Za=Ja,Qa=Ya,Xa=Object.keys||function(e){return Za(e,Qa)},eo=qr,to=Xa;Aa({target:"Object",stat:!0,forced:it((function(){to(1)}))},{keys:function(e){return to(eo(e))}});var ro=Jt.Object.keys,no=ro,ao=Lt,oo=Array.isArray||function(e){return"Array"==ao(e)},io={};io[sn("toStringTag")]="z";var so="[object z]"===String(io),lo=ot,co=so,po=bt,uo=Lt,ho=sn("toStringTag"),fo=lo.Object,mo="Arguments"==uo(function(){return arguments}()),yo=co?uo:function(e){var t,r,n;return void 0===e?"Undefined":null===e?"Null":"string"==typeof(r=function(e,t){try{return e[t]}catch(e){}}(t=fo(e),ho))?r:mo?uo(t):"Object"==(n=uo(t))&&po(t.callee)?"Arguments":n},go=yo,vo=ot.String,bo=function(e){if("Symbol"===go(e))throw TypeError("Cannot convert a Symbol value to a string");return vo(e)},xo={},wo=wt,So=Kn,ko=Gn,$o=Xn,Ao=Wt,Eo=Xa;xo.f=wo&&!So?Object.defineProperties:function(e,t){$o(e);for(var r,n=Ao(t),a=Eo(t),o=a.length,i=0;o>i;)ko.f(e,r=a[i++],n[r]);return e};var Oo,To=er("document","documentElement"),_o=Ir.exports,Co=Jr,jo=_o("keys"),Io=function(e){return jo[e]||(jo[e]=Co(e))},Po=Xn,Ro=xo,Lo=Ya,No=Ma,Fo=To,Do=Sn,Bo=Io("IE_PROTO"),zo=function(){},qo=function(e){return"
+
+
+
+