diff --git a/backend/go.mod b/backend/go.mod index fd535ed1..06f12696 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -10,6 +10,7 @@ require ( github.com/aws/smithy-go v1.25.1 github.com/caarlos0/env/v11 v11.4.1 github.com/cenkalti/backoff/v5 v5.0.3 + github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec github.com/disintegration/imaging v1.6.2 github.com/dunglas/go-urlpattern v0.0.0-20241020164140-716dfa1c80b1 diff --git a/backend/go.sum b/backend/go.sum index 39cbd15f..917097ed 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -65,6 +65,8 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= +github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/backend/internal/bootstrap/router_bootstrap.go b/backend/internal/bootstrap/router_bootstrap.go index 025dde67..e536517c 100644 --- a/backend/internal/bootstrap/router_bootstrap.go +++ b/backend/internal/bootstrap/router_bootstrap.go @@ -15,6 +15,7 @@ import ( "sync/atomic" "time" + "github.com/coreos/go-systemd/activation" "github.com/fsnotify/fsnotify" sloggin "github.com/gin-contrib/slog" "github.com/gin-gonic/gin" @@ -162,24 +163,26 @@ func initServer(r *gin.Engine) (*serverConfig, error) { return nil, err } - network, addr := listenerNetworkAndAddr() - listener, err := net.Listen(network, addr) //nolint:noctx - if err != nil { - return nil, fmt.Errorf("failed to create %s listener: %w", network, err) + var socketFn func() (*socket, error) + switch { + case common.EnvConfig.SystemdSocket: + socketFn = systemdSocket + case common.EnvConfig.UnixSocket != "": + socketFn = unixSocket + default: + socketFn = tcpSocket } - if err := setUnixSocketMode(network, addr); err != nil { - listener.Close() + socket, err := socketFn() + if err != nil { return nil, err } - return &serverConfig{ - addr: addr, - certProvider: certProvider, - listener: listener, - server: newHTTPServer(r, protocols), - tlsConfig: tlsConfig, - }, nil + addr := socket.addr + listener := socket.listener + server := newHTTPServer(r, protocols) + + return &serverConfig{addr, certProvider, listener, server, tlsConfig}, nil } func initServerProtocols() (*http.Protocols, *tls.Config, *tlsCertProvider, error) { @@ -207,6 +210,64 @@ func initServerProtocols() (*http.Protocols, *tls.Config, *tlsCertProvider, erro return protocols, tlsConfig, certProvider, nil } +type socket struct { + addr string + listener net.Listener +} + +func systemdSocket() (*socket, error) { + listeners, err := activation.Listeners() + if err != nil { + return nil, fmt.Errorf("failed to receive socket from systemd: %w", err) + } + + if len(listeners) == 0 { + return nil, errors.New("did not receive any sockets from systemd") + } + + if len(listeners) > 1 { + return nil, errors.New("received too many sockets from systemd") + } + + return &socket{"(systemd)", listeners[0]}, nil +} + +func unixSocket() (*socket, error) { + addr := common.EnvConfig.UnixSocket + os.Remove(addr) // remove dangling the socket file to avoid file-exist error + + listener, err := net.Listen("unix", addr) //nolint:noctx + if err != nil { + return nil, fmt.Errorf("failed to create UNIX socket: %w", err) + } + + if common.EnvConfig.UnixSocketMode != "" { + mode, err := strconv.ParseUint(common.EnvConfig.UnixSocketMode, 8, 32) + if err != nil { + listener.Close() + return nil, fmt.Errorf("failed to parse UNIX socket mode '%s': %w", common.EnvConfig.UnixSocketMode, err) + } + + if err := os.Chmod(addr, os.FileMode(mode)); err != nil { + listener.Close() + return nil, fmt.Errorf("failed to set UNIX socket mode '%s': %w", common.EnvConfig.UnixSocketMode, err) + } + } + + return &socket{addr, listener}, nil +} + +func tcpSocket() (*socket, error) { + addr := net.JoinHostPort(common.EnvConfig.Host, common.EnvConfig.Port) + + listener, err := net.Listen("tcp", addr) //nolint:noctx + if err != nil { + return nil, fmt.Errorf("failed to create TCP socket: %w", err) + } + + return &socket{addr, listener}, nil +} + func newHTTPServer(r *gin.Engine, protocols *http.Protocols) *http.Server { return &http.Server{ MaxHeaderBytes: 1 << 20, @@ -226,33 +287,6 @@ func newHTTPServer(r *gin.Engine, protocols *http.Protocols) *http.Server { } } -func listenerNetworkAndAddr() (string, string) { - if common.EnvConfig.UnixSocket == "" { - return "tcp", net.JoinHostPort(common.EnvConfig.Host, common.EnvConfig.Port) - } - - addr := common.EnvConfig.UnixSocket - os.Remove(addr) // remove dangling the socket file to avoid file-exist error - return "unix", addr -} - -func setUnixSocketMode(network, addr string) error { - if network != "unix" || common.EnvConfig.UnixSocketMode == "" { - return nil - } - - mode, err := strconv.ParseUint(common.EnvConfig.UnixSocketMode, 8, 32) - if err != nil { - return fmt.Errorf("failed to parse UNIX socket mode '%s': %w", common.EnvConfig.UnixSocketMode, err) - } - - if err := os.Chmod(addr, os.FileMode(mode)); err != nil { - return fmt.Errorf("failed to set UNIX socket mode '%s': %w", common.EnvConfig.UnixSocketMode, err) - } - - return nil -} - func runServer(ctx context.Context, config *serverConfig) error { slog.Info("Server listening", slog.String("addr", config.addr), slog.Bool("tls", config.tlsConfig != nil)) diff --git a/backend/internal/cmds/healthcheck.go b/backend/internal/cmds/healthcheck.go index d32bd1f4..bbea825c 100644 --- a/backend/internal/cmds/healthcheck.go +++ b/backend/internal/cmds/healthcheck.go @@ -2,9 +2,12 @@ package cmds import ( "context" + "fmt" "log/slog" + "net" "net/http" "os" + "strings" "time" "github.com/spf13/cobra" @@ -13,8 +16,14 @@ import ( ) type healthcheckFlags struct { - Endpoint string - Verbose bool + Endpoint string + UnixSocket string + Verbose bool +} + +type healthcheckResult struct { + StatusCode int + URL string } func init() { @@ -29,47 +38,26 @@ func init() { ctx, cancel := context.WithTimeout(cmd.Context(), 5*time.Second) defer cancel() - url := flags.Endpoint + "/healthz" - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if flags.UnixSocket == "" && !cmd.Flags().Changed("endpoint") { + flags.UnixSocket = common.EnvConfig.UnixSocket + } + + result, err := healthcheck(ctx, flags) if err != nil { slog.ErrorContext(ctx, - "Failed to create request object", + "Healthcheck failed", "error", err, - "url", url, "ms", time.Since(start).Milliseconds(), ) os.Exit(1) } - res, err := http.DefaultClient.Do(req) - if err != nil { - slog.ErrorContext(ctx, - "Failed to perform request", - "error", err, - "url", url, - "ms", time.Since(start).Milliseconds(), - ) - os.Exit(1) - } - defer res.Body.Close() - - if res.StatusCode < 200 || res.StatusCode >= 300 { - if err != nil { - slog.ErrorContext(ctx, - "Healthcheck failed", - "status", res.StatusCode, - "url", url, - "ms", time.Since(start).Milliseconds(), - ) - os.Exit(1) - } - } - if flags.Verbose { slog.InfoContext(ctx, "Healthcheck succeeded", - "status", res.StatusCode, - "url", url, + "status", result.StatusCode, + "url", result.URL, + "unixSocket", flags.UnixSocket, "ms", time.Since(start).Milliseconds(), ) } @@ -77,7 +65,42 @@ func init() { } healthcheckCmd.Flags().StringVarP(&flags.Endpoint, "endpoint", "e", "http://localhost:"+common.EnvConfig.Port, "Endpoint for Pocket ID") + healthcheckCmd.Flags().StringVar(&flags.UnixSocket, "unix-socket", "", "UNIX socket path for Pocket ID") healthcheckCmd.Flags().BoolVarP(&flags.Verbose, "verbose", "v", false, "Enable verbose mode") rootCmd.AddCommand(healthcheckCmd) } + +func healthcheck(ctx context.Context, flags healthcheckFlags) (*healthcheckResult, error) { + url := strings.TrimRight(flags.Endpoint, "/") + "/healthz" + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request object for %q: %w", url, err) + } + + client := http.DefaultClient + if flags.UnixSocket != "" { + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.Proxy = nil + transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + dialer := net.Dialer{} + return dialer.DialContext(ctx, "unix", flags.UnixSocket) + } + client = &http.Client{Transport: transport} + } + + res, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to perform request to %q: %w", url, err) + } + defer res.Body.Close() + + if res.StatusCode < 200 || res.StatusCode >= 300 { + return nil, fmt.Errorf("unexpected status %d from %q", res.StatusCode, url) + } + + return &healthcheckResult{ + StatusCode: res.StatusCode, + URL: url, + }, nil +} diff --git a/backend/internal/cmds/healthcheck_test.go b/backend/internal/cmds/healthcheck_test.go new file mode 100644 index 00000000..3df83cd8 --- /dev/null +++ b/backend/internal/cmds/healthcheck_test.go @@ -0,0 +1,79 @@ +package cmds + +import ( + "context" + "net" + "net/http" + "net/http/httptest" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHealthcheckTCPSuccess(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/healthz", r.URL.Path) + w.WriteHeader(http.StatusNoContent) + })) + defer server.Close() + + result, err := healthcheck(t.Context(), healthcheckFlags{ + Endpoint: server.URL, + }) + + require.NoError(t, err) + require.Equal(t, http.StatusNoContent, result.StatusCode) + require.Equal(t, server.URL+"/healthz", result.URL) +} + +func TestHealthcheckFailsOnUnexpectedStatus(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() + + _, err := healthcheck(t.Context(), healthcheckFlags{ + Endpoint: server.URL, + }) + + require.Error(t, err) + require.ErrorContains(t, err, "unexpected status 500") +} + +func TestHealthcheckUnixSocket(t *testing.T) { + socketPath := filepath.Join(t.TempDir(), "pocket-id.sock") + listener, err := (&net.ListenConfig{}).Listen(t.Context(), "unix", socketPath) + require.NoError(t, err) + + server := &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/healthz", r.URL.Path) + w.WriteHeader(http.StatusNoContent) + }), + ReadHeaderTimeout: time.Second, + } + + errCh := make(chan error, 1) + go func() { + errCh <- server.Serve(listener) + }() + + t.Cleanup(func() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + require.NoError(t, server.Shutdown(ctx)) + require.ErrorIs(t, <-errCh, http.ErrServerClosed) + }) + + result, err := healthcheck(t.Context(), healthcheckFlags{ + Endpoint: "http://localhost:1411", + UnixSocket: socketPath, + }) + + require.NoError(t, err) + require.Equal(t, http.StatusNoContent, result.StatusCode) + require.Equal(t, "http://localhost:1411/healthz", result.URL) +} diff --git a/backend/internal/common/env_config.go b/backend/internal/common/env_config.go index 82a421f4..fa4f3f0c 100644 --- a/backend/internal/common/env_config.go +++ b/backend/internal/common/env_config.go @@ -68,6 +68,7 @@ type EnvConfigSchema struct { Host string `env:"HOST" options:"toLower"` UnixSocket string `env:"UNIX_SOCKET"` UnixSocketMode string `env:"UNIX_SOCKET_MODE"` + SystemdSocket bool `env:"SYSTEMD_SOCKET"` LocalIPv6Ranges string `env:"LOCAL_IPV6_RANGES"` TLSCertFile string `env:"TLS_CERT" options:"file"` @@ -153,6 +154,9 @@ func ValidateEnvConfig(config *EnvConfigSchema) error { if err := validateFileBackend(config); err != nil { return err } + if config.SystemdSocket && config.UnixSocket != "" { + return errors.New("SYSTEMD_SOCKET and UNIX_SOCKET are mutually exclusive") + } if err := validateLocalIPv6Ranges(config.LocalIPv6Ranges); err != nil { return err } diff --git a/backend/internal/dto/validations.go b/backend/internal/dto/validations.go index 7b514cd4..5578efcb 100644 --- a/backend/internal/dto/validations.go +++ b/backend/internal/dto/validations.go @@ -124,11 +124,11 @@ func ValidateCallbackURLPattern(raw string) bool { } // ValidateResponseMode validates response_mode parameter -// If responseMode is present, it must be "form_post" or "query" +// If responseMode is present, it must be "form_post", "query", or "fragment" // Empty responseMode is allowed (field not provided, use default) func ValidateResponseMode(responseMode string) bool { switch responseMode { - case "form_post", "query": + case "form_post", "query", "fragment": return true case "": return true diff --git a/backend/internal/dto/validations_test.go b/backend/internal/dto/validations_test.go index da7e2cc4..e488468d 100644 --- a/backend/internal/dto/validations_test.go +++ b/backend/internal/dto/validations_test.go @@ -102,8 +102,9 @@ func TestValidateResponseMode(t *testing.T) { }{ {"valid form_post", "form_post", true}, {"valid query", "query", true}, + {"valid fragment", "fragment", true}, {"valid empty", "", true}, - {"invalid fragment", "fragment", false}, + {"invalid unknown", "unknown", false}, } for _, tt := range tests { diff --git a/backend/internal/utils/image/profile_picture.go b/backend/internal/utils/image/profile_picture.go index 21d78797..4c4debde 100644 --- a/backend/internal/utils/image/profile_picture.go +++ b/backend/internal/utils/image/profile_picture.go @@ -54,7 +54,7 @@ func CreateDefaultProfilePicture(initials string) (*bytes.Buffer, error) { img := imaging.New(profilePictureSize, profilePictureSize, color.RGBA{R: 255, G: 255, B: 255, A: 255}) // Load the font - fontBytes, err := resources.FS.ReadFile("fonts/PlayfairDisplay-Bold.ttf") + fontBytes, err := resources.FS.ReadFile("fonts/Gloock.ttf") if err != nil { return nil, fmt.Errorf("failed to read font file: %w", err) } @@ -84,7 +84,7 @@ func CreateDefaultProfilePicture(initials string) (*bytes.Buffer, error) { // Center the initials x := (profilePictureSize - font.MeasureString(face, initials).Ceil()) / 2 - y := (profilePictureSize-face.Metrics().Height.Ceil())/2 + face.Metrics().Ascent.Ceil() - 10 + y := (profilePictureSize-face.Metrics().Height.Ceil())/2 + face.Metrics().Ascent.Ceil() drawer.Dot = fixed.P(x, y) // Draw the initials diff --git a/backend/resources/aaguids.json b/backend/resources/aaguids.json index 659f9d76..5b6b630b 100644 --- a/backend/resources/aaguids.json +++ b/backend/resources/aaguids.json @@ -1 +1 @@ -{"fcb1bcb4-f370-078c-6993-bc24d0ae3fbe":"Ledger Nano X FIDO2 Authenticator","6e8d1eae-8d40-4c25-bcf8-4633959afc71":"Veridium iOS SDK","e8b7f4a2-c3d5-e6f7-890a-b1c2d3e4f567":"Sherlocked","9eb7eabc-9db5-49a1-b6c3-555a802093f4":"YubiKey 5 Series with NFC KVZR57","4d41190c-7beb-4a84-8018-adf265a6352d":"Thales IDPrime FIDO Bio","2772ce93-eb4b-4090-8b73-330f48477d73":"Security Key NFC by Yubico - Enterprise Edition Preview","6dae43be-af9c-417b-8b9f-1b611168ec60":"Dapple Authenticator from Dapple Security Inc.","5626bed4-e756-430b-a7ff-ca78c8b12738":"VALMIDO PRO FIDO","260e3021-482d-442d-838c-7edfbe153b7e":"Feitian ePass FIDO2-NFC Plus Authenticator","95e4d58c-056e-4a65-866d-f5a69659e880":"TruU Windows Authenticator","90636e1f-ef82-43bf-bdcf-5255f139d12f":"YubiKey Bio Series - Multi-protocol Edition","9c835346-796b-4c27-8898-d6032f515cc5":"Cryptnox FIDO2","c3f47802-de73-4dfc-ba22-671fe3304f90":"eToken Fusion NFC PIV Enterprise","0d9b2e56-566b-c393-2940-f821b7f15d6d":"Excelsecu eSecu FIDO2 Pro Security Key","2bff89f2-323a-48fc-b7c8-9ff7fe87c07e":"Feitian BioPass FIDO2 Pro (Enterprise Profile)","c5ef55ff-ad9a-4b9f-b580-adebafe026d0":"YubiKey 5 Series with Lightning","2194b428-9397-4046-8f39-007a1605a482":"IDPrime 931 Fido","39a5647e-1853-446c-a1f6-a79bae9f5bc7":"IDmelon","664d9f67-84a2-412a-9ff7-b4f7d8ee6d05":"OpenSK authenticator","3789da91-f943-46bc-95c3-50ea2012f03a":"NEOWAVE Winkeo FIDO2","fa2b99dc-9e39-4257-8f92-4a30d23c4118":"YubiKey 5 Series with NFC","341e4da9-3c2e-8103-5a9f-aad887135200":"Ledger Nano S FIDO2 Authenticator","69700f79-d1fb-472e-bd9b-a3a3b9a9eda0":"Pone Biometrics OFFPAD Authenticator","8da0e4dc-164b-454e-972e-88f362b23d59":"CardOS FIDO2 Token","89b19028-256b-4025-8872-255358d950e4":"Sentry Enterprises CTAP2 Authenticator","4e768f2c-5fab-48b3-b300-220eb487752b":"Hideez Key 4 FIDO2 SDK","47ab2fb4-66ac-4184-9ae1-86be814012d5":"Security Key NFC by Yubico - Enterprise Edition","931327dd-c89b-406c-a81e-ed7058ef36c6":"Swissbit iShield Key FIDO2","f8d5c4e9-e539-4c06-8662-ec2a4155a555":"StarSign Key Fob","b7d3f68e-88a6-471e-9ecf-2df26d041ede":"Security Key NFC by Yubico","8d1b1fcb-3c76-49a9-9129-5515b346aa02":"IDEMIA ID-ONE Card","30b5035e-d297-4ff7-020b-addc96ba6a98":"OneSpan DIGIPASS FX7","454e5346-4944-4ffd-6c93-8e9267193e9a":"Ensurity ThinC","e1a96183-5016-4f24-b55b-e3ae23614cc6":"ATKey.Pro CTAP2.0","9ff4cc65-6154-4fff-ba09-9e2af7882ad2":"Security Key NFC by Yubico - Enterprise Edition (Enterprise Profile)","4599062e-6926-4fe7-9566-9e8fb1aedaa0":"YubiKey 5 Series (Enterprise Profile)","9d3df6ba-282f-11ed-a261-0242ac120002":"Arculus FIDO2/U2F Key Card","fbefdf68-fe86-0106-213e-4d5fa24cbe2e":"Excelsecu eSecu FIDO2 NFC Security Key","62e54e98-c209-4df3-b692-de71bb6a8528":"YubiKey 5 FIPS Series with NFC Preview","ab32f0c6-2239-afbb-c470-d2ef4e254db7":"TOKEN2 FIDO2 Security Key","ce6bf97f-9f69-4ba7-9032-97adc6ca5cf1":"YubiKey 5 FIPS Series with NFC (RC Preview)","ad08c78a-4e41-49b9-86a2-ac15b06899e2":"YubiKey Bio Series - FIDO Edition (Enterprise Profile)","930b0c03-ef46-4ac4-935c-538dccd1fcdb":"Chipwon Clife Key","7787a482-13e8-4784-8a06-c7ed49a7aaf4":"Swissbit iShield Key 2","72c6b72d-8512-4c66-8359-9d3d10d9222f":"Security Key NFC by Yubico - Enterprise Edition (Enterprise Profile)","99ed6c29-4573-4847-816d-78ad8f1c75ef":"VeroCard FIDO2 Authenticator","973446ca-e21c-9a9b-99f5-9b985a67af0f":"ACS FIDO Authenticator Card","74820b05-a6c9-40f9-8fb0-9f86aca93998":"SafeNet eToken Fusion","1105e4ed-af1d-02ff-ffff-ffffffffffff":"Egomet FIDO2 Authenticator for Android","08987058-cadc-4b81-b6e1-30de50dcbe96":"Windows Hello","a4e9fc6d-4cbe-4758-b8ba-37598bb5bbaa":"Security Key NFC by Yubico","0acf3011-bc60-f375-fb53-6f05f43154e0":"Nymi FIDO2 Authenticator","d91c5288-0ef0-49b7-b8ae-21ca0aa6b3f3":"KEY-ID FIDO2 Authenticator","8eec9bf9-486c-46da-9a67-1fbb4f66b9ed":"HID Crescendo 4000 FIPS","4c50ff10-1057-4fc6-b8ed-43a529530c3c":"ImproveID Authenticator","c611b55c-77b2-4527-8082-590e931b2f08":"GoTrust Idem Key ","ee041bce-25e5-4cdb-8f86-897fd6418464":"Feitian ePass FIDO2-NFC Authenticator","2588ae83-5a3b-4536-b8de-5e540200d191":"Dapple Authenticator from Dapple Security Inc.","4b89f401-464e-4745-a520-486ddfc5d80e":"IIST FIDO2 Authenticator","2cd2f727-f6ca-44da-8f48-5c2e5da000a2":"Nitrokey 3 AM","10c70715-2a9a-4de1-b0aa-3cff6d496d39":"eToken Fusion NFC FIPS","efb96b10-a9ee-4b6c-a4a9-d32125ccd4a4":"Safenet eToken FIDO","24083bcb-3034-4867-99de-a3b52e1d426a":"Enterprise Security Key Series with NFC (Consumer Profile)","4b3f8944-d4f2-4d21-bb19-764a986ec160":"KeyXentic FIDO2 Secp256R1 FIDO2 CTAP2 Authenticator","4c0cf95d-2f40-43b5-ba42-4c83a11c04ba":"Feitian BioPass FIDO2 Pro Authenticator","5343502d-5343-5343-6172-644649444f32":"ESS Smart Card Inc. Authenticator","69e7c36f-f2f6-9e0d-07a6-bcc243262e6b":"OneKey FIDO2 Authenticator","524de2de-982f-49b4-a769-2b5e3b73ad79":"YubiKey 5 Series (Enterprise Profile)","09591fc6-9811-48f7-8f57-b9f23df6413f":"Pone Biometrics OFFPAD Authenticator","912435d9-4a88-42f3-972d-1244b0d51420":"SI0X FIDO CL WRIST v1.0","7e3f3d30-3557-4442-bdae-139312178b39":"RSA DS100","73bb0cd4-e502-49b8-9c6f-b59445bf720b":"YubiKey 5 FIPS Series","39589099-9a75-49fc-afaa-801ca211c62a":"Feitian ePass FIDO-NFC (Enterprise Profile) (CTAP2.1, CTAP2.0, U2F)","dc5e949d-f939-43b3-9877-a85c7186b753":"YubiKey Bio Multi-protocol Edition (Enterprise Profile)","149a2021-8ef6-4133-96b8-81f8d5b7f1f5":"Security Key by Yubico with NFC","5df66f62-5b47-43d3-aa1d-a6e31c8dbeb5":"Securitag Assembly Group FIDO Authenticator NFC","9a272558-5cfa-4424-be37-65509677b77d":"SECORA ID Key S USB by Infineon Consumer Edition","09619fbf-d75e-4a62-be1d-fe4d240864ae":"VeriMark(TM) Guard 2.1 Fingerprint Security Key","50cbf15a-238c-4457-8f16-812c43bf3c49":"Ensurity AUTH TouchPro","ee7fa1e0-9539-432f-bd43-9c2fc6d4f311":"VeriMark NFC+ USB-C Security Key","b90e7dc1-316e-4fee-a25a-56a666a670fe":"YubiKey 5 Series with Lightning (Enterprise Profile)","6d4aa745-dad5-40c4-b9b4-6a252fcee70f":"GoTrust Cyber Key","175cd298-83d2-4a26-b637-313c07a6434e":"Chunghwa Telecom FIDO2 Smart Card Authenticator","34744913-4f57-4e6e-a527-e9ec3c4b94e6":"YubiKey Bio Series - Multi-protocol Edition","5ea308b2-7ac7-48b9-ac09-7e2da9015f8c":"Veridium Android SDK","3b1adb99-0dfe-46fd-90b8-7f7614a4de2a":"GoTrust Idem Key","46544d5d-8f5d-4db4-89ac-ea8977073fff":"Foongtone FIDO Authenticator","0a357157-9b18-4c8a-920e-d156e972b2f8":"YubiKey 5 Series (Consumer Profile)","998f358b-2dd2-4cbe-a43a-e8107438dfb3":"OnlyKey Secp256R1 FIDO2 CTAP2 Authenticator","30b5035e-d297-4ff2-010b-addc96ba6a98":"OneSpan DIGIPASS FX2-A","817cdab8-0d51-4de1-a821-e25b88519cf3":"Swissbit iShield Key 2 FIPS","61250591-b2bc-4456-b719-0b17be90bb30":"eWBM eFPA FIDO2 Authenticator","8c39ee86-7f9a-4a95-9ba3-f6b097e5c2ee":"YubiKey Bio Series - FIDO Edition (Enterprise Profile)","f8a011f3-8c0a-4d15-8006-17111f9edc7d":"Security Key by Yubico","8976631b-d4a0-427f-5773-0ec71c9e0279":"Solo Tap Secp256R1 FIDO2 CTAP2 Authenticator","7dab85a5-d16d-4eaf-a7ef-4c1385b151c5":"YubiKey 5 Series with NFC (Consumer Profile) KVZR57-2","516d3969-5a57-5651-5958-4e7a49434167":"SmartDisplayer BobeePass FIDO2 Authenticator","8681a073-5f50-4d52-bce4-e21658d207b3":"RSA Authenticator 4 for iOS","30b5035e-d297-4ff7-030b-addc96ba6a98":"OneSpan DIGIPASS FX7-C","e41b42a3-60ac-4afb-8757-a98f2d7f6c9f":"Deepnet SafeKey/Classic (FP)","c89e6a38-6c00-5426-5aa5-c9cbf48f0382":"ACS FIDO Authenticator NFC","eb7ef748-cbe0-4b40-b8f6-07bd2d592d35":"YubiKey 5 CCN Series with NFC (Consumer Profile)","a02167b9-ae71-4ac7-9a07-06432ebb6f1c":"YubiKey 5 Series with Lightning","82b0a720-127a-4788-b56d-d1d4b2d82eac":"ID-One Key","2c0df832-92de-4be1-8412-88a8f074df4a":"Feitian FIDO Smart Card","59f85fe7-faa5-4c92-9f52-697b9d4d5473":"RSA Authenticator 4 for Android","22682ee5-4f0e-4c19-be3e-797d5770ebfe":"Precision InnaIT Key FIDO 2 Level 2 certified","79f3c8ba-9e35-484b-8f47-53a5a0f5c630":"YubiKey 5 FIPS Series with NFC (Enterprise Profile)","6e34341a-88c7-483d-b777-a425c355be93":"WebComm OETHenticator","7a53c643-9dec-4219-b3a4-f9d24aca4e12":"G+D StarKey FIDO2-NFC","def8ab1a-9f91-44f1-a103-088d8dc7d681":"IDEMIA SOLVO Fly 80 R3 FIDO Card e","9955a1cd-564c-d388-ad68-9878a27be9f1":"StarSign Chase FIDO Card","9dd8d593-2213-438a-97f8-d6b813d51c27":"YubiKey Bio Fido Edition (Consumer Profile)","970c8d9c-19d2-46af-aa32-3f448db49e35":"WinMagic FIDO Eazy - TPM","c5703116-972b-4851-a3e7-ae1259843399":"NEOWAVE Badgeo FIDO2","c80dbd9a-533f-4a17-b941-1a2f1c7cedff":"HID Crescendo C3000","0b8b05a4-ebd4-4b0b-8f5f-33d7b6e606ab":"HID Crescendo 4000","5b0e46ba-db02-44ac-b979-ca9b84f5e335":"YubiKey 5 FIPS Series with Lightning Preview","12755c32-8ad1-46eb-881c-e0b38d848b09":"Feitian ePass FIDO Authenticator (CTAP2.1, CTAP2.0, U2F)","2a55aee6-27cb-42c0-bc6e-04efe999e88a":"HID Crescendo 4000","820d89ed-d65a-409e-85cb-f73f0578f82a":"IDmelon Authenticator","019614a3-2703-7e35-a453-285fd06c5d24":"ATLKey Authenticator","3124e301-f14e-4e38-876d-fbeeb090e7bf":"YubiKey 5 Series with Lightning Preview","b6ede29c-3772-412c-8a78-539c1f4c62d2":"Feitian BioPass FIDO2 Plus Authenticator","ed042a3a-4b22-4455-bb69-a267b652ae7e":"Security Key NFC by Yubico - Enterprise Edition","b2c1a50b-dad8-4dc7-ba4d-0ce9597904bc":"YubiKey 5 Series with NFC - Enhanced PIN (Enterprise Profile)","85203421-48f9-4355-9bc8-8a53846e5083":"YubiKey 5 FIPS Series with Lightning","fcc0118f-cd45-435b-8da1-9782b2da0715":"YubiKey 5 FIPS Series with NFC","d821a7d4-e97c-4cb6-bd82-4237731fd4be":"Hyper FIDO Bio Security Key","9876631b-d4a0-427f-5773-0ec71c9e0279":"Somu Secp256R1 FIDO2 CTAP2 Authenticator","522a3f91-5f5d-480d-be37-6cecfad5a27b":"Precision InnaIT Key FIDO 2 Level 2 certified","f56f58b3-d711-4afc-ba7d-6ac05f88cb19":"WinMagic FIDO Eazy - Phone","6ec5cff2-a0f9-4169-945b-f33b563f7b99":"YubiKey Bio Series - Multi-protocol Edition (Enterprise Profile)","882adaf5-3aa9-4708-8e7d-3957103775b4":"T-Shield TrustSec FIDO2 Bio and client PIN version","49a15c1c-3f63-3f51-23a7-b9e00096edd1":"IDEX CTAP2.1 Biometrics","f4c63eff-d26c-4248-801c-3736c7eaa93a":"FIDO KeyPass S3","d384db22-4d50-ebde-2eac-5765cf1e2a44":"Excelsecu eSecu FIDO2 Fingerprint Security Key","0db01cd6-5618-455b-bb46-1ec203d3213e":"GoldKey Security Token","b93fd961-f2e6-462f-b122-82002247de78":"Android Authenticator","aa79f476-ea00-417e-9628-1e8365123922":"HID Crescendo 4000 FIDO","1e906e14-77af-46bc-ae9f-fe6ef18257e4":"VeridiumID Passkey iOS SDK","2fc0579f-8113-47ea-b116-bb5a8db9202a":"YubiKey 5 Series with NFC","31c3f7ff-bf15-4327-83ec-9336abcbcd34":"WinMagic FIDO Eazy - Software","cb4f796c-a20a-af9e-d639-213c1ec247f3":"ACS PocketKey+ Bio","9ddd1817-af5a-4672-a2b9-3e3dd95000a9":"Windows Hello","d8522d9f-575b-4866-88a9-ba99fa02f35b":"YubiKey Bio Series - FIDO Edition","050dd0bc-ff20-4265-8d5d-305c4b215192":"eToken Fusion FIPS","50a45b0c-80e7-f944-bf29-f552bfa2e048":"ACS FIDO Authenticator","f7c558a0-f465-11e8-b568-0800200c9a66":"KONAI Secp256R1 FIDO2 Conformance Testing CTAP2 Authenticator","3f59672f-20aa-4afe-b6f4-7e5e916b6d98":"Arculus FIDO 2.1 Key Card [P71]","42b4fb4a-2866-43b2-9bf7-6c6669c2e5d3":"Google Titan Security Key v2","361a3082-0278-4583-a16f-72a527f973e4":"eWBM eFA500 FIDO2 Authenticator","2ffd6452-01da-471f-821b-ea4bf6c8676a":"IDPrime 941 Fido","9806a2c8-c0da-478e-b4ca-620005d34182":"YubiKey Bio Multi-protocol Edition (Consumer Profile) 1VDJSN-2","03012cb7-4fb2-42e7-9e8d-a81f10e2a5e9":"YubiKey 5 Series with Lightning (Consumer Profile)","30b5035e-d297-4ff7-b00b-addc96ba6a98":"OneSpan DIGIPASS FX7","0f083f18-4105-43a8-ad69-24e812e38141":"Security Key Series with NFC (Consumer Profile)","5eaff75a-dd43-451f-af9f-87c9eeae293e":"Swissbit iShield Key 2 FIPS Enterprise","b415094c-49d3-4c8b-b3fe-7d0ad28a6bc4":"ZTPass SmartAuth","692db549-7ae5-44d5-a1e5-dd20a493b723":"HID Crescendo Key","0ebd9f2c-f685-441c-8c3e-a02a234a840a":"YubiKey 5 Series with NFC Enhanced PIN (Consumer Profile)","23315ad0-6aca-4ba1-952e-f044f1e36976":"Clife Key 2 NFC","1d1b4e33-76a1-47fb-97a0-14b10d0933f1":"Cryptnox FIDO2.1","bbf4b6a7-679d-f6fc-c4f2-8ac0ddf9015a":"Excelsecu eSecu FIDO2 PRO Security Key","3e22415d-7fdf-4ea4-8a0c-dd60c4249b9d":"Feitian iePass FIDO Authenticator","ab7d1767-3fa0-4388-b6c4-feef7a844809":"Enterprise Security Key Series with NFC (Enterprise Profile)","f4ce5fc0-57d3-46f5-a736-efb7d5bc63b5":"YubiKey 5 Series with NFC (Consumer Profile)","23786452-f02d-4344-87ed-aaf703726881":"SafeNet eToken Fusion CC","5e264d9d-28ef-4d34-95b4-5941e7a4faa8":"Ideem ZSM FIDO2 Authenticator","d2fbd093-ee62-488d-9dad-1e36389f8826":"YubiKey 5 FIPS Series (RC Preview)","234cd403-35a2-4cc2-8015-77ea280c77f5":"Feitian ePass FIDO2-NFC Series (CTAP2.1, CTAP2.0, U2F)","6999180d-630c-442d-b8f7-424b90a43fae":"Hyper FIDO Pro (CTAP2.1, CTAP2.0, U2F)","662ef48a-95e2-4aaa-a6c1-5b9c40375824":"YubiKey 5 Series with NFC - Enhanced PIN","aeb6569c-f8fb-4950-ac60-24ca2bbe2e52":"HID Crescendo C2300","87dbc5a1-4c94-4dc8-8a47-97d800fd1f3c":"eWBM eFA320 FIDO2 Authenticator","58276709-bb4b-4bb3-baf1-60eea99282a7":"YubiKey Bio Series - Multi-protocol Edition 1VDJSN","7d2afadd-bf6b-44a2-a66b-e831fceb8eff":"Taglio CTAP2.1 EP","30b5035e-d297-4ff1-020b-addc96ba6a98":"OneSpan DIGIPASS FX1-C","04a8fcf2-19c1-457b-911e-69219f17583f":"Thales PAY GFCX13 authenticator","20ac7a17-c814-4833-93fe-539f0d5e3389":"YubiKey 5 Series (Enterprise Profile)","9012593f-43e4-4461-a97a-d92777b55d74":"VinCSS FIDO2 Fingerprint","d7781e5d-e353-46aa-afe2-3ca49f13332a":"YubiKey 5 Series with NFC","9f0d8150-baa5-4c00-9299-ad62c8bb4e87":"GoTrust Idem Card","12ded745-4bed-47d4-abaa-e713f51d6393":"Feitian AllinOne FIDO2 Authenticator","88bbd2f0-342a-42e7-9729-dd158be5407a":"Precision InnaIT Key FIDO 2 Level 2 certified","1d8cac46-47a1-3386-af50-e88ae46fe802":"Ledger Flex FIDO2 Authenticator","dd86a2da-86a0-4cbe-b462-4bd31f57bc6f":"YubiKey Bio Series - FIDO Edition","773c30d9-5919-4e96-a4f5-db65e95cf890":"GSTAG OAK FIDO2 Authenticator","34f5766d-1536-4a24-9033-0e294e510fb0":"YubiKey 5 Series with NFC Preview","83c47309-aabb-4108-8470-8be838b573cb":"YubiKey Bio Series - FIDO Edition (Enterprise Profile)","c1288a5c-d66b-495c-a68f-4e81f9ec5b53":"Deepnet SafeKey/Classic (FP) XF","4e2ddbc2-2687-4709-8551-cb66c9776bfe":"SECORA ID V2 FIDO2.1 L1","be727034-574a-f799-5c76-0929e0430973":"Crayonic KeyVault K1 (USB-NFC-BLE FIDO2 Authenticator)","092277e5-8437-46b5-b911-ea64b294acb7":"Taglio CTAP2.1 CS","ca87cb70-4c1b-4579-a8e8-4efdd7c007e0":"FIDO Alliance TruU Sample FIDO2 Authenticator","23195a52-62d9-40fa-8ee5-23b173f4fb52":"Hyper FIDO Pro NFC","ba0a9266-40d8-4048-9786-d710b5474752":"YubiKey Bio Multi-protocol Edition (Consumer Profile)","3e9db280-256a-4e17-b08e-19d79e9be166":"SECORA ID V2 by Infineon Pay Edition","a7fc3f84-86a3-4da4-a3d7-eb6485a066d8":"NEOWAVE Badgeo FIDO2 (CTAP 2.1)","1f8e43df-71ff-e11d-bea3-c4ee7003b232":"Thetis Pro FIDO2 Key","3ec9c8d3-a5a7-415b-a7b5-f1d606368d3f":"YubiKey 5 CCN Series with NFC (Enterprise Profile)","9e66c661-e428-452a-a8fb-51f7ed088acf":"YubiKey 5 FIPS Series with Lightning (RC Preview)","58b44d0b-0a7c-f33a-fd48-f7153c871352":"Ledger Nano S Plus FIDO2 Authenticator","9a3f2abd-a73d-439c-9ee7-1b53a857eaa7":"YubiKey 5 Series with NFC Enhanced PIN (Enterprise Profile)","454e5346-4944-4ffd-6c93-8e9267193e9b":"Ensurity AUTH BioPro","41e39911-c669-4811-b860-c6ad0b411b96":"YubiKey 5 Series with NFC (Enterprise Profile)","146e77ef-11eb-4423-b847-ce77864e9411":"eToken Fusion NFC PIV","13ac47cf-1d78-4fd5-9060-aedaabacf826":"HID Crescendo Key V3 - Enterprise Edition","e77e3c64-05e3-428b-8824-0cbeb04b829d":"Security Key NFC by Yubico","33d6d7d0-279f-4ef3-96b3-2d3282f4bde6":"Thales eToken Fusion BIO Enterprise","8d4378b0-725d-4432-b3c2-01fcdaf46286":"VeridiumID Passkey Android SDK","7409272d-1ff9-4e10-9fc9-ac0019c124fd":"YubiKey Bio Series - FIDO Edition","bb66c294-de08-47e4-b7aa-d12c2cd3fb20":"Mettlesemi Vishwaas Hawk Authenticator using FIDO2","c4ddaf11-3032-4e77-b3b9-3a340369b9ad":"HID Crescendo Fusion","add92433-0d69-4026-8166-29b25bce64e9":"YubiKey Bio Fido Edition (Enterprise Profile)","7d1351a6-e097-4852-b8bf-c9ac5c9ce4a3":"YubiKey Bio Series - Multi-protocol Edition","07a9f89c-6407-4594-9d56-621d5f1e358b":"NXP Semiconductros FIDO2 Conformance Testing CTAP2 Authenticator","d61d3b87-3e7c-4aea-9c50-441c371903ad":"KeyVault Secp256R1 FIDO2 CTAP2 Authenticator","c62100de-759b-4bf8-b22b-63b3e3a80401":"Token Ring 3 FIDO2 Authenticator","5ca1ab1e-1337-fa57-f1d0-a117e71ca702":"Allthenticator iOS App: roaming BLE FIDO2 Allthenticator for Windows, Mac, Linux, and Allthenticate door readers","b92c3f9a-c014-4056-887f-140a2501163b":"Security Key by Yubico","54d9fee8-e621-4291-8b18-7157b99c5bec":"HID Crescendo Enabled","72a2b5b1-95a5-4df9-a881-4192aff4f72e":"GoTrust Idem Key mini","a25342c0-3cdc-4414-8e46-f4807fca511c":"YubiKey 5 Series with NFC","3a662962-c6d4-4023-bebb-98ae92e78e20":"YubiKey 5 FIPS Series with Lightning (Enterprise Profile)","20f0be98-9af9-986a-4b42-8eca4acb28e4":"Excelsecu eSecu FIDO2 Fingerprint Security Key","ca4cff1b-5a81-4404-8194-59aabcf1660b":"IDPrime 3930 FIDO","ab32f0c6-2239-afbb-c470-d2ef4e254db6":"TEST (DUMMY RECORD)","760eda36-00aa-4d29-855b-4012a182cdeb":"Security Key NFC by Yubico Preview","6028b017-b1d4-4c02-b4b3-afcdafc96bb2":"Windows Hello","b12eac35-586c-4809-a4b1-d81af6c305cf":"Deepnet SafeKey/Classic (NFC)","30b5035e-d297-4fc1-b00b-addc96ba6a97":"OneSpan FIDO Touch","560a780c-b6ae-4f03-b110-082f856425b4":"KQC QuKey Bio FIDO2 Authenticator","1ac71f64-468d-4fe0-bef1-0e5f2f551f18":"YubiKey 5 Series with NFC (Enterprise Profile)","6d44ba9b-f6ec-2e49-b930-0c8fe920cb73":"Security Key by Yubico with NFC","6832d205-75f2-44c7-a864-e868c796d06e":"Precision InnaIT Key FIDO 2 Level 2 certified","9eb85bb6-9625-4a72-815d-0487830ccab2":"Ensurity AUTH BioPro Desktop","30b5035e-d297-4ff7-010b-addc96ba6a98":"OneSpan DIGIPASS FX7-B","5ca1ab1e-fa57-1337-f1d0-a117371ca702":"Allthenticator Android App: roaming BLE FIDO2 Allthenticator for Windows, Mac, Linux, and Allthenticate door readers","eabb46cc-e241-80bf-ae9e-96fa6d2975cf":"TOKEN2 PIN Plus Security Key Series ","53414d53-554e-4700-0000-000000000000":"Samsung Pass","e416201b-afeb-41ca-a03d-2281c28322aa":"ATKey.Pro CTAP2.1","905b4cb4-ed6f-4da9-92fc-45e0d4e9b5c7":"YubiKey 5 FIPS Series (Enterprise Profile)","cfcb13a2-244f-4b36-9077-82b79d6a7de7":"USB/NFC Passcode Authenticator","76692dc1-c56a-48d9-8e7d-31b5ced430ac":"VeriMark NFC+ USB-A Security Key","91ad6b93-264b-4987-8737-3a690cad6917":"Token Ring FIDO2 Authenticator","a02140b7-0cbd-42e1-a9b5-a39da2545114":"Feitian BioPass FIDO2 Plus (Enterprise Profile)","5753362b-4e6b-6345-7b2f-255438404c75":"WiSECURE Blentity FIDO2 Authenticator","9f77e279-a6e2-4d58-b700-31e5943c6a98":"Hyper FIDO Pro","b9f6b7b6-f929-4189-bca9-dd951240c132":"Deepnet SafeKey/Classic (USB)","cc45f64e-52a2-451b-831a-4edd8022a202":"ToothPic Passkey Provider","0bb43545-fd2c-4185-87dd-feb0b2916ace":"Security Key NFC by Yubico - Enterprise Edition","73402251-f2a8-4f03-873e-3cb6db604b03":"uTrust FIDO2 Security Key","c1f9a0bc-1dd2-404a-b27f-8e29047a43fd":"YubiKey 5 FIPS Series with NFC","70e7c36f-f2f6-9e0d-07a6-bcc243262e6b":"OneKey FIDO2 Bluetooth Authenticator","4fc84f16-2545-4e53-b8fc-7bf4d7282a10":"YubiKey 5 CCN Series with NFC (Enterprise Profile)","6ab56fad-881f-4a43-acb2-0be065924522":"YubiKey 5 Series with NFC (Enterprise Profile)","504d7149-4e4c-3841-4555-55445a677357":"WiSECURE AuthTron USB FIDO2 Authenticator","2c2aeed8-8174-4159-814b-486e92a261d0":"NEOWAVE WINKEO V2.0","f2145e86-211e-4931-b874-e22bba7d01cc":"ID-One Key","53334693-4b3f-4198-8857-53772de2ab65":"Precision InnaIT Key FIDO 2 Level 2 certified","a3975549-b191-fd67-b8fb-017e2917fdb3":"Excelsecu eSecu FIDO2 NFC Security Key","19083c3d-8383-4b18-bc03-8f1c9ab2fd1b":"YubiKey 5 Series","da1fa263-8b25-42b6-a820-c0036f21ba7f":"ATKey.Card NFC","6002f033-3c07-ce3e-d0f7-0ffe5ed42543":"Excelsecu eSecu FIDO2 Fingerprint Key","5fdb81b8-53f0-4967-a881-f5ec26fe4d18":"VinCSS FIDO2 Authenticator","78ba3993-d784-4f44-8d6e-cc0a8ad5230e":"Feitian ePass FIDO-NFC(CTAP2.1, CTAP2.0, U2F)","57f7de54-c807-4eab-b1c6-1c9be7984e92":"YubiKey 5 FIPS Series","bb405265-40cf-4115-93e5-a332c1968d8c":"ID-One Card","2d3bec26-15ee-4f5d-88b2-53622490270b":"HID Crescendo Key V2","489ff376-b48d-6640-bb69-782a860ca795":"Mettlesemi Vishwaas Eagle Authenticator using FIDO2","3b24bf49-1d45-4484-a917-13175df0867b":"YubiKey 5 Series with Lightning (Enterprise Profile)","30b5035e-d297-4ff1-010b-addc96ba6a98":"OneSpan DIGIPASS FX1a","cb69481e-8ff7-4039-93ec-0a2729a154a8":"YubiKey 5 Series","0076631b-d4a0-427f-5773-0ec71c9e0279":"HYPR FIDO2 Authenticator","d716019a-9f4e-4041-9750-17c78f8ae81a":"eToken Fusion BIO","57235694-51a5-4a4d-a81a-f42185df6502":"SHALO AUTH","24673149-6c86-42e7-98d9-433fb5b73296":"YubiKey 5 Series with Lightning","357f2718-434f-4124-8a58-7e28c5e4a2fc":"Deepnet SafeKey/Classic (NFC)","42df17de-06ba-4177-a2bb-6701be1380d6":"Feitian BioPass FIDO2 Plus Authenticator","d7a423ad-3e19-4492-9200-78137dccc136":"VivoKey Apex FIDO2","b3315166-f36c-b05f-fea8-66a3dfdad171":"Ledger Nano Gen5 FIDO2 Authenticator","ba76a271-6eb6-4171-874d-b6428dbe3437":"ATKey.ProS","97e6a830-c952-4740-95fc-7c78dc97ce47":"YubiKey Bio Series - Multi-protocol Edition (Enterprise Profile)","f573f209-b7fb-b261-671a-d7cf624cc812":"Excelsecu eSecu FIDO2 PRO+ Security Key","005b20e1-f146-4b87-8f3a-36848ff60ea6":"SECORA ID V2 by Infineon Pay Edition M","6e24d385-004a-16a0-7bfe-efd963845b34":"Ledger Stax FIDO2 Authenticator","ee882879-721c-4913-9775-3dfcce97072a":"YubiKey 5 Series","8876631b-d4a0-427f-5773-0ec71c9e0279":"Solo Secp256R1 FIDO2 CTAP2 Authenticator","fec067a1-f1d0-4c5e-b4c0-cc3237475461":"KX701 SmartToken FIDO","30b5035e-d297-4ff1-b00b-addc96ba6a98":"OneSpan DIGIPASS FX1 BIO","b267239b-954f-4041-a01b-ee4f33c145b6":"authenton1 - CTAP2.1","b50d5e0a-7f81-4959-9b12-f45407407503":"IDPrime 3940 FIDO","8c97a730-3f7b-41a6-87d6-1e9b62bda6f0":"FT-JCOS FIDO Fingerprint Card","99bf4610-ec26-4252-b31f-7380ccd59db5":"ZTPass SmartAuth","a1f52be5-dfab-4364-b51c-2bd496b14a56":"OCTATCO EzFinger2 FIDO2 AUTHENTICATOR","c3479970-e58a-4f70-836f-853bf42fb063":"YubiKey 5 Series with Lightning (Enterprise Profile)","0f00cc22-4640-41e7-9585-384ec73ffe9b":"Taglio CTAP2.1 BIO","ff4dac45-ede8-4ec2-aced-cf66103f4335":"YubiKey 5 Series","ba86dc56-635f-4141-aef6-00227b1b9af6":"TruU Windows Authenticator","3e078ffd-4c54-4586-8baa-a77da113aec5":"Hideez Key 3 FIDO2","fc5ca237-69a0-4f3c-afe4-1ebc66def6df":"Clife Key 2","ec31b4cc-2acc-4b8e-9c01-bade00ccbe26":"KeyXentic FIDO2 Secp256R1 FIDO2 CTAP2 Authenticator","5d629218-d3a5-11ed-afa1-0242ac120002":"Swissbit iShield Key Pro","bb878d7b-cf54-4784-b390-357030497043":"TruU FIDO2 Authenticator","d41f5a69-b817-4144-a13c-9ebd6d9254d6":"ATKey.Card CTAP2.0","e86addcd-7711-47e5-b42a-c18257b0bf61":"IDCore 3121 Fido","b113a455-cfb6-4c17-8cba-cd952feb7d48":"eToken FIDO NFC","95442b2e-f15e-4def-b270-efb106facb4e":"eWBM eFA310 FIDO2 Authenticator","dda9aa35-aaf1-4d3c-b6db-7902fd7dbbbf":"IDEMIA SOLVO Fly 80 R3 FIDO Card c","cdbdaea2-c415-5073-50f7-c04e968640b6":"Excelsecu eSecu FIDO2 Security Key","3aa78eb1-ddd8-46a8-a821-8f8ec57a7bd5":"YubiKey 5 CCN Series with NFC","bc2fe499-0d8e-4ffe-96f3-94a82840cf8c":"OCTATCO EzQuant FIDO2 AUTHENTICATOR","eb3b131e-59dc-536a-d176-cb7306da10f5":"ellipticSecure MIRkey USB Authenticator","3fd410dc-8ab7-4b86-a1cb-c7174620b2dc":"IDEMIA SOLVO Fly 80 R1 FIDO Card Draft","a6c5f5d8-2ad0-48b6-8257-e502c8970931":"eToken FIDO NFC Enterprise","e400ef8c-711d-4692-af46-7f2cf7da23ad":"Swissbit iShield Key 2 Enterprise","87c13177-85d6-40ac-8c61-fe7ab3de9dfb":"HID Crescendo Key V3","1c086528-58d5-f211-823c-356786e36140":"Atos CardOS FIDO2","77010bd7-212a-4fc9-b236-d2ca5e9d4084":"Feitian BioPass FIDO2 Authenticator","d94a29d9-52dd-4247-9c2d-8b818b610389":"VeriMark Guard Fingerprint Key","7b96457d-e3cd-432b-9ceb-c9fdd7ef7432":"YubiKey 5 FIPS Series with Lightning","7991798a-a7f3-487f-98c0-3faf7a458a04":"HID Crescendo Key V3","833b721a-ff5f-4d00-bb2e-bdda3ec01e29":"Feitian ePass FIDO2 Authenticator","c89674e3-a765-4b07-888a-7c086fbdf04b":"StarSign FIDO Card","a11a5faa-9f32-4b8c-8c5d-2f7d13e8c942":"AliasVault","ea9b8d66-4d01-1d21-3ce4-b6b48cb575d4":"Google Password Manager","adce0002-35bc-c60a-648b-0b25f1f05503":"Chrome on Mac","dd4ec289-e01d-41c9-bb89-70fa845d4bf2":"iCloud Keychain (Managed)","531126d6-e717-415c-9320-3d9aa6981239":"Dashlane","bada5566-a7aa-401f-bd96-45619a55120d":"1Password","b84e4048-15dc-4dd0-8640-f4f60813c8af":"NordPass","0ea242b4-43c4-4a1b-8b17-dd6d0b6baec6":"Keeper","891494da-2c90-4d31-a9cd-4eab0aed1309":"Sésame","f3809540-7f14-49c1-a8b3-8f813b225541":"Enpass","b5397666-4885-aa6b-cebf-e52262a439a2":"Chromium Browser","771b48fd-d3d4-4f74-9232-fc157ab0507a":"Edge on Mac","d548826e-79b4-db40-a3d8-11116f7e8349":"Bitwarden","fbfc3007-154e-4ecc-8c0b-6e020557d7bd":"Apple Passwords","66a0ccb3-bd6a-191f-ee06-e375c50b9846":"Thales Bio iOS SDK","8836336a-f590-0921-301d-46427531eee6":"Thales Bio Android SDK","cd69adb5-3c7a-deb9-3177-6800ea6cb72a":"Thales PIN Android SDK","17290f1e-c212-34d0-1423-365d729f09d9":"Thales PIN iOS SDK","50726f74-6f6e-5061-7373-50726f746f6e":"Proton Pass","fdb141b2-5d84-443e-8a35-4698c205a502":"KeePassXC","eaecdef2-1c31-5634-8639-f1cbd9c00a08":"KeePassDX","bfc748bb-3429-4faa-b9f9-7cfa9f3b76d0":"iPasswords","b35a26b2-8f6e-4697-ab1d-d44db4da28c6":"Zoho Vault","b78a0a55-6ef8-d246-a042-ba0f6d55050c":"LastPass","de503f9c-21a4-4f76-b4b7-558eb55c6f89":"Devolutions","22248c4c-7a12-46e2-9a41-44291b373a4d":"LogMeOnce","a10c6dd9-465e-4226-8198-c7c44b91c555":"Kaspersky Password Manager","d350af52-0351-4ba2-acd3-dfeeadc3f764":"pwSafe","d3452668-01fd-4c12-926c-83a4204853aa":"Microsoft Password Manager","6d212b28-a2c1-4638-b375-5932070f62e9":"initial","d49b2120-b865-4191-8cea-be84a52b0485":"Heimlane Vault","d9be9d39-e6a6-4c28-a581-32b044d986e4":"Sticky Password Manager","70617373-7761-6c6c-6669-646f32303236":"Passwall","c9cadfc9-89a9-489e-a25a-c7e86a4d5f15":"Burp Suite Navigation Recorder"} +{"fcb1bcb4-f370-078c-6993-bc24d0ae3fbe":"Ledger Nano X FIDO2 Authenticator","6e8d1eae-8d40-4c25-bcf8-4633959afc71":"Veridium iOS SDK","e8b7f4a2-c3d5-e6f7-890a-b1c2d3e4f567":"Sherlocked","9eb7eabc-9db5-49a1-b6c3-555a802093f4":"YubiKey 5 Series with NFC KVZR57","4d41190c-7beb-4a84-8018-adf265a6352d":"Thales IDPrime FIDO Bio","2772ce93-eb4b-4090-8b73-330f48477d73":"Security Key NFC by Yubico - Enterprise Edition Preview","6dae43be-af9c-417b-8b9f-1b611168ec60":"Dapple Authenticator from Dapple Security Inc.","5626bed4-e756-430b-a7ff-ca78c8b12738":"VALMIDO PRO FIDO","260e3021-482d-442d-838c-7edfbe153b7e":"Feitian ePass FIDO2-NFC Plus Authenticator","95e4d58c-056e-4a65-866d-f5a69659e880":"TruU Windows Authenticator","90636e1f-ef82-43bf-bdcf-5255f139d12f":"YubiKey Bio Series - Multi-protocol Edition","9c835346-796b-4c27-8898-d6032f515cc5":"Cryptnox FIDO2","c3f47802-de73-4dfc-ba22-671fe3304f90":"eToken Fusion NFC PIV Enterprise","0d9b2e56-566b-c393-2940-f821b7f15d6d":"Excelsecu eSecu FIDO2 Pro Security Key","2bff89f2-323a-48fc-b7c8-9ff7fe87c07e":"Feitian BioPass FIDO2 Pro (Enterprise Profile)","c5ef55ff-ad9a-4b9f-b580-adebafe026d0":"YubiKey 5 Series with Lightning","2194b428-9397-4046-8f39-007a1605a482":"IDPrime 931 Fido","39a5647e-1853-446c-a1f6-a79bae9f5bc7":"IDmelon","664d9f67-84a2-412a-9ff7-b4f7d8ee6d05":"OpenSK authenticator","3789da91-f943-46bc-95c3-50ea2012f03a":"NEOWAVE Winkeo FIDO2","fa2b99dc-9e39-4257-8f92-4a30d23c4118":"YubiKey 5 Series with NFC","341e4da9-3c2e-8103-5a9f-aad887135200":"Ledger Nano S FIDO2 Authenticator","69700f79-d1fb-472e-bd9b-a3a3b9a9eda0":"Pone Biometrics OFFPAD Authenticator","8da0e4dc-164b-454e-972e-88f362b23d59":"CardOS FIDO2 Token","89b19028-256b-4025-8872-255358d950e4":"Sentry Enterprises CTAP2 Authenticator","4e768f2c-5fab-48b3-b300-220eb487752b":"Hideez Key 4 FIDO2 SDK","47ab2fb4-66ac-4184-9ae1-86be814012d5":"Security Key NFC by Yubico - Enterprise Edition","931327dd-c89b-406c-a81e-ed7058ef36c6":"Swissbit iShield Key FIDO2","f8d5c4e9-e539-4c06-8662-ec2a4155a555":"StarSign Key Fob","b7d3f68e-88a6-471e-9ecf-2df26d041ede":"Security Key NFC by Yubico","8d1b1fcb-3c76-49a9-9129-5515b346aa02":"IDEMIA ID-ONE Card","30b5035e-d297-4ff7-020b-addc96ba6a98":"OneSpan DIGIPASS FX7","454e5346-4944-4ffd-6c93-8e9267193e9a":"Ensurity ThinC","e1a96183-5016-4f24-b55b-e3ae23614cc6":"ATKey.Pro CTAP2.0","9ff4cc65-6154-4fff-ba09-9e2af7882ad2":"Security Key NFC by Yubico - Enterprise Edition (Enterprise Profile)","4599062e-6926-4fe7-9566-9e8fb1aedaa0":"YubiKey 5 Series (Enterprise Profile)","9d3df6ba-282f-11ed-a261-0242ac120002":"Arculus FIDO2/U2F Key Card","fbefdf68-fe86-0106-213e-4d5fa24cbe2e":"Excelsecu eSecu FIDO2 NFC Security Key","62e54e98-c209-4df3-b692-de71bb6a8528":"YubiKey 5 FIPS Series with NFC Preview","ab32f0c6-2239-afbb-c470-d2ef4e254db7":"TOKEN2 FIDO2 Security Key","ce6bf97f-9f69-4ba7-9032-97adc6ca5cf1":"YubiKey 5 FIPS Series with NFC (RC Preview)","ad08c78a-4e41-49b9-86a2-ac15b06899e2":"YubiKey Bio Series - FIDO Edition (Enterprise Profile)","930b0c03-ef46-4ac4-935c-538dccd1fcdb":"Chipwon Clife Key","7787a482-13e8-4784-8a06-c7ed49a7aaf4":"Swissbit iShield Key 2","72c6b72d-8512-4c66-8359-9d3d10d9222f":"Security Key NFC by Yubico - Enterprise Edition (Enterprise Profile)","99ed6c29-4573-4847-816d-78ad8f1c75ef":"VeroCard FIDO2 Authenticator","973446ca-e21c-9a9b-99f5-9b985a67af0f":"ACS FIDO Authenticator Card","74820b05-a6c9-40f9-8fb0-9f86aca93998":"SafeNet eToken Fusion","1105e4ed-af1d-02ff-ffff-ffffffffffff":"Egomet FIDO2 Authenticator for Android","08987058-cadc-4b81-b6e1-30de50dcbe96":"Windows Hello","a4e9fc6d-4cbe-4758-b8ba-37598bb5bbaa":"Security Key NFC by Yubico","0acf3011-bc60-f375-fb53-6f05f43154e0":"Nymi FIDO2 Authenticator","d91c5288-0ef0-49b7-b8ae-21ca0aa6b3f3":"KEY-ID FIDO2 Authenticator","8eec9bf9-486c-46da-9a67-1fbb4f66b9ed":"HID Crescendo 4000 FIPS","4c50ff10-1057-4fc6-b8ed-43a529530c3c":"ImproveID Authenticator","c611b55c-77b2-4527-8082-590e931b2f08":"GoTrust Idem Key ","ee041bce-25e5-4cdb-8f86-897fd6418464":"Feitian ePass FIDO2-NFC Authenticator","2588ae83-5a3b-4536-b8de-5e540200d191":"Dapple Authenticator from Dapple Security Inc.","4b89f401-464e-4745-a520-486ddfc5d80e":"IIST FIDO2 Authenticator","2cd2f727-f6ca-44da-8f48-5c2e5da000a2":"Nitrokey 3 AM","10c70715-2a9a-4de1-b0aa-3cff6d496d39":"eToken Fusion NFC FIPS","efb96b10-a9ee-4b6c-a4a9-d32125ccd4a4":"Safenet eToken FIDO","24083bcb-3034-4867-99de-a3b52e1d426a":"Enterprise Security Key Series with NFC (Consumer Profile)","4b3f8944-d4f2-4d21-bb19-764a986ec160":"KeyXentic FIDO2 Secp256R1 FIDO2 CTAP2 Authenticator","4c0cf95d-2f40-43b5-ba42-4c83a11c04ba":"Feitian BioPass FIDO2 Pro Authenticator","5343502d-5343-5343-6172-644649444f32":"ESS Smart Card Inc. Authenticator","69e7c36f-f2f6-9e0d-07a6-bcc243262e6b":"OneKey FIDO2 Authenticator","524de2de-982f-49b4-a769-2b5e3b73ad79":"YubiKey 5 Series (Enterprise Profile)","09591fc6-9811-48f7-8f57-b9f23df6413f":"Pone Biometrics OFFPAD Authenticator","912435d9-4a88-42f3-972d-1244b0d51420":"SI0X FIDO CL WRIST v1.0","7e3f3d30-3557-4442-bdae-139312178b39":"RSA DS100","73bb0cd4-e502-49b8-9c6f-b59445bf720b":"YubiKey 5 FIPS Series","39589099-9a75-49fc-afaa-801ca211c62a":"Feitian ePass FIDO-NFC (Enterprise Profile) (CTAP2.1, CTAP2.0, U2F)","dc5e949d-f939-43b3-9877-a85c7186b753":"YubiKey Bio Multi-protocol Edition (Enterprise Profile)","149a2021-8ef6-4133-96b8-81f8d5b7f1f5":"Security Key by Yubico with NFC","5df66f62-5b47-43d3-aa1d-a6e31c8dbeb5":"Securitag Assembly Group FIDO Authenticator NFC","9a272558-5cfa-4424-be37-65509677b77d":"SECORA ID Key S USB by Infineon Consumer Edition","09619fbf-d75e-4a62-be1d-fe4d240864ae":"VeriMark(TM) Guard 2.1 Fingerprint Security Key","50cbf15a-238c-4457-8f16-812c43bf3c49":"Ensurity AUTH TouchPro","ee7fa1e0-9539-432f-bd43-9c2fc6d4f311":"VeriMark NFC+ USB-C Security Key","b90e7dc1-316e-4fee-a25a-56a666a670fe":"YubiKey 5 Series with Lightning (Enterprise Profile)","6d4aa745-dad5-40c4-b9b4-6a252fcee70f":"GoTrust Cyber Key","175cd298-83d2-4a26-b637-313c07a6434e":"Chunghwa Telecom FIDO2 Smart Card Authenticator","34744913-4f57-4e6e-a527-e9ec3c4b94e6":"YubiKey Bio Series - Multi-protocol Edition","5ea308b2-7ac7-48b9-ac09-7e2da9015f8c":"Veridium Android SDK","3b1adb99-0dfe-46fd-90b8-7f7614a4de2a":"GoTrust Idem Key","46544d5d-8f5d-4db4-89ac-ea8977073fff":"Foongtone FIDO Authenticator","0a357157-9b18-4c8a-920e-d156e972b2f8":"YubiKey 5 Series (Consumer Profile)","998f358b-2dd2-4cbe-a43a-e8107438dfb3":"OnlyKey Secp256R1 FIDO2 CTAP2 Authenticator","30b5035e-d297-4ff2-010b-addc96ba6a98":"OneSpan DIGIPASS FX2-A","817cdab8-0d51-4de1-a821-e25b88519cf3":"Swissbit iShield Key 2 FIPS","61250591-b2bc-4456-b719-0b17be90bb30":"eWBM eFPA FIDO2 Authenticator","8c39ee86-7f9a-4a95-9ba3-f6b097e5c2ee":"YubiKey Bio Series - FIDO Edition (Enterprise Profile)","f8a011f3-8c0a-4d15-8006-17111f9edc7d":"Security Key by Yubico","8976631b-d4a0-427f-5773-0ec71c9e0279":"Solo Tap Secp256R1 FIDO2 CTAP2 Authenticator","7dab85a5-d16d-4eaf-a7ef-4c1385b151c5":"YubiKey 5 Series with NFC (Consumer Profile) KVZR57-2","516d3969-5a57-5651-5958-4e7a49434167":"SmartDisplayer BobeePass FIDO2 Authenticator","8681a073-5f50-4d52-bce4-e21658d207b3":"RSA Authenticator 4 for iOS","30b5035e-d297-4ff7-030b-addc96ba6a98":"OneSpan DIGIPASS FX7-C","e41b42a3-60ac-4afb-8757-a98f2d7f6c9f":"Deepnet SafeKey/Classic (FP)","c89e6a38-6c00-5426-5aa5-c9cbf48f0382":"ACS FIDO Authenticator NFC","eb7ef748-cbe0-4b40-b8f6-07bd2d592d35":"YubiKey 5 CCN Series with NFC (Consumer Profile)","a02167b9-ae71-4ac7-9a07-06432ebb6f1c":"YubiKey 5 Series with Lightning","82b0a720-127a-4788-b56d-d1d4b2d82eac":"ID-One Key","2c0df832-92de-4be1-8412-88a8f074df4a":"Feitian FIDO Smart Card","59f85fe7-faa5-4c92-9f52-697b9d4d5473":"RSA Authenticator 4 for Android","22682ee5-4f0e-4c19-be3e-797d5770ebfe":"Precision InnaIT Key FIDO 2 Level 2 certified","79f3c8ba-9e35-484b-8f47-53a5a0f5c630":"YubiKey 5 FIPS Series with NFC (Enterprise Profile)","6e34341a-88c7-483d-b777-a425c355be93":"WebComm OETHenticator","7a53c643-9dec-4219-b3a4-f9d24aca4e12":"G+D StarKey FIDO2-NFC","def8ab1a-9f91-44f1-a103-088d8dc7d681":"IDEMIA SOLVO Fly 80 R3 FIDO Card e","9955a1cd-564c-d388-ad68-9878a27be9f1":"StarSign Chase FIDO Card","9dd8d593-2213-438a-97f8-d6b813d51c27":"YubiKey Bio Fido Edition (Consumer Profile)","970c8d9c-19d2-46af-aa32-3f448db49e35":"WinMagic FIDO Eazy - TPM","c5703116-972b-4851-a3e7-ae1259843399":"NEOWAVE Badgeo FIDO2","c80dbd9a-533f-4a17-b941-1a2f1c7cedff":"HID Crescendo C3000","0b8b05a4-ebd4-4b0b-8f5f-33d7b6e606ab":"HID Crescendo 4000","5b0e46ba-db02-44ac-b979-ca9b84f5e335":"YubiKey 5 FIPS Series with Lightning Preview","12755c32-8ad1-46eb-881c-e0b38d848b09":"Feitian ePass FIDO Authenticator (CTAP2.1, CTAP2.0, U2F)","2a55aee6-27cb-42c0-bc6e-04efe999e88a":"HID Crescendo 4000","820d89ed-d65a-409e-85cb-f73f0578f82a":"IDmelon Authenticator","019614a3-2703-7e35-a453-285fd06c5d24":"ATLKey Authenticator","3124e301-f14e-4e38-876d-fbeeb090e7bf":"YubiKey 5 Series with Lightning Preview","b6ede29c-3772-412c-8a78-539c1f4c62d2":"Feitian BioPass FIDO2 Plus Authenticator","ed042a3a-4b22-4455-bb69-a267b652ae7e":"Security Key NFC by Yubico - Enterprise Edition","b2c1a50b-dad8-4dc7-ba4d-0ce9597904bc":"YubiKey 5 Series with NFC - Enhanced PIN (Enterprise Profile)","85203421-48f9-4355-9bc8-8a53846e5083":"YubiKey 5 FIPS Series with Lightning","fcc0118f-cd45-435b-8da1-9782b2da0715":"YubiKey 5 FIPS Series with NFC","d821a7d4-e97c-4cb6-bd82-4237731fd4be":"Hyper FIDO Bio Security Key","9876631b-d4a0-427f-5773-0ec71c9e0279":"Somu Secp256R1 FIDO2 CTAP2 Authenticator","522a3f91-5f5d-480d-be37-6cecfad5a27b":"Precision InnaIT Key FIDO 2 Level 2 certified","f56f58b3-d711-4afc-ba7d-6ac05f88cb19":"WinMagic FIDO Eazy - Phone","6ec5cff2-a0f9-4169-945b-f33b563f7b99":"YubiKey Bio Series - Multi-protocol Edition (Enterprise Profile)","882adaf5-3aa9-4708-8e7d-3957103775b4":"T-Shield TrustSec FIDO2 Bio and client PIN version","49a15c1c-3f63-3f51-23a7-b9e00096edd1":"IDEX CTAP2.1 Biometrics","f4c63eff-d26c-4248-801c-3736c7eaa93a":"FIDO KeyPass S3","d384db22-4d50-ebde-2eac-5765cf1e2a44":"Excelsecu eSecu FIDO2 Fingerprint Security Key","0db01cd6-5618-455b-bb46-1ec203d3213e":"GoldKey Security Token","b93fd961-f2e6-462f-b122-82002247de78":"Android Authenticator","aa79f476-ea00-417e-9628-1e8365123922":"HID Crescendo 4000 FIDO","1e906e14-77af-46bc-ae9f-fe6ef18257e4":"VeridiumID Passkey iOS SDK","2fc0579f-8113-47ea-b116-bb5a8db9202a":"YubiKey 5 Series with NFC","31c3f7ff-bf15-4327-83ec-9336abcbcd34":"WinMagic FIDO Eazy - Software","cb4f796c-a20a-af9e-d639-213c1ec247f3":"ACS PocketKey+ Bio","9ddd1817-af5a-4672-a2b9-3e3dd95000a9":"Windows Hello","d8522d9f-575b-4866-88a9-ba99fa02f35b":"YubiKey Bio Series - FIDO Edition","050dd0bc-ff20-4265-8d5d-305c4b215192":"eToken Fusion FIPS","50a45b0c-80e7-f944-bf29-f552bfa2e048":"ACS FIDO Authenticator","f7c558a0-f465-11e8-b568-0800200c9a66":"KONAI Secp256R1 FIDO2 Conformance Testing CTAP2 Authenticator","3f59672f-20aa-4afe-b6f4-7e5e916b6d98":"Arculus FIDO 2.1 Key Card [P71]","42b4fb4a-2866-43b2-9bf7-6c6669c2e5d3":"Google Titan Security Key v2","361a3082-0278-4583-a16f-72a527f973e4":"eWBM eFA500 FIDO2 Authenticator","2ffd6452-01da-471f-821b-ea4bf6c8676a":"IDPrime 941 Fido","9806a2c8-c0da-478e-b4ca-620005d34182":"YubiKey Bio Multi-protocol Edition (Consumer Profile) 1VDJSN-2","03012cb7-4fb2-42e7-9e8d-a81f10e2a5e9":"YubiKey 5 Series with Lightning (Consumer Profile)","30b5035e-d297-4ff7-b00b-addc96ba6a98":"OneSpan DIGIPASS FX7","0f083f18-4105-43a8-ad69-24e812e38141":"Security Key Series with NFC (Consumer Profile)","5eaff75a-dd43-451f-af9f-87c9eeae293e":"Swissbit iShield Key 2 FIPS Enterprise","b415094c-49d3-4c8b-b3fe-7d0ad28a6bc4":"ZTPass SmartAuth","692db549-7ae5-44d5-a1e5-dd20a493b723":"HID Crescendo Key","0ebd9f2c-f685-441c-8c3e-a02a234a840a":"YubiKey 5 Series with NFC Enhanced PIN (Consumer Profile)","23315ad0-6aca-4ba1-952e-f044f1e36976":"Clife Key 2 NFC","1d1b4e33-76a1-47fb-97a0-14b10d0933f1":"Cryptnox FIDO2.1","bbf4b6a7-679d-f6fc-c4f2-8ac0ddf9015a":"Excelsecu eSecu FIDO2 PRO Security Key","3e22415d-7fdf-4ea4-8a0c-dd60c4249b9d":"Feitian iePass FIDO Authenticator","ab7d1767-3fa0-4388-b6c4-feef7a844809":"Enterprise Security Key Series with NFC (Enterprise Profile)","f4ce5fc0-57d3-46f5-a736-efb7d5bc63b5":"YubiKey 5 Series with NFC (Consumer Profile)","23786452-f02d-4344-87ed-aaf703726881":"SafeNet eToken Fusion CC","5e264d9d-28ef-4d34-95b4-5941e7a4faa8":"Ideem ZSM FIDO2 Authenticator","d2fbd093-ee62-488d-9dad-1e36389f8826":"YubiKey 5 FIPS Series (RC Preview)","234cd403-35a2-4cc2-8015-77ea280c77f5":"Feitian ePass FIDO2-NFC Series (CTAP2.1, CTAP2.0, U2F)","6999180d-630c-442d-b8f7-424b90a43fae":"Hyper FIDO Pro (CTAP2.1, CTAP2.0, U2F)","662ef48a-95e2-4aaa-a6c1-5b9c40375824":"YubiKey 5 Series with NFC - Enhanced PIN","aeb6569c-f8fb-4950-ac60-24ca2bbe2e52":"HID Crescendo C2300","87dbc5a1-4c94-4dc8-8a47-97d800fd1f3c":"eWBM eFA320 FIDO2 Authenticator","58276709-bb4b-4bb3-baf1-60eea99282a7":"YubiKey Bio Series - Multi-protocol Edition 1VDJSN","7d2afadd-bf6b-44a2-a66b-e831fceb8eff":"Taglio CTAP2.1 EP","30b5035e-d297-4ff1-020b-addc96ba6a98":"OneSpan DIGIPASS FX1-C","04a8fcf2-19c1-457b-911e-69219f17583f":"Thales PAY GFCX13 authenticator","20ac7a17-c814-4833-93fe-539f0d5e3389":"YubiKey 5 Series (Enterprise Profile)","9012593f-43e4-4461-a97a-d92777b55d74":"VinCSS FIDO2 Fingerprint","d7781e5d-e353-46aa-afe2-3ca49f13332a":"YubiKey 5 Series with NFC","9f0d8150-baa5-4c00-9299-ad62c8bb4e87":"GoTrust Idem Card","12ded745-4bed-47d4-abaa-e713f51d6393":"Feitian AllinOne FIDO2 Authenticator","88bbd2f0-342a-42e7-9729-dd158be5407a":"Precision InnaIT Key FIDO 2 Level 2 certified","1d8cac46-47a1-3386-af50-e88ae46fe802":"Ledger Flex FIDO2 Authenticator","dd86a2da-86a0-4cbe-b462-4bd31f57bc6f":"YubiKey Bio Series - FIDO Edition","773c30d9-5919-4e96-a4f5-db65e95cf890":"GSTAG OAK FIDO2 Authenticator","34f5766d-1536-4a24-9033-0e294e510fb0":"YubiKey 5 Series with NFC Preview","83c47309-aabb-4108-8470-8be838b573cb":"YubiKey Bio Series - FIDO Edition (Enterprise Profile)","c1288a5c-d66b-495c-a68f-4e81f9ec5b53":"Deepnet SafeKey/Classic (FP) XF","4e2ddbc2-2687-4709-8551-cb66c9776bfe":"SECORA ID V2 FIDO2.1 L1","be727034-574a-f799-5c76-0929e0430973":"Crayonic KeyVault K1 (USB-NFC-BLE FIDO2 Authenticator)","092277e5-8437-46b5-b911-ea64b294acb7":"Taglio CTAP2.1 CS","ca87cb70-4c1b-4579-a8e8-4efdd7c007e0":"FIDO Alliance TruU Sample FIDO2 Authenticator","23195a52-62d9-40fa-8ee5-23b173f4fb52":"Hyper FIDO Pro NFC","ba0a9266-40d8-4048-9786-d710b5474752":"YubiKey Bio Multi-protocol Edition (Consumer Profile)","3e9db280-256a-4e17-b08e-19d79e9be166":"SECORA ID V2 by Infineon Pay Edition","a7fc3f84-86a3-4da4-a3d7-eb6485a066d8":"NEOWAVE Badgeo FIDO2 (CTAP 2.1)","1f8e43df-71ff-e11d-bea3-c4ee7003b232":"Thetis Pro FIDO2 Key","3ec9c8d3-a5a7-415b-a7b5-f1d606368d3f":"YubiKey 5 CCN Series with NFC (Enterprise Profile)","9e66c661-e428-452a-a8fb-51f7ed088acf":"YubiKey 5 FIPS Series with Lightning (RC Preview)","58b44d0b-0a7c-f33a-fd48-f7153c871352":"Ledger Nano S Plus FIDO2 Authenticator","9a3f2abd-a73d-439c-9ee7-1b53a857eaa7":"YubiKey 5 Series with NFC Enhanced PIN (Enterprise Profile)","454e5346-4944-4ffd-6c93-8e9267193e9b":"Ensurity AUTH BioPro","41e39911-c669-4811-b860-c6ad0b411b96":"YubiKey 5 Series with NFC (Enterprise Profile)","146e77ef-11eb-4423-b847-ce77864e9411":"eToken Fusion NFC PIV","13ac47cf-1d78-4fd5-9060-aedaabacf826":"HID Crescendo Key V3 - Enterprise Edition","e77e3c64-05e3-428b-8824-0cbeb04b829d":"Security Key NFC by Yubico","33d6d7d0-279f-4ef3-96b3-2d3282f4bde6":"Thales eToken Fusion BIO Enterprise","8d4378b0-725d-4432-b3c2-01fcdaf46286":"VeridiumID Passkey Android SDK","7409272d-1ff9-4e10-9fc9-ac0019c124fd":"YubiKey Bio Series - FIDO Edition","bb66c294-de08-47e4-b7aa-d12c2cd3fb20":"Mettlesemi Vishwaas Hawk Authenticator using FIDO2","c4ddaf11-3032-4e77-b3b9-3a340369b9ad":"HID Crescendo Fusion","add92433-0d69-4026-8166-29b25bce64e9":"YubiKey Bio Fido Edition (Enterprise Profile)","7d1351a6-e097-4852-b8bf-c9ac5c9ce4a3":"YubiKey Bio Series - Multi-protocol Edition","07a9f89c-6407-4594-9d56-621d5f1e358b":"NXP Semiconductros FIDO2 Conformance Testing CTAP2 Authenticator","d61d3b87-3e7c-4aea-9c50-441c371903ad":"KeyVault Secp256R1 FIDO2 CTAP2 Authenticator","c62100de-759b-4bf8-b22b-63b3e3a80401":"Token Ring 3 FIDO2 Authenticator","5ca1ab1e-1337-fa57-f1d0-a117e71ca702":"Allthenticator iOS App: roaming BLE FIDO2 Allthenticator for Windows, Mac, Linux, and Allthenticate door readers","b92c3f9a-c014-4056-887f-140a2501163b":"Security Key by Yubico","54d9fee8-e621-4291-8b18-7157b99c5bec":"HID Crescendo Enabled","72a2b5b1-95a5-4df9-a881-4192aff4f72e":"GoTrust Idem Key mini","a25342c0-3cdc-4414-8e46-f4807fca511c":"YubiKey 5 Series with NFC","3a662962-c6d4-4023-bebb-98ae92e78e20":"YubiKey 5 FIPS Series with Lightning (Enterprise Profile)","20f0be98-9af9-986a-4b42-8eca4acb28e4":"Excelsecu eSecu FIDO2 Fingerprint Security Key","ca4cff1b-5a81-4404-8194-59aabcf1660b":"IDPrime 3930 FIDO","ab32f0c6-2239-afbb-c470-d2ef4e254db6":"TEST (DUMMY RECORD)","760eda36-00aa-4d29-855b-4012a182cdeb":"Security Key NFC by Yubico Preview","6028b017-b1d4-4c02-b4b3-afcdafc96bb2":"Windows Hello","b12eac35-586c-4809-a4b1-d81af6c305cf":"Deepnet SafeKey/Classic (NFC)","30b5035e-d297-4fc1-b00b-addc96ba6a97":"OneSpan FIDO Touch","560a780c-b6ae-4f03-b110-082f856425b4":"KQC QuKey Bio FIDO2 Authenticator","1ac71f64-468d-4fe0-bef1-0e5f2f551f18":"YubiKey 5 Series with NFC (Enterprise Profile)","6d44ba9b-f6ec-2e49-b930-0c8fe920cb73":"Security Key by Yubico with NFC","6832d205-75f2-44c7-a864-e868c796d06e":"Precision InnaIT Key FIDO 2 Level 2 certified","9eb85bb6-9625-4a72-815d-0487830ccab2":"Ensurity AUTH BioPro Desktop","30b5035e-d297-4ff7-010b-addc96ba6a98":"OneSpan DIGIPASS FX7-B","5ca1ab1e-fa57-1337-f1d0-a117371ca702":"Allthenticator Android App: roaming BLE FIDO2 Allthenticator for Windows, Mac, Linux, and Allthenticate door readers","eabb46cc-e241-80bf-ae9e-96fa6d2975cf":"TOKEN2 PIN Plus Security Key Series ","53414d53-554e-4700-0000-000000000000":"Samsung Pass","e416201b-afeb-41ca-a03d-2281c28322aa":"ATKey.Pro CTAP2.1","905b4cb4-ed6f-4da9-92fc-45e0d4e9b5c7":"YubiKey 5 FIPS Series (Enterprise Profile)","cfcb13a2-244f-4b36-9077-82b79d6a7de7":"USB/NFC Passcode Authenticator","76692dc1-c56a-48d9-8e7d-31b5ced430ac":"VeriMark NFC+ USB-A Security Key","91ad6b93-264b-4987-8737-3a690cad6917":"Token Ring FIDO2 Authenticator","a02140b7-0cbd-42e1-a9b5-a39da2545114":"Feitian BioPass FIDO2 Plus (Enterprise Profile)","5753362b-4e6b-6345-7b2f-255438404c75":"WiSECURE Blentity FIDO2 Authenticator","9f77e279-a6e2-4d58-b700-31e5943c6a98":"Hyper FIDO Pro","b9f6b7b6-f929-4189-bca9-dd951240c132":"Deepnet SafeKey/Classic (USB)","cc45f64e-52a2-451b-831a-4edd8022a202":"ToothPic Passkey Provider","0bb43545-fd2c-4185-87dd-feb0b2916ace":"Security Key NFC by Yubico - Enterprise Edition","73402251-f2a8-4f03-873e-3cb6db604b03":"uTrust FIDO2 Security Key","c1f9a0bc-1dd2-404a-b27f-8e29047a43fd":"YubiKey 5 FIPS Series with NFC","70e7c36f-f2f6-9e0d-07a6-bcc243262e6b":"OneKey FIDO2 Bluetooth Authenticator","4fc84f16-2545-4e53-b8fc-7bf4d7282a10":"YubiKey 5 CCN Series with NFC (Enterprise Profile)","6ab56fad-881f-4a43-acb2-0be065924522":"YubiKey 5 Series with NFC (Enterprise Profile)","504d7149-4e4c-3841-4555-55445a677357":"WiSECURE AuthTron USB FIDO2 Authenticator","2c2aeed8-8174-4159-814b-486e92a261d0":"NEOWAVE WINKEO V2.0","f2145e86-211e-4931-b874-e22bba7d01cc":"ID-One Key","53334693-4b3f-4198-8857-53772de2ab65":"Precision InnaIT Key FIDO 2 Level 2 certified","a3975549-b191-fd67-b8fb-017e2917fdb3":"Excelsecu eSecu FIDO2 NFC Security Key","19083c3d-8383-4b18-bc03-8f1c9ab2fd1b":"YubiKey 5 Series","da1fa263-8b25-42b6-a820-c0036f21ba7f":"ATKey.Card NFC","6002f033-3c07-ce3e-d0f7-0ffe5ed42543":"Excelsecu eSecu FIDO2 Fingerprint Key","5fdb81b8-53f0-4967-a881-f5ec26fe4d18":"VinCSS FIDO2 Authenticator","78ba3993-d784-4f44-8d6e-cc0a8ad5230e":"Feitian ePass FIDO-NFC(CTAP2.1, CTAP2.0, U2F)","57f7de54-c807-4eab-b1c6-1c9be7984e92":"YubiKey 5 FIPS Series","bb405265-40cf-4115-93e5-a332c1968d8c":"ID-One Card","2d3bec26-15ee-4f5d-88b2-53622490270b":"HID Crescendo Key V2","489ff376-b48d-6640-bb69-782a860ca795":"Mettlesemi Vishwaas Eagle Authenticator using FIDO2","3b24bf49-1d45-4484-a917-13175df0867b":"YubiKey 5 Series with Lightning (Enterprise Profile)","30b5035e-d297-4ff1-010b-addc96ba6a98":"OneSpan DIGIPASS FX1a","cb69481e-8ff7-4039-93ec-0a2729a154a8":"YubiKey 5 Series","0076631b-d4a0-427f-5773-0ec71c9e0279":"HYPR FIDO2 Authenticator","d716019a-9f4e-4041-9750-17c78f8ae81a":"eToken Fusion BIO","57235694-51a5-4a4d-a81a-f42185df6502":"SHALO AUTH","24673149-6c86-42e7-98d9-433fb5b73296":"YubiKey 5 Series with Lightning","357f2718-434f-4124-8a58-7e28c5e4a2fc":"Deepnet SafeKey/Classic (NFC)","42df17de-06ba-4177-a2bb-6701be1380d6":"Feitian BioPass FIDO2 Plus Authenticator","d7a423ad-3e19-4492-9200-78137dccc136":"VivoKey Apex FIDO2","b3315166-f36c-b05f-fea8-66a3dfdad171":"Ledger Nano Gen5 FIDO2 Authenticator","ba76a271-6eb6-4171-874d-b6428dbe3437":"ATKey.ProS","97e6a830-c952-4740-95fc-7c78dc97ce47":"YubiKey Bio Series - Multi-protocol Edition (Enterprise Profile)","f573f209-b7fb-b261-671a-d7cf624cc812":"Excelsecu eSecu FIDO2 PRO+ Security Key","005b20e1-f146-4b87-8f3a-36848ff60ea6":"SECORA ID V2 by Infineon Pay Edition M","6e24d385-004a-16a0-7bfe-efd963845b34":"Ledger Stax FIDO2 Authenticator","ee882879-721c-4913-9775-3dfcce97072a":"YubiKey 5 Series","8876631b-d4a0-427f-5773-0ec71c9e0279":"Solo Secp256R1 FIDO2 CTAP2 Authenticator","fec067a1-f1d0-4c5e-b4c0-cc3237475461":"KX701 SmartToken FIDO","30b5035e-d297-4ff1-b00b-addc96ba6a98":"OneSpan DIGIPASS FX1 BIO","b267239b-954f-4041-a01b-ee4f33c145b6":"authenton1 - CTAP2.1","b50d5e0a-7f81-4959-9b12-f45407407503":"IDPrime 3940 FIDO","8c97a730-3f7b-41a6-87d6-1e9b62bda6f0":"FT-JCOS FIDO Fingerprint Card","99bf4610-ec26-4252-b31f-7380ccd59db5":"ZTPass SmartAuth","a1f52be5-dfab-4364-b51c-2bd496b14a56":"OCTATCO EzFinger2 FIDO2 AUTHENTICATOR","c3479970-e58a-4f70-836f-853bf42fb063":"YubiKey 5 Series with Lightning (Enterprise Profile)","0f00cc22-4640-41e7-9585-384ec73ffe9b":"Taglio CTAP2.1 BIO","ff4dac45-ede8-4ec2-aced-cf66103f4335":"YubiKey 5 Series","ba86dc56-635f-4141-aef6-00227b1b9af6":"TruU Windows Authenticator","3e078ffd-4c54-4586-8baa-a77da113aec5":"Hideez Key 3 FIDO2","fc5ca237-69a0-4f3c-afe4-1ebc66def6df":"Clife Key 2","ec31b4cc-2acc-4b8e-9c01-bade00ccbe26":"KeyXentic FIDO2 Secp256R1 FIDO2 CTAP2 Authenticator","5d629218-d3a5-11ed-afa1-0242ac120002":"Swissbit iShield Key Pro","bb878d7b-cf54-4784-b390-357030497043":"TruU FIDO2 Authenticator","d41f5a69-b817-4144-a13c-9ebd6d9254d6":"ATKey.Card CTAP2.0","e86addcd-7711-47e5-b42a-c18257b0bf61":"IDCore 3121 Fido","b113a455-cfb6-4c17-8cba-cd952feb7d48":"eToken FIDO NFC","95442b2e-f15e-4def-b270-efb106facb4e":"eWBM eFA310 FIDO2 Authenticator","dda9aa35-aaf1-4d3c-b6db-7902fd7dbbbf":"IDEMIA SOLVO Fly 80 R3 FIDO Card c","cdbdaea2-c415-5073-50f7-c04e968640b6":"Excelsecu eSecu FIDO2 Security Key","3aa78eb1-ddd8-46a8-a821-8f8ec57a7bd5":"YubiKey 5 CCN Series with NFC","bc2fe499-0d8e-4ffe-96f3-94a82840cf8c":"OCTATCO EzQuant FIDO2 AUTHENTICATOR","eb3b131e-59dc-536a-d176-cb7306da10f5":"ellipticSecure MIRkey USB Authenticator","3fd410dc-8ab7-4b86-a1cb-c7174620b2dc":"IDEMIA SOLVO Fly 80 R1 FIDO Card Draft","a6c5f5d8-2ad0-48b6-8257-e502c8970931":"eToken FIDO NFC Enterprise","e400ef8c-711d-4692-af46-7f2cf7da23ad":"Swissbit iShield Key 2 Enterprise","87c13177-85d6-40ac-8c61-fe7ab3de9dfb":"HID Crescendo Key V3","1c086528-58d5-f211-823c-356786e36140":"Atos CardOS FIDO2","77010bd7-212a-4fc9-b236-d2ca5e9d4084":"Feitian BioPass FIDO2 Authenticator","d94a29d9-52dd-4247-9c2d-8b818b610389":"VeriMark Guard Fingerprint Key","7b96457d-e3cd-432b-9ceb-c9fdd7ef7432":"YubiKey 5 FIPS Series with Lightning","7991798a-a7f3-487f-98c0-3faf7a458a04":"HID Crescendo Key V3","833b721a-ff5f-4d00-bb2e-bdda3ec01e29":"Feitian ePass FIDO2 Authenticator","c89674e3-a765-4b07-888a-7c086fbdf04b":"StarSign FIDO Card","a11a5faa-9f32-4b8c-8c5d-2f7d13e8c942":"AliasVault","ea9b8d66-4d01-1d21-3ce4-b6b48cb575d4":"Google Password Manager","adce0002-35bc-c60a-648b-0b25f1f05503":"Chrome on Mac","dd4ec289-e01d-41c9-bb89-70fa845d4bf2":"iCloud Keychain (Managed)","531126d6-e717-415c-9320-3d9aa6981239":"Dashlane","bada5566-a7aa-401f-bd96-45619a55120d":"1Password","b84e4048-15dc-4dd0-8640-f4f60813c8af":"NordPass","0ea242b4-43c4-4a1b-8b17-dd6d0b6baec6":"Keeper","891494da-2c90-4d31-a9cd-4eab0aed1309":"Sésame","f3809540-7f14-49c1-a8b3-8f813b225541":"Enpass","b5397666-4885-aa6b-cebf-e52262a439a2":"Chromium Browser","771b48fd-d3d4-4f74-9232-fc157ab0507a":"Edge on Mac","d548826e-79b4-db40-a3d8-11116f7e8349":"Bitwarden","fbfc3007-154e-4ecc-8c0b-6e020557d7bd":"Apple Passwords","66a0ccb3-bd6a-191f-ee06-e375c50b9846":"Thales Bio iOS SDK","8836336a-f590-0921-301d-46427531eee6":"Thales Bio Android SDK","cd69adb5-3c7a-deb9-3177-6800ea6cb72a":"Thales PIN Android SDK","17290f1e-c212-34d0-1423-365d729f09d9":"Thales PIN iOS SDK","50726f74-6f6e-5061-7373-50726f746f6e":"Proton Pass","fdb141b2-5d84-443e-8a35-4698c205a502":"KeePassXC","eaecdef2-1c31-5634-8639-f1cbd9c00a08":"KeePassDX","9addb28c-b46f-4402-808f-019651441ff3":"KeePassPasskey","bfc748bb-3429-4faa-b9f9-7cfa9f3b76d0":"iPasswords","b35a26b2-8f6e-4697-ab1d-d44db4da28c6":"Zoho Vault","b78a0a55-6ef8-d246-a042-ba0f6d55050c":"LastPass","de503f9c-21a4-4f76-b4b7-558eb55c6f89":"Devolutions","22248c4c-7a12-46e2-9a41-44291b373a4d":"LogMeOnce","a10c6dd9-465e-4226-8198-c7c44b91c555":"Kaspersky Password Manager","d350af52-0351-4ba2-acd3-dfeeadc3f764":"pwSafe","d3452668-01fd-4c12-926c-83a4204853aa":"Microsoft Password Manager","6d212b28-a2c1-4638-b375-5932070f62e9":"initial","d49b2120-b865-4191-8cea-be84a52b0485":"Heimlane Vault","d9be9d39-e6a6-4c28-a581-32b044d986e4":"Sticky Password Manager","70617373-7761-6c6c-6669-646f32303236":"Passwall","c9cadfc9-89a9-489e-a25a-c7e86a4d5f15":"Burp Suite Navigation Recorder"} diff --git a/backend/resources/fonts/Gloock.ttf b/backend/resources/fonts/Gloock.ttf new file mode 100644 index 00000000..3e58c4e4 Binary files /dev/null and b/backend/resources/fonts/Gloock.ttf differ diff --git a/backend/resources/fonts/PlayfairDisplay-Bold.ttf b/backend/resources/fonts/PlayfairDisplay-Bold.ttf deleted file mode 100644 index 029a1a60..00000000 Binary files a/backend/resources/fonts/PlayfairDisplay-Bold.ttf and /dev/null differ diff --git a/frontend/messages/tr.json b/frontend/messages/tr.json index 537f416b..dfccd700 100644 --- a/frontend/messages/tr.json +++ b/frontend/messages/tr.json @@ -249,7 +249,7 @@ "edit": "Düzenle", "user_groups_updated_successfully": "Kullanıcı grupları başarıyla güncellendi", "user_updated_successfully": "Kullanıcı başarıyla güncellendi", - "custom_claims_updated_successfully": "Özel alanlar başarıyla güncellendi", + "custom_claims_updated_successfully": "Özel yetkiler başarıyla güncellendi", "back": "Geri", "user_details_firstname_lastname": "Kullanıcı Detayları {firstName} {lastName}", "manage_which_groups_this_user_belongs_to": "Bu kullanıcının ait olduğu grupları yönetin.", diff --git a/frontend/package.json b/frontend/package.json index 08a0178d..486cacac 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,64 +1,65 @@ { - "name": "pocket-id-frontend", - "version": "2.7.0", - "private": true, - "type": "module", - "scripts": { - "preinstall": "npx only-allow pnpm", - "dev": "vite dev --port 3000", - "build": "vite build", - "preview": "vite preview --port 3000", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "lint": "prettier --check . && eslint .", - "format": "prettier --write ." - }, - "dependencies": { - "@simplewebauthn/browser": "^13.3.0", - "@tailwindcss/vite": "^4.3.0", - "axios": "^1.16.1", - "clsx": "^2.1.1", - "date-fns": "^4.2.1", - "jose": "^6.2.3", - "qrcode": "^1.5.4", - "runed": "^0.37.1", - "sveltekit-superforms": "^2.30.1", - "tailwind-merge": "^3.6.0", - "zod": "^4.4.3" - }, - "devDependencies": { - "@inlang/paraglide-js": "^2.18.0", - "@inlang/plugin-m-function-matcher": "^2.2.6", - "@inlang/plugin-message-format": "^4.4.0", - "@internationalized/date": "^3.12.1", - "@lucide/svelte": "^1.16.0", - "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.60.1", - "@sveltejs/vite-plugin-svelte": "^7.1.2", - "@types/eslint": "^9.6.1", - "@types/node": "^25.9.0", - "@types/qrcode": "^1.5.6", - "bits-ui": "^2.18.1", - "eslint": "^10.4.0", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-svelte": "^3.17.1", - "formsnap": "^2.0.1", - "globals": "^17.6.0", - "mode-watcher": "^1.1.0", - "prettier": "^3.8.3", - "prettier-plugin-svelte": "^3.5.2", - "prettier-plugin-tailwindcss": "^0.8.0", - "rollup": "^4.60.4", - "svelte": "^5.55.8", - "svelte-check": "^4.4.8", - "svelte-sonner": "^1.1.1", - "tailwind-variants": "^3.2.2", - "tailwindcss": "^4.3.0", - "tslib": "^2.8.1", - "tw-animate-css": "^1.4.0", - "typescript": "^6.0.3", - "typescript-eslint": "^8.59.4", - "vite": "^8.0.13", - "vite-plugin-compression": "^0.5.1" - } + "name": "pocket-id-frontend", + "version": "2.7.0", + "private": true, + "type": "module", + "scripts": { + "preinstall": "npx only-allow pnpm", + "dev": "vite dev --port 3000", + "build": "vite build", + "preview": "vite preview --port 3000", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "prettier --check . && eslint .", + "format": "prettier --write ." + }, + "dependencies": { + "@fontsource/gloock": "^5.2.8", + "@simplewebauthn/browser": "^13.3.0", + "@tailwindcss/vite": "^4.3.0", + "axios": "^1.16.1", + "clsx": "^2.1.1", + "date-fns": "^4.2.1", + "jose": "^6.2.3", + "qrcode": "^1.5.4", + "runed": "^0.37.1", + "sveltekit-superforms": "^2.30.1", + "tailwind-merge": "^3.6.0", + "zod": "^4.4.3" + }, + "devDependencies": { + "@inlang/paraglide-js": "^2.18.0", + "@inlang/plugin-m-function-matcher": "^2.2.6", + "@inlang/plugin-message-format": "^4.4.0", + "@internationalized/date": "^3.12.1", + "@lucide/svelte": "^1.16.0", + "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/kit": "^2.60.1", + "@sveltejs/vite-plugin-svelte": "^7.1.2", + "@types/eslint": "^9.6.1", + "@types/node": "^25.9.0", + "@types/qrcode": "^1.5.6", + "bits-ui": "^2.18.1", + "eslint": "^10.4.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-svelte": "^3.17.1", + "formsnap": "^2.0.1", + "globals": "^17.6.0", + "mode-watcher": "^1.1.0", + "prettier": "^3.8.3", + "prettier-plugin-svelte": "^3.5.2", + "prettier-plugin-tailwindcss": "^0.8.0", + "rollup": "^4.60.4", + "svelte": "^5.55.8", + "svelte-check": "^4.4.8", + "svelte-sonner": "^1.1.1", + "tailwind-variants": "^3.2.2", + "tailwindcss": "^4.3.0", + "tslib": "^2.8.1", + "tw-animate-css": "^1.4.0", + "typescript": "^6.0.3", + "typescript-eslint": "^8.59.4", + "vite": "^8.0.13", + "vite-plugin-compression": "^0.5.1" + } } diff --git a/frontend/src/app.css b/frontend/src/app.css index a29105f9..27e1d304 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -1,5 +1,6 @@ @import 'tailwindcss'; @import 'tw-animate-css'; +@import '@fontsource/gloock'; @variant dark (&:where(.dark, .dark *)); @@ -22,7 +23,7 @@ } :root { - --radius: 0.625rem; + --radius: 1rem; --background: oklch(1 0 0); --foreground: oklch(0.145 0 0); --card: oklch(1 0 0); @@ -133,13 +134,8 @@ --color-sidebar-border: var(--sidebar-border); --color-sidebar-ring: var(--sidebar-ring); - /* Animations */ - --animate-accordion-up: accordion-up 0.2s ease-out; - --animate-accordion-down: accordion-down 0.2s ease-out; - --animate-caret-blink: caret-blink 1.25s ease-out infinite; - /* Font */ - --font-playfair: 'Playfair Display', serif; + --font-glooock: 'glooock ', serif; } @layer base { @@ -154,60 +150,6 @@ button { @apply cursor-pointer; } - - @font-face { - font-family: 'Playfair Display'; - font-weight: 400; - src: url('/fonts/PlayfairDisplay-Regular.woff') format('woff'); - } - @font-face { - font-family: 'Playfair Display'; - font-weight: 500; - src: url('/fonts/PlayfairDisplay-Medium.woff') format('woff'); - } - @font-face { - font-family: 'Playfair Display'; - font-weight: 600; - src: url('/fonts/PlayfairDisplay-SemiBold.woff') format('woff'); - } - @font-face { - font-family: 'Playfair Display'; - font-weight: 700; - src: url('/fonts/PlayfairDisplay-Bold.woff') format('woff'); - } -} - -@keyframes accordion-down { - from { - height: 0; - } - - to { - height: var(--bits-accordion-content-height); - } -} - -@keyframes accordion-up { - from { - height: var(--bits-accordion-content-height); - } - - to { - height: 0; - } -} - -@keyframes caret-blink { - 0%, - 70%, - 100% { - opacity: 1; - } - - 20%, - 50% { - opacity: 0; - } } .animate-fade-in { @@ -220,6 +162,7 @@ opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); @@ -230,6 +173,7 @@ 0% { transform: scale(1.3); } + 100% { transform: scale(1); } @@ -246,6 +190,7 @@ 40% { opacity: 0; } + 100% { opacity: 1; } diff --git a/frontend/src/lib/components/form/searchable-multi-select.svelte b/frontend/src/lib/components/form/searchable-multi-select.svelte index d2fb24f2..343ab32e 100644 --- a/frontend/src/lib/components/form/searchable-multi-select.svelte +++ b/frontend/src/lib/components/form/searchable-multi-select.svelte @@ -81,18 +81,20 @@ variant="outline" role="combobox" aria-expanded={open} - class="h-auto min-h-10 w-full justify-between" + class="h-auto min-h-10 w-full bg-input/30!" > -
- {#if selectedItems.length > 0} - {#each selectedLabels as label} - {label} - {/each} - {:else} - {m.select_items()} - {/if} +
+
+ {#if selectedItems.length > 0} + {#each selectedLabels as label} + {label} + {/each} + {:else} + {m.select_items()} + {/if} +
+
- {/snippet} diff --git a/frontend/src/lib/components/form/searchable-select.svelte b/frontend/src/lib/components/form/searchable-select.svelte index cfc2301f..1b9dcaa8 100644 --- a/frontend/src/lib/components/form/searchable-select.svelte +++ b/frontend/src/lib/components/form/searchable-select.svelte @@ -70,10 +70,12 @@ role="combobox" aria-expanded={open} {...props} - class={cn('justify-between', restProps.class)} + class={restProps.class} > - {items.find((item) => item.value === value)?.label || selectText} - + + {items.find((item) => item.value === value)?.label || selectText} + + {/snippet} diff --git a/frontend/src/lib/components/header/header.svelte b/frontend/src/lib/components/header/header.svelte index e3b0625a..94336270 100644 --- a/frontend/src/lib/components/header/header.svelte +++ b/frontend/src/lib/components/header/header.svelte @@ -1,8 +1,9 @@ -
+
{#if !isAuthPage} - + -

- {$appConfigStore.appName} + +

+ {m.settings()}

{/if} diff --git a/frontend/src/lib/components/passkey-row.svelte b/frontend/src/lib/components/passkey-row.svelte index 77dde69c..b5777040 100644 --- a/frontend/src/lib/components/passkey-row.svelte +++ b/frontend/src/lib/components/passkey-row.svelte @@ -23,7 +23,7 @@ - + {#if icon}{@const Icon = icon} {/if} diff --git a/frontend/src/lib/components/qrcode/qrcode.svelte b/frontend/src/lib/components/qrcode/qrcode.svelte index 9b4984a7..868525ad 100644 --- a/frontend/src/lib/components/qrcode/qrcode.svelte +++ b/frontend/src/lib/components/qrcode/qrcode.svelte @@ -9,7 +9,7 @@ value, size = 200, color = '#000000', - backgroundColor = '#FFFFFF', + backgroundColor = 'transparent', ...restProps }: HTMLAttributes & { value: string | null; @@ -39,4 +39,4 @@ }); - + diff --git a/frontend/src/lib/components/signup/signup-token-modal.svelte b/frontend/src/lib/components/signup/signup-token-modal.svelte index 3456f09c..ab1f79fe 100644 --- a/frontend/src/lib/components/signup/signup-token-modal.svelte +++ b/frontend/src/lib/components/signup/signup-token-modal.svelte @@ -187,7 +187,6 @@ value={signupLink} size={180} color={mode.current === 'dark' ? '#FFFFFF' : '#000000'} - backgroundColor={mode.current === 'dark' ? '#000000' : '#FFFFFF'} />

diff --git a/frontend/src/lib/components/table/advanced-table-column-selection.svelte b/frontend/src/lib/components/table/advanced-table-column-selection.svelte index 0fc5d329..c0839476 100644 --- a/frontend/src/lib/components/table/advanced-table-column-selection.svelte +++ b/frontend/src/lib/components/table/advanced-table-column-selection.svelte @@ -16,7 +16,7 @@ class={buttonVariants({ variant: 'outline', size: 'sm', - class: 'ml-auto h-8' + class: 'ml-auto' })} > diff --git a/frontend/src/lib/components/table/advanced-table-filter.svelte b/frontend/src/lib/components/table/advanced-table-filter.svelte index 4e1b5609..57d6c23c 100644 --- a/frontend/src/lib/components/table/advanced-table-filter.svelte +++ b/frontend/src/lib/components/table/advanced-table-filter.svelte @@ -38,11 +38,13 @@ {...props} variant="outline" size="sm" - class="h-8 border-dashed" + class="order-dashed" data-testid={`facet-${title.toLowerCase()}-trigger`} > - - {title} + + + {title} + {#if selectedValues.size > 0} diff --git a/frontend/src/lib/components/table/advanced-table.svelte b/frontend/src/lib/components/table/advanced-table.svelte index d1625250..c30b03a0 100644 --- a/frontend/src/lib/components/table/advanced-table.svelte +++ b/frontend/src/lib/components/table/advanced-table.svelte @@ -243,7 +243,7 @@ class="h-12 w-full justify-start px-4 font-medium hover:bg-transparent" onclick={() => onSort(column.column)} > - + {column.label} action.onClick(item)} disabled={action.disabled} - class={action.variant === 'danger' - ? 'text-red-500 focus:!text-red-700' - : ''} + class={action.variant === 'danger' ? 'text-red-500!' : ''} > {#if action.icon} {@const Icon = action.icon} diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte index 1f849e51..946a1870 100644 --- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte +++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte @@ -1,18 +1,27 @@ diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte index dac534d5..f3089db6 100644 --- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte +++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte @@ -1,18 +1,27 @@ diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte index a9808dae..9d3e9585 100644 --- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte +++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte @@ -1,27 +1,32 @@ - + - + diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte index 4622dec1..3d6ede31 100644 --- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte +++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte @@ -12,6 +12,9 @@ diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte index 8a7c1611..09d285f5 100644 --- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte +++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte @@ -13,7 +13,10 @@

{@render children?.()} diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte index 484c948b..419a434b 100644 --- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte +++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte @@ -13,7 +13,10 @@
{@render children?.()} diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-media.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-media.svelte new file mode 100644 index 00000000..ec2baca5 --- /dev/null +++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-media.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte index 4e1d6fe8..5e974556 100644 --- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte +++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte @@ -13,7 +13,7 @@ bind:ref data-slot="alert-dialog-overlay" class={cn( - 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50', + 'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 z-50', className )} {...restProps} diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte new file mode 100644 index 00000000..dd3ac7d9 --- /dev/null +++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte index 0bc78202..f8c458ea 100644 --- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte +++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte @@ -12,6 +12,9 @@ diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog.svelte new file mode 100644 index 00000000..cde7911a --- /dev/null +++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog.svelte @@ -0,0 +1,7 @@ + + + diff --git a/frontend/src/lib/components/ui/alert-dialog/index.ts b/frontend/src/lib/components/ui/alert-dialog/index.ts index bc8e9709..260a5441 100644 --- a/frontend/src/lib/components/ui/alert-dialog/index.ts +++ b/frontend/src/lib/components/ui/alert-dialog/index.ts @@ -1,4 +1,5 @@ -import { AlertDialog as AlertDialogPrimitive } from 'bits-ui'; +import Root from './alert-dialog.svelte'; +import Portal from './alert-dialog-portal.svelte'; import Trigger from './alert-dialog-trigger.svelte'; import Title from './alert-dialog-title.svelte'; import Action from './alert-dialog-action.svelte'; @@ -8,29 +9,32 @@ import Header from './alert-dialog-header.svelte'; import Overlay from './alert-dialog-overlay.svelte'; import Content from './alert-dialog-content.svelte'; import Description from './alert-dialog-description.svelte'; - -const Root = AlertDialogPrimitive.Root; +import Media from './alert-dialog-media.svelte'; export { Root, Title, Action, Cancel, + Portal, Footer, Header, Trigger, Overlay, Content, Description, + Media, // Root as AlertDialog, Title as AlertDialogTitle, Action as AlertDialogAction, Cancel as AlertDialogCancel, + Portal as AlertDialogPortal, Footer as AlertDialogFooter, Header as AlertDialogHeader, Trigger as AlertDialogTrigger, Overlay as AlertDialogOverlay, Content as AlertDialogContent, - Description as AlertDialogDescription + Description as AlertDialogDescription, + Media as AlertDialogMedia }; diff --git a/frontend/src/lib/components/ui/alert/alert.svelte b/frontend/src/lib/components/ui/alert/alert.svelte index 09e6d8ac..c09ed4ac 100644 --- a/frontend/src/lib/components/ui/alert/alert.svelte +++ b/frontend/src/lib/components/ui/alert/alert.svelte @@ -2,7 +2,7 @@ import { tv, type VariantProps } from 'tailwind-variants'; export const alertVariants = tv({ - base: 'relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current', + base: 'relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-3xl border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current', variants: { variant: { default: 'bg-card text-card-foreground', diff --git a/frontend/src/lib/components/ui/badge/badge.svelte b/frontend/src/lib/components/ui/badge/badge.svelte index d9e34464..1a3c5b5c 100644 --- a/frontend/src/lib/components/ui/badge/badge.svelte +++ b/frontend/src/lib/components/ui/badge/badge.svelte @@ -2,15 +2,16 @@ import { type VariantProps, tv } from 'tailwind-variants'; export const badgeVariants = tv({ - base: 'focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-md border px-2 py-0.5 text-xs font-medium transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3', + base: 'h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive group/badge inline-flex w-fit shrink-0 items-center justify-center overflow-hidden whitespace-nowrap transition-colors focus-visible:ring-[3px] [&>svg]:pointer-events-none', variants: { variant: { - default: 'bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent', - secondary: - 'bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent', + default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80', + secondary: 'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80', destructive: - 'bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white', - outline: 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground' + 'bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20', + outline: 'border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground', + ghost: 'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50', + link: 'text-primary underline-offset-4 hover:underline' } }, defaultVariants: { diff --git a/frontend/src/lib/components/ui/button/button.svelte b/frontend/src/lib/components/ui/button/button.svelte index ae785f3d..2e640acd 100644 --- a/frontend/src/lib/components/ui/button/button.svelte +++ b/frontend/src/lib/components/ui/button/button.svelte @@ -4,22 +4,24 @@ import { tv, type VariantProps } from 'tailwind-variants'; export const buttonVariants = tv({ - base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", + base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-full text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:shrink-0 transition active:scale-95", variants: { variant: { - default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90', - destructive: - 'bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white', + default: 'bg-primary text-primary-foreground hover:bg-primary/80', outline: - 'bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border', - secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80', - ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', + 'border border-border bg-background hover:bg-muted hover:text-foreground dark:hover:bg-input/30 aria-expanded:bg-muted aria-expanded:text-foreground dark:bg-transparent', + secondary: + 'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground', + ghost: + 'hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground', + destructive: + 'bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30', link: 'text-primary underline-offset-4 hover:underline' }, size: { default: 'h-10 px-4 py-2', - sm: 'h-8 rounded-md px-3', - lg: 'h-11 rounded-md px-8', + sm: 'h-9 px-3 py-2', + lg: 'h-11 px-8', icon: 'h-10 w-10' } }, @@ -96,9 +98,6 @@ tabindex={disabled ? -1 : undefined} {...restProps} > - {#if isLoading} - - {/if} {@render children?.()} {:else} @@ -111,9 +110,15 @@ onclick={handleOnClick} {...restProps} > - {#if isLoading} - - {/if} - {@render children?.()} + + + {@render children?.()} + {/if} diff --git a/frontend/src/lib/components/ui/card/card.svelte b/frontend/src/lib/components/ui/card/card.svelte index 23f876d6..536744ca 100644 --- a/frontend/src/lib/components/ui/card/card.svelte +++ b/frontend/src/lib/components/ui/card/card.svelte @@ -14,7 +14,7 @@ bind:this={ref} data-slot="card" class={cn( - 'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm', + 'bg-card text-card-foreground flex flex-col gap-6 rounded-4xl border py-6 shadow-sm', className )} {...restProps} diff --git a/frontend/src/lib/components/ui/dialog/dialog-content.svelte b/frontend/src/lib/components/ui/dialog/dialog-content.svelte index b4aba473..4dce737b 100644 --- a/frontend/src/lib/components/ui/dialog/dialog-content.svelte +++ b/frontend/src/lib/components/ui/dialog/dialog-content.svelte @@ -23,14 +23,14 @@ bind:ref data-slot="dialog-content" class={cn( - 'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg', + 'bg-popover data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-4xl border p-6 shadow-lg duration-200 sm:max-w-lg', className )} {...restProps} > {@render children?.()} Close diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte new file mode 100644 index 00000000..c0bf94b5 --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte @@ -0,0 +1,16 @@ + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte index 74e21bb8..8ff36900 100644 --- a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte @@ -1,7 +1,7 @@ - + - + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte index 231aee49..5115173e 100644 --- a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte @@ -17,6 +17,6 @@ bind:ref data-slot="dropdown-menu-group-heading" data-inset={inset} - class={cn('px-2 py-1.5 text-sm font-semibold data-[inset]:pl-8', className)} + class={cn('px-2 py-1.5 text-sm font-semibold data-[inset]:ps-8', className)} {...restProps} /> diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte index 204a67d0..405fbe2e 100644 --- a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte @@ -20,7 +20,7 @@ data-inset={inset} data-variant={variant} class={cn( - "data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:data-highlighted:bg-destructive/10 dark:data-[variant=destructive]:data-highlighted:bg-destructive/20 data-[variant=destructive]:data-highlighted:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground gap-1.5 rounded-md px-2 py-1.5 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 group/dropdown-menu-item relative flex cursor-pointer items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0", className )} {...restProps} diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte index fe2ef4a7..ed35bc77 100644 --- a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte @@ -17,7 +17,10 @@ bind:this={ref} data-slot="dropdown-menu-label" data-inset={inset} - class={cn('px-2 py-1.5 text-sm font-semibold data-[inset]:pl-8', className)} + class={cn( + 'text-muted-foreground px-1.5 py-1 text-xs font-medium data-inset:pl-7 data-[inset]:pl-8', + className + )} {...restProps} > {@render children?.()} diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte new file mode 100644 index 00000000..089a25cc --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte index d4171b97..4c06cd7f 100644 --- a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte @@ -1,6 +1,6 @@ + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte new file mode 100644 index 00000000..fbbb7908 --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte @@ -0,0 +1,7 @@ + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/index.ts b/frontend/src/lib/components/ui/dropdown-menu/index.ts index aeb398e0..b097e3fd 100644 --- a/frontend/src/lib/components/ui/dropdown-menu/index.ts +++ b/frontend/src/lib/components/ui/dropdown-menu/index.ts @@ -1,4 +1,6 @@ -import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'; +import Root from './dropdown-menu.svelte'; +import Sub from './dropdown-menu-sub.svelte'; +import CheckboxGroup from './dropdown-menu-checkbox-group.svelte'; import CheckboxItem from './dropdown-menu-checkbox-item.svelte'; import Content from './dropdown-menu-content.svelte'; import Group from './dropdown-menu-group.svelte'; @@ -12,15 +14,18 @@ import Trigger from './dropdown-menu-trigger.svelte'; import SubContent from './dropdown-menu-sub-content.svelte'; import SubTrigger from './dropdown-menu-sub-trigger.svelte'; import GroupHeading from './dropdown-menu-group-heading.svelte'; -const Sub = DropdownMenuPrimitive.Sub; -const Root = DropdownMenuPrimitive.Root; +import Portal from './dropdown-menu-portal.svelte'; export { + CheckboxGroup, CheckboxItem, Content, + Portal, Root as DropdownMenu, + CheckboxGroup as DropdownMenuCheckboxGroup, CheckboxItem as DropdownMenuCheckboxItem, Content as DropdownMenuContent, + Portal as DropdownMenuPortal, Group as DropdownMenuGroup, Item as DropdownMenuItem, Label as DropdownMenuLabel, diff --git a/frontend/src/lib/components/ui/input/input.svelte b/frontend/src/lib/components/ui/input/input.svelte index 8f865f96..e92db9e7 100644 --- a/frontend/src/lib/components/ui/input/input.svelte +++ b/frontend/src/lib/components/ui/input/input.svelte @@ -24,7 +24,7 @@ bind:this={ref} data-slot="input" class={cn( - 'selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-2 text-base font-medium shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm', + 'selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-full border bg-transparent px-3 py-2 text-base font-medium shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm', 'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]', 'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive', className @@ -39,7 +39,7 @@ bind:this={ref} data-slot="input" class={cn( - 'border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm', + 'border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-full border px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm', 'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]', 'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive', className diff --git a/frontend/src/lib/components/ui/item/item.svelte b/frontend/src/lib/components/ui/item/item.svelte index 4839643e..8f0f4e76 100644 --- a/frontend/src/lib/components/ui/item/item.svelte +++ b/frontend/src/lib/components/ui/item/item.svelte @@ -2,7 +2,7 @@ import { tv, type VariantProps } from 'tailwind-variants'; export const itemVariants = tv({ - base: 'group/item focus-visible:border-ring focus-visible:ring-ring/50 flex flex-wrap items-center rounded-xl border border-transparent text-sm outline-none transition-colors duration-100 focus-visible:ring-[3px]', + base: 'group/item focus-visible:border-ring focus-visible:ring-ring/50 flex flex-wrap items-center rounded-4xl border border-transparent text-sm outline-none transition-colors duration-100 focus-visible:ring-[3px]', variants: { variant: { default: 'bg-transparent [a&]:hover:bg-accent/50 [a&]:transition-colors', @@ -12,8 +12,8 @@ transparent: 'bg-transparent' }, size: { - default: 'gap-4 p-4', - sm: 'gap-2.5 px-4 py-3' + default: 'gap-4 p-6', + sm: 'gap-2.5 px-4 py-4' } }, defaultVariants: { diff --git a/frontend/src/lib/components/ui/popover/index.ts b/frontend/src/lib/components/ui/popover/index.ts index 0dcc3ccb..7ca18f4c 100644 --- a/frontend/src/lib/components/ui/popover/index.ts +++ b/frontend/src/lib/components/ui/popover/index.ts @@ -1,17 +1,28 @@ -import { Popover as PopoverPrimitive } from 'bits-ui'; +import Root from './popover.svelte'; +import Close from './popover-close.svelte'; import Content from './popover-content.svelte'; +import Description from './popover-description.svelte'; +import Header from './popover-header.svelte'; +import Title from './popover-title.svelte'; import Trigger from './popover-trigger.svelte'; -const Root = PopoverPrimitive.Root; -const Close = PopoverPrimitive.Close; +import Portal from './popover-portal.svelte'; export { Root, Content, + Description, + Header, + Title, Trigger, Close, + Portal, // Root as Popover, Content as PopoverContent, + Description as PopoverDescription, + Header as PopoverHeader, + Title as PopoverTitle, Trigger as PopoverTrigger, - Close as PopoverClose + Close as PopoverClose, + Portal as PopoverPortal }; diff --git a/frontend/src/lib/components/ui/popover/popover-close.svelte b/frontend/src/lib/components/ui/popover/popover-close.svelte new file mode 100644 index 00000000..dc4dec4b --- /dev/null +++ b/frontend/src/lib/components/ui/popover/popover-close.svelte @@ -0,0 +1,7 @@ + + + diff --git a/frontend/src/lib/components/ui/popover/popover-content.svelte b/frontend/src/lib/components/ui/popover/popover-content.svelte index 95e209a5..58ef2c5d 100644 --- a/frontend/src/lib/components/ui/popover/popover-content.svelte +++ b/frontend/src/lib/components/ui/popover/popover-content.svelte @@ -1,6 +1,8 @@ - + - + diff --git a/frontend/src/lib/components/ui/popover/popover-description.svelte b/frontend/src/lib/components/ui/popover/popover-description.svelte new file mode 100644 index 00000000..a560005f --- /dev/null +++ b/frontend/src/lib/components/ui/popover/popover-description.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/frontend/src/lib/components/ui/popover/popover-header.svelte b/frontend/src/lib/components/ui/popover/popover-header.svelte new file mode 100644 index 00000000..b64b0906 --- /dev/null +++ b/frontend/src/lib/components/ui/popover/popover-header.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/frontend/src/lib/components/ui/popover/popover-portal.svelte b/frontend/src/lib/components/ui/popover/popover-portal.svelte new file mode 100644 index 00000000..25efb877 --- /dev/null +++ b/frontend/src/lib/components/ui/popover/popover-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/frontend/src/lib/components/ui/popover/popover-title.svelte b/frontend/src/lib/components/ui/popover/popover-title.svelte new file mode 100644 index 00000000..1ca16a96 --- /dev/null +++ b/frontend/src/lib/components/ui/popover/popover-title.svelte @@ -0,0 +1,15 @@ + + +
+ {@render children?.()} +
diff --git a/frontend/src/lib/components/ui/popover/popover.svelte b/frontend/src/lib/components/ui/popover/popover.svelte new file mode 100644 index 00000000..f39b867a --- /dev/null +++ b/frontend/src/lib/components/ui/popover/popover.svelte @@ -0,0 +1,7 @@ + + + diff --git a/frontend/src/lib/components/ui/select/index.ts b/frontend/src/lib/components/ui/select/index.ts index e6acdf8e..305c47ad 100644 --- a/frontend/src/lib/components/ui/select/index.ts +++ b/frontend/src/lib/components/ui/select/index.ts @@ -1,5 +1,4 @@ -import { Select as SelectPrimitive } from 'bits-ui'; - +import Root from './select.svelte'; import Group from './select-group.svelte'; import Label from './select-label.svelte'; import Item from './select-item.svelte'; @@ -8,8 +7,8 @@ import Trigger from './select-trigger.svelte'; import Separator from './select-separator.svelte'; import ScrollDownButton from './select-scroll-down-button.svelte'; import ScrollUpButton from './select-scroll-up-button.svelte'; - -const Root = SelectPrimitive.Root; +import GroupHeading from './select-group-heading.svelte'; +import Portal from './select-portal.svelte'; export { Root, @@ -21,6 +20,8 @@ export { Separator, ScrollDownButton, ScrollUpButton, + GroupHeading, + Portal, // Root as Select, Group as SelectGroup, @@ -30,5 +31,7 @@ export { Trigger as SelectTrigger, Separator as SelectSeparator, ScrollDownButton as SelectScrollDownButton, - ScrollUpButton as SelectScrollUpButton + ScrollUpButton as SelectScrollUpButton, + GroupHeading as SelectGroupHeading, + Portal as SelectPortal }; diff --git a/frontend/src/lib/components/ui/select/select-content.svelte b/frontend/src/lib/components/ui/select/select-content.svelte index 9450e3be..edb50d06 100644 --- a/frontend/src/lib/components/ui/select/select-content.svelte +++ b/frontend/src/lib/components/ui/select/select-content.svelte @@ -1,8 +1,11 @@ - + {@render children?.()} - + diff --git a/frontend/src/lib/components/ui/select/select-group-heading.svelte b/frontend/src/lib/components/ui/select/select-group-heading.svelte new file mode 100644 index 00000000..8a5aa3b1 --- /dev/null +++ b/frontend/src/lib/components/ui/select/select-group-heading.svelte @@ -0,0 +1,21 @@ + + + + {@render children?.()} + diff --git a/frontend/src/lib/components/ui/select/select-group.svelte b/frontend/src/lib/components/ui/select/select-group.svelte index 2520795d..bb26231a 100644 --- a/frontend/src/lib/components/ui/select/select-group.svelte +++ b/frontend/src/lib/components/ui/select/select-group.svelte @@ -1,7 +1,17 @@ - + diff --git a/frontend/src/lib/components/ui/select/select-item.svelte b/frontend/src/lib/components/ui/select/select-item.svelte index 4bf38167..f40cb30b 100644 --- a/frontend/src/lib/components/ui/select/select-item.svelte +++ b/frontend/src/lib/components/ui/select/select-item.svelte @@ -1,7 +1,7 @@ + + diff --git a/frontend/src/lib/components/ui/select/select-scroll-down-button.svelte b/frontend/src/lib/components/ui/select/select-scroll-down-button.svelte index 7ede728a..a9d95bb9 100644 --- a/frontend/src/lib/components/ui/select/select-scroll-down-button.svelte +++ b/frontend/src/lib/components/ui/select/select-scroll-down-button.svelte @@ -1,7 +1,7 @@ + + diff --git a/frontend/src/lib/components/ui/separator/separator.svelte b/frontend/src/lib/components/ui/separator/separator.svelte index 9e2570b2..f2a2ef2d 100644 --- a/frontend/src/lib/components/ui/separator/separator.svelte +++ b/frontend/src/lib/components/ui/separator/separator.svelte @@ -5,15 +5,18 @@ let { ref = $bindable(null), class: className, + 'data-slot': dataSlot = 'separator', ...restProps }: SeparatorPrimitive.RootProps = $props(); diff --git a/frontend/src/lib/components/ui/tabs/index.ts b/frontend/src/lib/components/ui/tabs/index.ts index d2a7939e..2ea36233 100644 --- a/frontend/src/lib/components/ui/tabs/index.ts +++ b/frontend/src/lib/components/ui/tabs/index.ts @@ -1,6 +1,6 @@ import Root from './tabs.svelte'; import Content from './tabs-content.svelte'; -import List from './tabs-list.svelte'; +import List, { tabsListVariants, type TabsListVariant } from './tabs-list.svelte'; import Trigger from './tabs-trigger.svelte'; export { @@ -8,6 +8,8 @@ export { Content, List, Trigger, + tabsListVariants, + type TabsListVariant, // Root as Tabs, Content as TabsContent, diff --git a/frontend/src/lib/components/ui/tabs/tabs-content.svelte b/frontend/src/lib/components/ui/tabs/tabs-content.svelte index 35e44f66..07ab3308 100644 --- a/frontend/src/lib/components/ui/tabs/tabs-content.svelte +++ b/frontend/src/lib/components/ui/tabs/tabs-content.svelte @@ -12,6 +12,6 @@ diff --git a/frontend/src/lib/components/ui/tabs/tabs-list.svelte b/frontend/src/lib/components/ui/tabs/tabs-list.svelte index fffdc1f8..24177222 100644 --- a/frontend/src/lib/components/ui/tabs/tabs-list.svelte +++ b/frontend/src/lib/components/ui/tabs/tabs-list.svelte @@ -1,16 +1,40 @@ - + + diff --git a/frontend/src/lib/components/ui/tabs/tabs-trigger.svelte b/frontend/src/lib/components/ui/tabs/tabs-trigger.svelte index 3766b0ad..c0a8cd18 100644 --- a/frontend/src/lib/components/ui/tabs/tabs-trigger.svelte +++ b/frontend/src/lib/components/ui/tabs/tabs-trigger.svelte @@ -1,6 +1,6 @@ - + - + diff --git a/frontend/src/lib/components/ui/tooltip/tooltip-portal.svelte b/frontend/src/lib/components/ui/tooltip/tooltip-portal.svelte new file mode 100644 index 00000000..7d328f01 --- /dev/null +++ b/frontend/src/lib/components/ui/tooltip/tooltip-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/frontend/src/lib/components/ui/tooltip/tooltip-provider.svelte b/frontend/src/lib/components/ui/tooltip/tooltip-provider.svelte new file mode 100644 index 00000000..fde5bb28 --- /dev/null +++ b/frontend/src/lib/components/ui/tooltip/tooltip-provider.svelte @@ -0,0 +1,7 @@ + + + diff --git a/frontend/src/lib/components/ui/tooltip/tooltip.svelte b/frontend/src/lib/components/ui/tooltip/tooltip.svelte new file mode 100644 index 00000000..7c9d7125 --- /dev/null +++ b/frontend/src/lib/components/ui/tooltip/tooltip.svelte @@ -0,0 +1,7 @@ + + + diff --git a/frontend/src/lib/components/web-authn-unsupported.svelte b/frontend/src/lib/components/web-authn-unsupported.svelte index 5bdc97d1..bec0709d 100644 --- a/frontend/src/lib/components/web-authn-unsupported.svelte +++ b/frontend/src/lib/components/web-authn-unsupported.svelte @@ -7,7 +7,7 @@
-

{m.browser_unsupported()}

+

{m.browser_unsupported()}

{m.this_browser_does_not_support_passkeys()}

diff --git a/frontend/src/routes/authorize/+page.svelte b/frontend/src/routes/authorize/+page.svelte index 4909541e..af79cd69 100644 --- a/frontend/src/routes/authorize/+page.svelte +++ b/frontend/src/routes/authorize/+page.svelte @@ -180,11 +180,10 @@ throw new Error('Invalid redirect URL protocol'); } - redirectURL.searchParams.append('error', error); - if (authorizeState) { - redirectURL.searchParams.append('state', authorizeState); - } - window.location.href = redirectURL.toString(); + window.location.href = createRedirectURL(callbackURL, { + error, + state: authorizeState + }); } function onSuccess(code: string, callbackURL: string, issuer: string) { @@ -193,10 +192,6 @@ throw new Error('Invalid redirect URL protocol'); } - redirectURL.searchParams.append('code', code); - redirectURL.searchParams.append('state', authorizeState); - redirectURL.searchParams.append('iss', issuer); - success = true; setTimeout(() => { if (responseMode === 'form_post') { @@ -231,16 +226,34 @@ document.body.appendChild(form); form.submit(); } else { - // Default query parameter redirect (response_mode=query or not specified) - const redirectURL = new URL(callbackURL); - redirectURL.searchParams.append('code', code); - redirectURL.searchParams.append('state', authorizeState); - redirectURL.searchParams.append('iss', issuer); - - window.location.href = redirectURL.toString(); + window.location.href = createRedirectURL(callbackURL, { + code, + state: authorizeState, + iss: issuer + }); } }, 1000); } + + function createRedirectURL(url: string, params: Record) { + const redirectURL = new URL(url); + const responseParams = + responseMode === 'fragment' + ? new URLSearchParams(redirectURL.hash.slice(1)) + : redirectURL.searchParams; + + for (const [key, value] of Object.entries(params)) { + if (value) { + responseParams.set(key, value); + } + } + + if (responseMode === 'fragment') { + redirectURL.hash = responseParams.toString(); + } + + return redirectURL.toString(); + } @@ -252,7 +265,7 @@ {:else} -

+

{m.sign_in_to({ name: client.name })}

{#if errorMessage} @@ -260,7 +273,22 @@ {errorMessage}.

{/if} - {#if accountSelectionRequired && $userStore && !errorMessage} + {#if authorizationRequired} +
+ + +

+ +

+
+ + + +
+
+ {:else if accountSelectionRequired && $userStore && !errorMessage}

@@ -301,21 +329,6 @@ })} />

- {:else if authorizationRequired} -
- - -

- -

-
- - - -
-
{/if}
diff --git a/frontend/src/routes/device/+page.svelte b/frontend/src/routes/device/+page.svelte index 72b955de..bdc6d377 100644 --- a/frontend/src/routes/device/+page.svelte +++ b/frontend/src/routes/device/+page.svelte @@ -79,7 +79,7 @@ {/if}
-

{m.authorize_device()}

+

{m.authorize_device()}

{#if errorMessage}

{errorMessage}. {m.please_try_again()} diff --git a/frontend/src/routes/login/+page.svelte b/frontend/src/routes/login/+page.svelte index 597cc791..0daedab0 100644 --- a/frontend/src/routes/login/+page.svelte +++ b/frontend/src/routes/login/+page.svelte @@ -44,7 +44,7 @@

-

+

{m.sign_in_to_appname({ appName: $appConfigStore.appName })}

{#if error} diff --git a/frontend/src/routes/login/alternative/+page.svelte b/frontend/src/routes/login/alternative/+page.svelte index 033db476..704dc3a3 100644 --- a/frontend/src/routes/login/alternative/+page.svelte +++ b/frontend/src/routes/login/alternative/+page.svelte @@ -35,7 +35,7 @@
-

{m.alternative_sign_in()}

+

{m.alternative_sign_in()}

{m.if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods()}

diff --git a/frontend/src/routes/login/alternative/code/+page.svelte b/frontend/src/routes/login/alternative/code/+page.svelte index aa3236e9..59e4bfc9 100644 --- a/frontend/src/routes/login/alternative/code/+page.svelte +++ b/frontend/src/routes/login/alternative/code/+page.svelte @@ -59,7 +59,7 @@
-

{m.login_code()}

+

{m.login_code()}

{#if error}

{error}. {m.please_try_again()} diff --git a/frontend/src/routes/login/alternative/email/+page.svelte b/frontend/src/routes/login/alternative/email/+page.svelte index 3cef4135..e1718fcd 100644 --- a/frontend/src/routes/login/alternative/email/+page.svelte +++ b/frontend/src/routes/login/alternative/email/+page.svelte @@ -5,9 +5,9 @@ import Input from '$lib/components/ui/input/input.svelte'; import { m } from '$lib/paraglide/messages'; import UserService from '$lib/services/user-service'; + import { preventDefault } from '$lib/utils/event-util'; import { fade } from 'svelte/transition'; import LoginLogoErrorSuccessIndicator from '../../components/login-logo-error-success-indicator.svelte'; - import { preventDefault } from '$lib/utils/event-util'; const { data } = $props(); @@ -37,7 +37,7 @@

-

{m.email_login()}

+

{m.email_login()}

{#if error}

{error}. {m.please_try_again()} diff --git a/frontend/src/routes/logout/+page.svelte b/frontend/src/routes/logout/+page.svelte index d8792831..b95d2c0b 100644 --- a/frontend/src/routes/logout/+page.svelte +++ b/frontend/src/routes/logout/+page.svelte @@ -34,7 +34,7 @@

-

{m.sign_out()}

+

{m.sign_out()}

-
-

- - {m.settings()} -

-
-
-
+
{@render children()} diff --git a/frontend/src/routes/settings/account/+page.svelte b/frontend/src/routes/settings/account/+page.svelte index 4c3b91a0..6889a998 100644 --- a/frontend/src/routes/settings/account/+page.svelte +++ b/frontend/src/routes/settings/account/+page.svelte @@ -141,7 +141,7 @@ - + diff --git a/frontend/src/routes/settings/account/login-code-modal.svelte b/frontend/src/routes/settings/account/login-code-modal.svelte index 709a047a..20437492 100644 --- a/frontend/src/routes/settings/account/login-code-modal.svelte +++ b/frontend/src/routes/settings/account/login-code-modal.svelte @@ -64,7 +64,6 @@ value={loginCodeLink} size={150} color={mode.current === 'dark' ? '#FFFFFF' : '#000000'} - backgroundColor={mode.current === 'dark' ? '#000000' : '#FFFFFF'} />

{loginCodeLink!}

diff --git a/frontend/src/routes/settings/admin/oidc-clients/oidc-client-preview-modal.svelte b/frontend/src/routes/settings/admin/oidc-clients/oidc-client-preview-modal.svelte index f888a281..b3bdc1c3 100644 --- a/frontend/src/routes/settings/admin/oidc-clients/oidc-client-preview-modal.svelte +++ b/frontend/src/routes/settings/admin/oidc-clients/oidc-client-preview-modal.svelte @@ -115,7 +115,7 @@
{/if} -
+
{m.users()} - + diff --git a/frontend/src/routes/signup/+page.svelte b/frontend/src/routes/signup/+page.svelte index 41747516..094fcc3b 100644 --- a/frontend/src/routes/signup/+page.svelte +++ b/frontend/src/routes/signup/+page.svelte @@ -61,7 +61,7 @@
-

+

{m.signup_to_appname({ appName: $appConfigStore.appName })}

diff --git a/frontend/src/routes/signup/add-passkey/+page.svelte b/frontend/src/routes/signup/add-passkey/+page.svelte index d30f7d43..f8369169 100644 --- a/frontend/src/routes/signup/add-passkey/+page.svelte +++ b/frontend/src/routes/signup/add-passkey/+page.svelte @@ -69,7 +69,7 @@
-

+

{m.setup_your_passkey()}

diff --git a/frontend/src/routes/signup/setup/+page.svelte b/frontend/src/routes/signup/setup/+page.svelte index 78c16739..fb9bc1d5 100644 --- a/frontend/src/routes/signup/setup/+page.svelte +++ b/frontend/src/routes/signup/setup/+page.svelte @@ -47,7 +47,7 @@

-

+

{m.signup_to_appname({ appName: $appConfigStore.appName })}

diff --git a/frontend/static/fonts/PlayfairDisplay-Bold.woff b/frontend/static/fonts/PlayfairDisplay-Bold.woff deleted file mode 100644 index 3e4f6da7..00000000 Binary files a/frontend/static/fonts/PlayfairDisplay-Bold.woff and /dev/null differ diff --git a/frontend/static/fonts/PlayfairDisplay-ExtraBold.woff b/frontend/static/fonts/PlayfairDisplay-ExtraBold.woff deleted file mode 100644 index a42fd641..00000000 Binary files a/frontend/static/fonts/PlayfairDisplay-ExtraBold.woff and /dev/null differ diff --git a/frontend/static/fonts/PlayfairDisplay-Medium.woff b/frontend/static/fonts/PlayfairDisplay-Medium.woff deleted file mode 100644 index 8c11b8d5..00000000 Binary files a/frontend/static/fonts/PlayfairDisplay-Medium.woff and /dev/null differ diff --git a/frontend/static/fonts/PlayfairDisplay-Regular.woff b/frontend/static/fonts/PlayfairDisplay-Regular.woff deleted file mode 100644 index 688623bc..00000000 Binary files a/frontend/static/fonts/PlayfairDisplay-Regular.woff and /dev/null differ diff --git a/frontend/static/fonts/PlayfairDisplay-SemiBold.woff b/frontend/static/fonts/PlayfairDisplay-SemiBold.woff deleted file mode 100644 index 76a30f32..00000000 Binary files a/frontend/static/fonts/PlayfairDisplay-SemiBold.woff and /dev/null differ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 244fc94a..9b565ee9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -61,6 +61,9 @@ importers: frontend: dependencies: + '@fontsource/gloock': + specifier: ^5.2.8 + version: 5.2.8 '@simplewebauthn/browser': specifier: ^13.3.0 version: 13.3.0 @@ -633,6 +636,9 @@ packages: '@floating-ui/utils@0.2.11': resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@fontsource/gloock@5.2.8': + resolution: {integrity: sha512-2qh1h3uC8bY93r2zuX3a0Q2IoGUf4vc1qxM0DUqoNVxwqDkXIOhGS7/0EEYNBLfSWO7ABfi/qkVmzedkj2jSZA==} + '@hapi/hoek@9.3.0': resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} @@ -3583,6 +3589,8 @@ snapshots: '@floating-ui/utils@0.2.11': {} + '@fontsource/gloock@5.2.8': {} + '@hapi/hoek@9.3.0': optional: true diff --git a/tests/resources/export/uploads/profile-pictures/defaults/CF.png b/tests/resources/export/uploads/profile-pictures/defaults/CF.png index 7c399f55..b608b8f7 100644 Binary files a/tests/resources/export/uploads/profile-pictures/defaults/CF.png and b/tests/resources/export/uploads/profile-pictures/defaults/CF.png differ diff --git a/tests/resources/export/uploads/profile-pictures/defaults/TC.png b/tests/resources/export/uploads/profile-pictures/defaults/TC.png index b8192304..316c10b3 100644 Binary files a/tests/resources/export/uploads/profile-pictures/defaults/TC.png and b/tests/resources/export/uploads/profile-pictures/defaults/TC.png differ diff --git a/tests/specs/application-configuration.spec.ts b/tests/specs/application-configuration.spec.ts index 570df386..73aa1179 100644 --- a/tests/specs/application-configuration.spec.ts +++ b/tests/specs/application-configuration.spec.ts @@ -18,14 +18,13 @@ test('Update general configuration', async ({ page }) => { await expect(page.locator('[data-type="success"]')).toHaveText( 'Application configuration updated successfully' ); - await expect(page.getByTestId('application-name')).toHaveText('Updated Name'); await page.reload(); await expect(page.getByLabel('Application Name', { exact: true })).toHaveValue('Updated Name'); await expect(page.getByLabel('Session Duration')).toHaveValue('30'); - await page.getByRole('link', { name: 'Logo Updated Name' }).click(); + await page.getByRole('link', { name: 'Logo' }).click(); await page.waitForURL('/settings/apps'); }); diff --git a/tests/specs/cli.spec.ts b/tests/specs/cli.spec.ts index 8b1733f8..0e6e7234 100644 --- a/tests/specs/cli.spec.ts +++ b/tests/specs/cli.spec.ts @@ -109,10 +109,16 @@ function compareExports(dir1: string, dir2: string): void { const hashes2 = hashAllFiles(dir2); const files1 = Object.keys(hashes1).sort(); - const files2 = Object.keys(hashes2).sort().filter(p => !p.includes('.inited')); + const files2 = Object.keys(hashes2) + .sort() + .filter((p) => !p.includes('.inited')); expect(files2).toEqual(files1); for (const file of files1) { + const profilePictures = ['CF.png', 'CR.png']; + if (profilePictures.includes(path.basename(file))) { + continue; // Skip profile pictures because they get generated on demand and can differ between exports + } expect(hashes2[file], `${file} hash should match`).toEqual(hashes1[file]); } diff --git a/tests/specs/oidc.spec.ts b/tests/specs/oidc.spec.ts index 825bc947..066ebfd8 100644 --- a/tests/specs/oidc.spec.ts +++ b/tests/specs/oidc.spec.ts @@ -15,36 +15,21 @@ test.beforeEach(async () => await cleanupBackend()); test('Authorize existing client', async ({ page }) => { const oidcClient = oidcClients.nextcloud; const urlParams = createUrlParams(oidcClient); - await page.goto(`/authorize?${urlParams.toString()}`); - - // Ignore DNS resolution error as the callback URL is not reachable - await page.waitForURL(oidcClient.callbackUrl).catch((e) => { - if ( - !e.message.includes('net::ERR_NAME_NOT_RESOLVED') && - !e.message.includes('net::ERR_CERT_AUTHORITY_INVALID') - ) { - throw e; - } - }); + await expectCallbackRedirect(page, oidcClient.callbackUrl, () => + page.goto(`/authorize?${urlParams.toString()}`) + ); }); test('Authorize existing client while not signed in', async ({ page }) => { const oidcClient = oidcClients.nextcloud; const urlParams = createUrlParams(oidcClient); await page.context().clearCookies(); - await page.goto(`/authorize?${urlParams.toString()}`); - await (await passkeyUtil.init(page)).addPasskey(); - await page.getByRole('button', { name: 'Sign in' }).click(); + await expectCallbackRedirect(page, oidcClient.callbackUrl, async () => { + await page.goto(`/authorize?${urlParams.toString()}`); - // Ignore DNS resolution error as the callback URL is not reachable - await page.waitForURL(oidcClient.callbackUrl).catch((e) => { - if ( - !e.message.includes('net::ERR_NAME_NOT_RESOLVED') && - !e.message.includes('net::ERR_CERT_AUTHORITY_INVALID') - ) { - throw e; - } + await (await passkeyUtil.init(page)).addPasskey(); + await page.getByRole('button', { name: 'Sign in' }).click(); }); }); @@ -56,17 +41,9 @@ test('Authorize new client', async ({ page }) => { await expect(page.getByTestId('scopes').getByRole('heading', { name: 'Email' })).toBeVisible(); await expect(page.getByTestId('scopes').getByRole('heading', { name: 'Profile' })).toBeVisible(); - await page.getByRole('button', { name: 'Sign in' }).click(); - - // Ignore DNS resolution error as the callback URL is not reachable - await page.waitForURL(oidcClient.callbackUrl).catch((e) => { - if ( - !e.message.includes('net::ERR_NAME_NOT_RESOLVED') && - !e.message.includes('net::ERR_CERT_AUTHORITY_INVALID') - ) { - throw e; - } - }); + await expectCallbackRedirect(page, oidcClient.callbackUrl, () => + page.getByRole('button', { name: 'Sign in' }).click() + ); }); test('Authorize new client while not signed in', async ({ page }) => { @@ -81,17 +58,9 @@ test('Authorize new client while not signed in', async ({ page }) => { await expect(page.getByTestId('scopes').getByRole('heading', { name: 'Email' })).toBeVisible(); await expect(page.getByTestId('scopes').getByRole('heading', { name: 'Profile' })).toBeVisible(); - await page.getByRole('button', { name: 'Sign in' }).click(); - - // Ignore DNS resolution error as the callback URL is not reachable - await page.waitForURL(oidcClient.callbackUrl).catch((e) => { - if ( - !e.message.includes('net::ERR_NAME_NOT_RESOLVED') && - !e.message.includes('net::ERR_CERT_AUTHORITY_INVALID') - ) { - throw e; - } - }); + await expectCallbackRedirect(page, oidcClient.callbackUrl, () => + page.getByRole('button', { name: 'Sign in' }).click() + ); }); test('Authorize new client fails with user group not allowed', async ({ page }) => { @@ -140,23 +109,11 @@ test('End session with id token hint redirects to callback URL', async ({ page } users.tim, client.id ); - let redirectedCorrectly = false; - await page - .goto( + await expectCallbackRedirect(page, client.logoutCallbackUrl, () => + page.goto( `/api/oidc/end-session?id_token_hint=${idToken}&post_logout_redirect_uri=${client.logoutCallbackUrl}` ) - .catch((e) => { - if ( - e.message.includes('net::ERR_NAME_NOT_RESOLVED') || - e.message.includes('net::ERR_CERT_AUTHORITY_INVALID') - ) { - redirectedCorrectly = true; - } else { - throw e; - } - }); - - expect(redirectedCorrectly).toBeTruthy(); + ); }); test('Successfully refresh tokens with valid refresh token', async ({ request }) => { @@ -681,17 +638,9 @@ test('Forces reauthentication when client requires it', async ({ page, request } await (await passkeyUtil.init(page)).addPasskey(); const urlParams = createUrlParams(oidcClients.nextcloud); - await page.goto(`/authorize?${urlParams.toString()}`); - - await expect(page.getByTestId('scopes')).not.toBeVisible(); - - await page.waitForURL(oidcClients.nextcloud.callbackUrl).catch((e) => { - if ( - !e.message.includes('net::ERR_NAME_NOT_RESOLVED') && - !e.message.includes('net::ERR_CERT_AUTHORITY_INVALID') - ) { - throw e; - } + await expectCallbackRedirect(page, oidcClients.nextcloud.callbackUrl, async () => { + await page.goto(`/authorize?${urlParams.toString()}`); + await expect(page.getByTestId('scopes')).not.toBeVisible(); }); expect(webauthnStartCalled).toBe(true); @@ -725,6 +674,22 @@ test('Authorize existing client with response_mode=form_post', async ({ page }) await expectFormPostRequest(formPostRequestPromise); }); +test('Authorize existing client with response_mode=fragment', async ({ page }) => { + const oidcClient = oidcClients.nextcloud; + const urlParams = createUrlParams(oidcClient); + urlParams.set('response_mode', 'fragment'); + + const redirectUrl = await expectCallbackRedirect(page, oidcClient.callbackUrl, () => + page.goto(`/authorize?${urlParams.toString()}`) + ); + expect(redirectUrl.search).toBe(''); + + const fragmentParams = new URLSearchParams(redirectUrl.hash.slice(1)); + expect(fragmentParams.get('code')).toBeTruthy(); + expect(fragmentParams.get('state')).toBe('nXx-6Qr-owc1SHBa'); + expect(fragmentParams.get('iss')).toBeTruthy(); +}); + function waitForFormPostRequest(page: Page, callbackUrl: string): Promise { return page.waitForRequest( (request) => request.method() === 'POST' && request.url() === callbackUrl @@ -758,6 +723,23 @@ test.describe('OIDC prompt parameter', () => { expect(redirectUrl.searchParams.get('state')).toBe('nXx-6Qr-owc1SHBa'); }); + test('prompt=none redirects errors with response_mode=fragment', async ({ page }) => { + await page.context().clearCookies(); + const oidcClient = oidcClients.nextcloud; + const urlParams = createUrlParams(oidcClient); + urlParams.set('prompt', 'none'); + urlParams.set('response_mode', 'fragment'); + + const redirectUrl = await expectCallbackRedirect(page, oidcClient.callbackUrl, () => + page.goto(`/authorize?${urlParams.toString()}`) + ); + expect(redirectUrl.search).toBe(''); + + const fragmentParams = new URLSearchParams(redirectUrl.hash.slice(1)); + expect(fragmentParams.get('error')).toBe('login_required'); + expect(fragmentParams.get('state')).toBe('nXx-6Qr-owc1SHBa'); + }); + test('prompt=none redirects with consent_required when authorization needed', async ({ page }) => { @@ -779,17 +761,9 @@ test.describe('OIDC prompt parameter', () => { const urlParams = createUrlParams(oidcClient); urlParams.set('prompt', 'none'); - await page.goto(`/authorize?${urlParams.toString()}`); - - // Should redirect successfully to callback URL with code - await page.waitForURL(oidcClient.callbackUrl).catch((e) => { - if ( - !e.message.includes('net::ERR_NAME_NOT_RESOLVED') && - !e.message.includes('net::ERR_CERT_AUTHORITY_INVALID') - ) { - throw e; - } - }); + await expectCallbackRedirect(page, oidcClient.callbackUrl, () => + page.goto(`/authorize?${urlParams.toString()}`) + ); }); test('prompt=consent forces consent display even for authorized client', async ({ page }) => { @@ -805,17 +779,9 @@ test.describe('OIDC prompt parameter', () => { ).toBeVisible(); await expect(page.getByTestId('scopes').getByRole('heading', { name: 'Email' })).toBeVisible(); - await page.getByRole('button', { name: 'Sign in' }).click(); - - // Should redirect successfully after consent - await page.waitForURL(oidcClient.callbackUrl).catch((e) => { - if ( - !e.message.includes('net::ERR_NAME_NOT_RESOLVED') && - !e.message.includes('net::ERR_CERT_AUTHORITY_INVALID') - ) { - throw e; - } - }); + await expectCallbackRedirect(page, oidcClient.callbackUrl, () => + page.getByRole('button', { name: 'Sign in' }).click() + ); }); test('prompt=login forces reauthentication', async ({ page }) => { @@ -830,17 +796,9 @@ test.describe('OIDC prompt parameter', () => { }); await (await passkeyUtil.init(page)).addPasskey(); - await page.goto(`/authorize?${urlParams.toString()}`); - - // Should require reauthentication even though user is signed in - await page.waitForURL(oidcClient.callbackUrl).catch((e) => { - if ( - !e.message.includes('net::ERR_NAME_NOT_RESOLVED') && - !e.message.includes('net::ERR_CERT_AUTHORITY_INVALID') - ) { - throw e; - } - }); + await expectCallbackRedirect(page, oidcClient.callbackUrl, () => + page.goto(`/authorize?${urlParams.toString()}`) + ); expect(reauthCalled).toBe(true); }); @@ -857,17 +815,9 @@ test.describe('OIDC prompt parameter', () => { await expect(selectionCard).toBeVisible(); await expect(selectionCard).toContainText('Tim Cook'); - await page.getByRole('button', { name: 'Sign In' }).click(); - - // Should redirect successfully to callback URL with code - await page.waitForURL(oidcClient.callbackUrl).catch((e) => { - if ( - !e.message.includes('net::ERR_NAME_NOT_RESOLVED') && - !e.message.includes('net::ERR_CERT_AUTHORITY_INVALID') - ) { - throw e; - } - }); + await expectCallbackRedirect(page, oidcClient.callbackUrl, () => + page.getByRole('button', { name: 'Sign In' }).click() + ); }); test('prompt=select_account account can be changed', async ({ page }) => { @@ -884,17 +834,9 @@ test.describe('OIDC prompt parameter', () => { await page.getByRole('button', { name: 'Sign In' }).click(); - await page.getByRole('button', { name: 'Sign In' }).click(); - - // Should redirect successfully to callback URL with code - await page.waitForURL(oidcClient.callbackUrl).catch((e) => { - if ( - !e.message.includes('net::ERR_NAME_NOT_RESOLVED') && - !e.message.includes('net::ERR_CERT_AUTHORITY_INVALID') - ) { - throw e; - } - }); + await expectCallbackRedirect(page, oidcClient.callbackUrl, () => + page.getByRole('button', { name: 'Sign In' }).click() + ); }); test('prompt=none with prompt=consent returns interaction_required', async ({ page }) => { @@ -939,3 +881,58 @@ test.describe('OIDC prompt parameter', () => { expect(redirectUrl.searchParams.get('state')).toBe('nXx-6Qr-owc1SHBa'); }); }); + +async function waitForCallbackURL(page: Page, callbackUrl: string): Promise { + const expectedUrl = new URL(callbackUrl); + const isCallbackURL = (url: URL) => + url.origin === expectedUrl.origin && url.pathname === expectedUrl.pathname; + + const callbackRequest = await page.waitForRequest((request) => + isCallbackURL(new URL(request.url())) + ); + await page.waitForURL(isCallbackURL, { waitUntil: 'commit' }).catch(() => {}); + + const currentURL = new URL(page.url()); + if (isCallbackURL(currentURL)) { + return currentURL; + } + + return new URL(callbackRequest.url()); +} + +async function expectCallbackRedirect( + page: Page, + callbackUrl: string, + action: () => Promise +): Promise { + const callbackRouteMatcher = await routeCallbackPage(page, callbackUrl); + + try { + const callbackURLPromise = waitForCallbackURL(page, callbackUrl); + const actionPromise = action().then( + () => undefined, + (error) => error + ); + const callbackURL = await callbackURLPromise; + await actionPromise; + return callbackURL; + } finally { + await page.unroute(callbackRouteMatcher); + } +} + +async function routeCallbackPage(page: Page, callbackUrl: string): Promise<(url: URL) => boolean> { + const expectedUrl = new URL(callbackUrl); + const callbackRouteMatcher = (url: URL) => + url.origin === expectedUrl.origin && url.pathname === expectedUrl.pathname; + + await page.route(callbackRouteMatcher, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'text/html', + body: 'OIDC callback' + }); + }); + + return callbackRouteMatcher; +}