mirror of
https://github.com/teamhanko/hanko.git
synced 2025-10-27 22:27:23 +08:00
fix: merge conflicts. remove import in quickstart
This commit is contained in:
@ -5,9 +5,9 @@ import (
|
||||
"fmt"
|
||||
"github.com/fatih/structs"
|
||||
"github.com/gobwas/glob"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
"github.com/knadh/koanf"
|
||||
"github.com/knadh/koanf/parsers/yaml"
|
||||
"github.com/knadh/koanf/providers/env"
|
||||
"github.com/knadh/koanf/providers/file"
|
||||
"github.com/sethvargo/go-limiter/httplimit"
|
||||
"log"
|
||||
@ -25,10 +25,10 @@ type Config struct {
|
||||
Secrets Secrets `yaml:"secrets" json:"secrets" koanf:"secrets"`
|
||||
Service Service `yaml:"service" json:"service" koanf:"service"`
|
||||
Session Session `yaml:"session" json:"session" koanf:"session"`
|
||||
AuditLog AuditLog `yaml:"audit_log" json:"audit_log" koanf:"audit_log"`
|
||||
AuditLog AuditLog `yaml:"audit_log" json:"audit_log" koanf:"audit_log" split_words:"true"`
|
||||
Emails Emails `yaml:"emails" json:"emails" koanf:"emails"`
|
||||
RateLimiter RateLimiter `yaml:"rate_limiter" json:"rate_limiter" koanf:"rate_limiter"`
|
||||
ThirdParty ThirdParty `yaml:"third_party" json:"third_party" koanf:"third_party"`
|
||||
RateLimiter RateLimiter `yaml:"rate_limiter" json:"rate_limiter" koanf:"rate_limiter" split_words:"true"`
|
||||
ThirdParty ThirdParty `yaml:"third_party" json:"third_party" koanf:"third_party" split_words:"true"`
|
||||
Log LoggerConfig `yaml:"log" json:"log" koanf:"log"`
|
||||
}
|
||||
|
||||
@ -44,19 +44,16 @@ func Load(cfgFile *string) (*Config, error) {
|
||||
log.Println("Using config file:", *cfgFile)
|
||||
}
|
||||
|
||||
err = k.Load(env.Provider("", ".", func(s string) string {
|
||||
return strings.Replace(strings.ToLower(s), "_", ".", -1)
|
||||
}), nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load config from env vars: %w", err)
|
||||
}
|
||||
|
||||
c := DefaultConfig()
|
||||
err = k.Unmarshal("", c)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
}
|
||||
|
||||
if err := envconfig.Process("", c); err != nil {
|
||||
return nil, fmt.Errorf("failed to load config from env vars: %w", err)
|
||||
}
|
||||
|
||||
err = c.PostProcess()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to post process config: %w", err)
|
||||
@ -211,13 +208,13 @@ func (s *Service) Validate() error {
|
||||
|
||||
type Password struct {
|
||||
Enabled bool `yaml:"enabled" json:"enabled" koanf:"enabled"`
|
||||
MinPasswordLength int `yaml:"min_password_length" json:"min_password_length" koanf:"min_password_length"`
|
||||
MinPasswordLength int `yaml:"min_password_length" json:"min_password_length" koanf:"min_password_length" split_words:"true"`
|
||||
}
|
||||
|
||||
type Cookie struct {
|
||||
Domain string `yaml:"domain" json:"domain" koanf:"domain"`
|
||||
HttpOnly bool `yaml:"http_only" json:"http_only" koanf:"http_only"`
|
||||
SameSite string `yaml:"same_site" json:"same_site" koanf:"same_site"`
|
||||
HttpOnly bool `yaml:"http_only" json:"http_only" koanf:"http_only" split_words:"true"`
|
||||
SameSite string `yaml:"same_site" json:"same_site" koanf:"same_site" split_words:"true"`
|
||||
Secure bool `yaml:"secure" json:"secure" koanf:"secure"`
|
||||
}
|
||||
|
||||
@ -230,12 +227,12 @@ type ServerSettings struct {
|
||||
|
||||
type Cors struct {
|
||||
Enabled bool `yaml:"enabled" json:"enabled" koanf:"enabled"`
|
||||
AllowCredentials bool `yaml:"allow_credentials" json:"allow_credentials" koanf:"allow_credentials"`
|
||||
AllowOrigins []string `yaml:"allow_origins" json:"allow_origins" koanf:"allow_origins"`
|
||||
AllowMethods []string `yaml:"allow_methods" json:"allow_methods" koanf:"allow_methods"`
|
||||
AllowHeaders []string `yaml:"allow_headers" json:"allow_headers" koanf:"allow_headers"`
|
||||
ExposeHeaders []string `yaml:"expose_headers" json:"expose_headers" koanf:"expose_headers"`
|
||||
MaxAge int `yaml:"max_age" json:"max_age" koanf:"max_age"`
|
||||
AllowCredentials bool `yaml:"allow_credentials" json:"allow_credentials" koanf:"allow_credentials" split_words:"true"`
|
||||
AllowOrigins []string `yaml:"allow_origins" json:"allow_origins" koanf:"allow_origins" split_words:"true"`
|
||||
AllowMethods []string `yaml:"allow_methods" json:"allow_methods" koanf:"allow_methods" split_words:"true"`
|
||||
AllowHeaders []string `yaml:"allow_headers" json:"allow_headers" koanf:"allow_headers" split_words:"true"`
|
||||
ExposeHeaders []string `yaml:"expose_headers" json:"expose_headers" koanf:"expose_headers" split_words:"true"`
|
||||
MaxAge int `yaml:"max_age" json:"max_age" koanf:"max_age" split_words:"true"`
|
||||
}
|
||||
|
||||
func (s *ServerSettings) Validate() error {
|
||||
@ -247,7 +244,7 @@ func (s *ServerSettings) Validate() error {
|
||||
|
||||
// WebauthnSettings defines the settings for the webauthn authentication mechanism
|
||||
type WebauthnSettings struct {
|
||||
RelyingParty RelyingParty `yaml:"relying_party" json:"relying_party" koanf:"relying_party"`
|
||||
RelyingParty RelyingParty `yaml:"relying_party" json:"relying_party" koanf:"relying_party" split_words:"true"`
|
||||
Timeout int `yaml:"timeout" json:"timeout" koanf:"timeout"`
|
||||
}
|
||||
|
||||
@ -259,7 +256,7 @@ func (r *WebauthnSettings) Validate() error {
|
||||
// RelyingParty webauthn settings for your application using hanko.
|
||||
type RelyingParty struct {
|
||||
Id string `yaml:"id" json:"id" koanf:"id"`
|
||||
DisplayName string `yaml:"display_name" json:"display_name" koanf:"display_name"`
|
||||
DisplayName string `yaml:"display_name" json:"display_name" koanf:"display_name" split_words:"true"`
|
||||
Icon string `yaml:"icon" json:"icon" koanf:"icon"`
|
||||
// Deprecated: Use Origins instead
|
||||
Origin string `yaml:"origin" json:"origin" koanf:"origin"`
|
||||
@ -285,8 +282,8 @@ func (s *SMTP) Validate() error {
|
||||
}
|
||||
|
||||
type Email struct {
|
||||
FromAddress string `yaml:"from_address" json:"from_address" koanf:"from_address"`
|
||||
FromName string `yaml:"from_name" json:"from_name" koanf:"from_name"`
|
||||
FromAddress string `yaml:"from_address" json:"from_address" koanf:"from_address" split_words:"true"`
|
||||
FromName string `yaml:"from_name" json:"from_name" koanf:"from_name" split_words:"true"`
|
||||
}
|
||||
|
||||
func (e *Email) Validate() error {
|
||||
@ -369,7 +366,7 @@ func (s *Secrets) Validate() error {
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
EnableAuthTokenHeader bool `yaml:"enable_auth_token_header" json:"enable_auth_token_header" koanf:"enable_auth_token_header"`
|
||||
EnableAuthTokenHeader bool `yaml:"enable_auth_token_header" json:"enable_auth_token_header" koanf:"enable_auth_token_header" split_words:"true"`
|
||||
Lifespan string `yaml:"lifespan" json:"lifespan" koanf:"lifespan"`
|
||||
Cookie Cookie `yaml:"cookie" json:"cookie" koanf:"cookie"`
|
||||
}
|
||||
@ -383,7 +380,7 @@ func (s *Session) Validate() error {
|
||||
}
|
||||
|
||||
type AuditLog struct {
|
||||
ConsoleOutput AuditLogConsole `yaml:"console_output" json:"console_output" koanf:"console_output"`
|
||||
ConsoleOutput AuditLogConsole `yaml:"console_output" json:"console_output" koanf:"console_output" split_words:"true"`
|
||||
Storage AuditLogStorage `yaml:"storage" json:"storage" koanf:"storage"`
|
||||
}
|
||||
|
||||
@ -393,12 +390,12 @@ type AuditLogStorage struct {
|
||||
|
||||
type AuditLogConsole struct {
|
||||
Enabled bool `yaml:"enabled" json:"enabled" koanf:"enabled"`
|
||||
OutputStream OutputStream `yaml:"output" json:"output" koanf:"output"`
|
||||
OutputStream OutputStream `yaml:"output" json:"output" koanf:"output" split_words:"true"`
|
||||
}
|
||||
|
||||
type Emails struct {
|
||||
RequireVerification bool `yaml:"require_verification" json:"require_verification" koanf:"require_verification"`
|
||||
MaxNumOfAddresses int `yaml:"max_num_of_addresses" json:"max_num_of_addresses" koanf:"max_num_of_addresses"`
|
||||
RequireVerification bool `yaml:"require_verification" json:"require_verification" koanf:"require_verification" split_words:"true"`
|
||||
MaxNumOfAddresses int `yaml:"max_num_of_addresses" json:"max_num_of_addresses" koanf:"max_num_of_addresses" split_words:"true"`
|
||||
}
|
||||
|
||||
type OutputStream string
|
||||
@ -412,8 +409,8 @@ type RateLimiter struct {
|
||||
Enabled bool `yaml:"enabled" json:"enabled" koanf:"enabled"`
|
||||
Store RateLimiterStoreType `yaml:"store" json:"store" koanf:"store"`
|
||||
Redis *RedisConfig `yaml:"redis_config" json:"redis_config" koanf:"redis_config"`
|
||||
PasscodeLimits RateLimits `yaml:"passcode_limits" json:"passcode_limits" koanf:"passcode_limits"`
|
||||
PasswordLimits RateLimits `yaml:"password_limits" json:"password_limits" koanf:"password_limits"`
|
||||
PasscodeLimits RateLimits `yaml:"passcode_limits" json:"passcode_limits" koanf:"passcode_limits" split_words:"true"`
|
||||
PasswordLimits RateLimits `yaml:"password_limits" json:"password_limits" koanf:"password_limits" split_words:"true"`
|
||||
}
|
||||
|
||||
type RateLimits struct {
|
||||
@ -455,9 +452,9 @@ type RedisConfig struct {
|
||||
|
||||
type ThirdParty struct {
|
||||
Providers ThirdPartyProviders `yaml:"providers" json:"providers" koanf:"providers"`
|
||||
RedirectURL string `yaml:"redirect_url" json:"redirect_url" koanf:"redirect_url"`
|
||||
ErrorRedirectURL string `yaml:"error_redirect_url" json:"error_redirect_url" koanf:"error_redirect_url"`
|
||||
AllowedRedirectURLs []string `yaml:"allowed_redirect_urls" json:"allowed_redirect_urls" koanf:"allowed_redirect_urls"`
|
||||
RedirectURL string `yaml:"redirect_url" json:"redirect_url" koanf:"redirect_url" split_words:"true"`
|
||||
ErrorRedirectURL string `yaml:"error_redirect_url" json:"error_redirect_url" koanf:"error_redirect_url" split_words:"true"`
|
||||
AllowedRedirectURLS []string `yaml:"allowed_redirect_urls" json:"allowed_redirect_urls" koanf:"allowed_redirect_urls" split_words:"true"`
|
||||
AllowedRedirectURLMap map[string]glob.Glob
|
||||
}
|
||||
|
||||
@ -471,11 +468,11 @@ func (t *ThirdParty) Validate() error {
|
||||
return errors.New("error_redirect_url must be set")
|
||||
}
|
||||
|
||||
if len(t.AllowedRedirectURLs) <= 0 {
|
||||
if len(t.AllowedRedirectURLS) <= 0 {
|
||||
return errors.New("at least one allowed redirect url must be set")
|
||||
}
|
||||
|
||||
urls := append(t.AllowedRedirectURLs, t.ErrorRedirectURL)
|
||||
urls := append(t.AllowedRedirectURLS, t.ErrorRedirectURL)
|
||||
for _, u := range urls {
|
||||
if strings.HasSuffix(u, "/") {
|
||||
return fmt.Errorf("redirect url %s must not have trailing slash", u)
|
||||
@ -493,7 +490,7 @@ func (t *ThirdParty) Validate() error {
|
||||
|
||||
func (t *ThirdParty) PostProcess() error {
|
||||
t.AllowedRedirectURLMap = make(map[string]glob.Glob)
|
||||
urls := append(t.AllowedRedirectURLs, t.ErrorRedirectURL)
|
||||
urls := append(t.AllowedRedirectURLS, t.ErrorRedirectURL)
|
||||
for _, url := range urls {
|
||||
g, err := glob.Compile(url, '.', '/')
|
||||
if err != nil {
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@ -64,3 +68,18 @@ func TestRateLimiterConfig(t *testing.T) {
|
||||
t.Error("notvalid is not a valid backend")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvironmentVariables(t *testing.T) {
|
||||
err := os.Setenv("PASSCODE_SMTP_HOST", "valueFromEnvVars")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.Setenv("SERVER_PUBLIC_CORS_ALLOW_METHODS", "GET,PUT,POST,DELETE")
|
||||
require.NoError(t, err)
|
||||
|
||||
configPath := "./minimal-config.yaml"
|
||||
cfg, err := Load(&configPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "valueFromEnvVars", cfg.Passcode.Smtp.Host)
|
||||
assert.True(t, reflect.DeepEqual([]string{"GET", "PUT", "POST", "DELETE"}, cfg.Server.Public.Cors.AllowMethods))
|
||||
}
|
||||
|
||||
@ -1,7 +1,17 @@
|
||||
# Hanko backend config
|
||||
|
||||
All config parameters with their defaults and allowed values are documented here. For some parameters there is an extra
|
||||
section with more detailed instructions below.
|
||||
The Hanko backend can be configured using a `yaml` configuration file or using environment variables.
|
||||
Environment variables have higher precedence than configuration via file (i.e. if provided, they overwrite the values
|
||||
given in the file - multivalued options, like arrays, are also _not_ merged but overwritten entirely).
|
||||
|
||||
The schema for the configuration file is given below. To set equivalent environment variables, join keys by `_`
|
||||
(underscore) and uppercase the keys, i.e. for `server.public.cors.allow_methods`
|
||||
use:
|
||||
|
||||
```shell
|
||||
export SERVER_PUBLIC_CORS_ALLOW_METHODS="GET,PUT,POST,DELETE"
|
||||
```
|
||||
|
||||
|
||||
## All available config options
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ require (
|
||||
github.com/gobwas/glob v0.2.3
|
||||
github.com/gofrs/uuid v4.4.0+incompatible
|
||||
github.com/gomodule/redigo v1.8.9
|
||||
github.com/kelseyhightower/envconfig v1.4.0
|
||||
github.com/knadh/koanf v1.5.0
|
||||
github.com/labstack/echo-contrib v0.14.0
|
||||
github.com/labstack/echo-jwt/v4 v4.1.0
|
||||
|
||||
@ -344,6 +344,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
|
||||
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
|
||||
@ -187,7 +187,7 @@ func setUpConfig(t *testing.T, enabledProviders []string, allowedRedirectURLs []
|
||||
}},
|
||||
ErrorRedirectURL: "https://error.test.example",
|
||||
RedirectURL: "https://api.test.example/callback",
|
||||
AllowedRedirectURLs: allowedRedirectURLs,
|
||||
AllowedRedirectURLS: allowedRedirectURLs,
|
||||
}}
|
||||
|
||||
for _, provider := range enabledProviders {
|
||||
|
||||
@ -219,3 +219,31 @@ func (h *UserHandler) Me(c echo.Context) error {
|
||||
|
||||
return c.JSON(http.StatusOK, map[string]string{"id": sessionToken.Subject()})
|
||||
}
|
||||
|
||||
func (h *UserHandler) Logout(c echo.Context) error {
|
||||
sessionToken, ok := c.Get("session").(jwt.Token)
|
||||
if !ok {
|
||||
return errors.New("missing or malformed jwt")
|
||||
}
|
||||
|
||||
userId := uuid.FromStringOrNil(sessionToken.Subject())
|
||||
|
||||
user, err := h.persister.GetUserPersister().Get(userId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
|
||||
err = h.auditLogger.Create(c, models.AuditLogUserLoggedOut, user, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write audit log: %w", err)
|
||||
}
|
||||
|
||||
cookie, err := h.sessionManager.DeleteCookie()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create session token: %w", err)
|
||||
}
|
||||
|
||||
c.SetCookie(cookie)
|
||||
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
}
|
||||
|
||||
@ -451,3 +451,29 @@ func (s *userSuite) TestUserHandler_Me() {
|
||||
s.Equal(userId, response.UserId)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *userSuite) TestUserHandler_Logout() {
|
||||
userId, _ := uuid.NewV4()
|
||||
|
||||
e := echo.New()
|
||||
e.Validator = dto.NewCustomValidator()
|
||||
req := httptest.NewRequest(http.MethodPost, "/logout", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
c := e.NewContext(req, rec)
|
||||
|
||||
token := jwt.New()
|
||||
err := token.Set(jwt.SubjectKey, userId.String())
|
||||
s.NoError(err)
|
||||
c.Set("session", token)
|
||||
|
||||
handler := NewUserHandler(&defaultConfig, s.storage, sessionManager{}, test.NewAuditLogger())
|
||||
|
||||
if s.NoError(handler.Logout(c)) {
|
||||
s.Equal(http.StatusNoContent, rec.Code)
|
||||
cookie := rec.Header().Get("Set-Cookie")
|
||||
s.NotEmpty(cookie)
|
||||
|
||||
split := strings.Split(cookie, ";")
|
||||
s.Equal("Max-Age=0", strings.TrimSpace(split[1]))
|
||||
}
|
||||
}
|
||||
|
||||
@ -195,6 +195,17 @@ func (s sessionManager) GenerateCookie(token string) (*http.Cookie, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s sessionManager) DeleteCookie() (*http.Cookie, error) {
|
||||
return &http.Cookie{
|
||||
Name: "hanko",
|
||||
Value: "",
|
||||
Secure: true,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
MaxAge: -1,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s sessionManager) Verify(token string) (jwt.Token, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@ -22,6 +22,7 @@ type AuditLogType string
|
||||
|
||||
var (
|
||||
AuditLogUserCreated AuditLogType = "user_created"
|
||||
AuditLogUserLoggedOut AuditLogType = "user_logged_out"
|
||||
|
||||
AuditLogPasswordSetSucceeded AuditLogType = "password_set_succeeded"
|
||||
AuditLogPasswordSetFailed AuditLogType = "password_set_failed"
|
||||
|
||||
@ -74,6 +74,7 @@ func NewPublicRouter(cfg *config.Config, persister persistence.Persister, promet
|
||||
user.GET("/:id", userHandler.Get, hankoMiddleware.Session(sessionManager))
|
||||
|
||||
e.POST("/user", userHandler.GetUserIdByEmail)
|
||||
e.POST("/logout", userHandler.Logout, hankoMiddleware.Session(sessionManager))
|
||||
|
||||
healthHandler := handler.NewHealthHandler()
|
||||
webauthnHandler, err := handler.NewWebauthnHandler(cfg, persister, sessionManager, auditLogger)
|
||||
|
||||
@ -15,6 +15,7 @@ type Manager interface {
|
||||
GenerateJWT(uuid.UUID) (string, error)
|
||||
Verify(string) (jwt.Token, error)
|
||||
GenerateCookie(token string) (*http.Cookie, error)
|
||||
DeleteCookie() (*http.Cookie, error)
|
||||
}
|
||||
|
||||
// Manager is used to create and verify session JWTs
|
||||
@ -111,3 +112,17 @@ func (g *manager) GenerateCookie(token string) (*http.Cookie, error) {
|
||||
SameSite: g.cookieConfig.SameSite,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeleteCookie returns a cookie that will expire the cookie on the frontend
|
||||
func (g *manager) DeleteCookie() (*http.Cookie, error) {
|
||||
return &http.Cookie{
|
||||
Name: "hanko",
|
||||
Value: "",
|
||||
Domain: g.cookieConfig.Domain,
|
||||
Path: "/",
|
||||
Secure: g.cookieConfig.Secure,
|
||||
HttpOnly: g.cookieConfig.HttpOnly,
|
||||
SameSite: g.cookieConfig.SameSite,
|
||||
MaxAge: -1,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -88,3 +88,16 @@ func TestGenerator_Verify_Error(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerator_DeleteCookie(t *testing.T) {
|
||||
manager := test.JwkManager{}
|
||||
cfg := config.Session{}
|
||||
sessionGenerator, err := NewManager(&manager, cfg)
|
||||
assert.NoError(t, err)
|
||||
require.NotEmpty(t, sessionGenerator)
|
||||
|
||||
cookie, err := sessionGenerator.DeleteCookie()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, -1, cookie.MaxAge)
|
||||
assert.Equal(t, "hanko", cookie.Name)
|
||||
}
|
||||
|
||||
2
backend/thirdparty/helper_test.go
vendored
2
backend/thirdparty/helper_test.go
vendored
@ -47,7 +47,7 @@ func TestIsValidRedirectTo(t *testing.T) {
|
||||
for _, testData := range tests {
|
||||
t.Run(testData.name, func(t *testing.T) {
|
||||
cfg := config.ThirdParty{
|
||||
AllowedRedirectURLs: testData.allowedRedirectURLs,
|
||||
AllowedRedirectURLS: testData.allowedRedirectURLs,
|
||||
}
|
||||
|
||||
if testData.errorRedirectURL != "" {
|
||||
|
||||
@ -11,7 +11,9 @@ import TabItem from '@theme/TabItem';
|
||||
|
||||
# Angular
|
||||
|
||||
In this guide you will learn how to add authentication to your Angular application using the Hanko custom element.
|
||||
In this guide you will learn how to use the
|
||||
[hanko-elements](https://github.com/teamhanko/hanko/blob/main/frontend/elements/README.md) web components to
|
||||
add authentication and a user profile to your Angular application.
|
||||
|
||||
## Install dependencies
|
||||
|
||||
@ -21,7 +23,7 @@ Install the `@teamhanko/hanko-elements` package:
|
||||
npm install @teamhanko/hanko-elements
|
||||
```
|
||||
|
||||
## Register custom element with Angular
|
||||
## Define custom elements schema
|
||||
|
||||
Angular requires you to explicitly declare that you are using custom elements inside your Angular modules, otherwise
|
||||
it will fail during build complaining about unknown elements. To do so, import the
|
||||
@ -46,13 +48,12 @@ import { AppComponent } from './app.component';
|
||||
})
|
||||
export class AppModule { }
|
||||
```
|
||||
## Add `<hanko-auth>` component
|
||||
|
||||
## Import & use custom element
|
||||
|
||||
Import the `register` function from `@teamhanko/hanko-elements` in the component where you want to use the
|
||||
Hanko custom element. Call `register` to register the `<hanko-auth>` element with the browser's
|
||||
[`CustomElementRegistry`](https://developer.mozilla.org/de/docs/Web/API/CustomElementRegistry). Then use the
|
||||
element in your component template.
|
||||
To provide a login interface in your app, use the `<hanko-auth>` web component. To do so, first import the `register` function
|
||||
from `@teamhanko/hanko-elements` in your Angular component. Then call `register` to register the `<hanko-auth>` element with
|
||||
the browser's [`CustomElementRegistry`](https://developer.mozilla.org/de/docs/Web/API/CustomElementRegistry) and use
|
||||
the element in your component template.
|
||||
|
||||
:::info
|
||||
|
||||
@ -67,8 +68,8 @@ If you are self-hosting you need to provide the URL of your running Hanko backen
|
||||
<TabItem value="html" label="login.component.html">
|
||||
```
|
||||
|
||||
```html title="login.component.html" showLineNumbers
|
||||
<hanko-auth [api]="hankoApi" [lang]="hankoLang"></hanko-auth>
|
||||
```html showLineNumbers
|
||||
<hanko-auth [api]="hankoApi" />
|
||||
```
|
||||
|
||||
```mdx-code-block
|
||||
@ -76,9 +77,8 @@ If you are self-hosting you need to provide the URL of your running Hanko backen
|
||||
<TabItem value="ts" label="login.component.ts">
|
||||
```
|
||||
|
||||
```js title="login.component.ts" showLineNumbers
|
||||
```js showLineNumbers
|
||||
import { Component } from '@angular/core';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { register } from '@teamhanko/hanko-elements';
|
||||
|
||||
@Component({
|
||||
@ -87,8 +87,7 @@ import { register } from '@teamhanko/hanko-elements';
|
||||
styleUrls: ['./login.component.css']
|
||||
})
|
||||
export class LoginComponent {
|
||||
hankoApi = environment.hankoApi;
|
||||
hankoLang = environment.hankoLang;
|
||||
hankoApi = "<YOUR_API_URL>";
|
||||
|
||||
constructor() {
|
||||
// register the component
|
||||
@ -106,12 +105,12 @@ export class LoginComponent {
|
||||
</Tabs>
|
||||
```
|
||||
|
||||
## Defining login callbacks
|
||||
### Define login callbacks
|
||||
|
||||
The `<hanko-auth>` element dispatches a custom `hankoAuthSuccess` event on successful login. React to this
|
||||
event in order to, for example, redirect your users to protected pages in your application.
|
||||
event to redirect your users to protected pages in your application, e.g. a [user profile page](#hanko-profile).
|
||||
|
||||
To do so, you can use Angular's event binding mechanism and supply a callback function that is defined in your component
|
||||
You can use Angular's event binding mechanism and supply a callback function that is defined in your component
|
||||
class directly on the `<hanko-auth>` element:
|
||||
|
||||
```mdx-code-block
|
||||
@ -119,12 +118,10 @@ class directly on the `<hanko-auth>` element:
|
||||
<TabItem value="html" label="login.component.html">
|
||||
```
|
||||
|
||||
```html {2} title="login.component.html" showLineNumbers
|
||||
```html {2} showLineNumbers
|
||||
<hanko-auth
|
||||
(hankoAuthSuccess)="redirectAfterLogin()"
|
||||
[api]="hankoApi"
|
||||
[lang]="hankoLang">
|
||||
</hanko-auth>
|
||||
[api]="hankoApi" />
|
||||
```
|
||||
|
||||
```mdx-code-block
|
||||
@ -132,9 +129,8 @@ class directly on the `<hanko-auth>` element:
|
||||
<TabItem value="ts" label="login.component.ts">
|
||||
```
|
||||
|
||||
```js {3,15,24-27} title="login.component.ts" showLineNumbers
|
||||
```js {2,13,22-25} showLineNumbers
|
||||
import { Component } from '@angular/core';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { Router } from '@angular/router';
|
||||
import { register } from '@teamhanko/hanko-elements';
|
||||
|
||||
@ -144,8 +140,7 @@ import { register } from '@teamhanko/hanko-elements';
|
||||
styleUrls: ['./login.component.css'],
|
||||
})
|
||||
export class LoginComponent {
|
||||
hankoApi = environment.hankoApi;
|
||||
hankoLang = environment.hankoLang;
|
||||
hankoApi = "<YOUR_API_URL>";
|
||||
|
||||
constructor(private router: Router) {
|
||||
// register the component
|
||||
@ -153,7 +148,7 @@ export class LoginComponent {
|
||||
register({ shadow: true })
|
||||
.catch((error) => {
|
||||
// handle error
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
redirectAfterLogin() {
|
||||
@ -168,11 +163,70 @@ export class LoginComponent {
|
||||
</Tabs>
|
||||
```
|
||||
|
||||
## UI customization
|
||||
## Add `<hanko-profile>` component {#hanko-profile}
|
||||
|
||||
The styles of the `hanko-auth` element can be customized using CSS variables and parts. See our guide
|
||||
on customization [here](https://github.com/teamhanko/hanko/tree/main/frontend/elements#ui-customization).
|
||||
To provide a page where users can manage their email addresses, password and passkeys, use the `<hanko-profile>` web
|
||||
component. Just as with the `<hanko-auth>` component, import the `register` function from `@teamhanko/hanko-elements` in
|
||||
your Angular component. Then call `register` to register the `<hanko-profile>` element with the browser's
|
||||
[`CustomElementRegistry`](https://developer.mozilla.org/de/docs/Web/API/CustomElementRegistry) and use
|
||||
the element in your component template.
|
||||
|
||||
## Backend request authentication
|
||||
|
||||
:::info
|
||||
|
||||
When adding the `<hanko-profile>` element to your template you must provide the URL of the Hanko API via the `api`
|
||||
attribute. If you are using [Hanko Cloud](https://cloud.hanko.io), you can find the API URL on your project dashboard.
|
||||
If you are self-hosting you need to provide the URL of your running Hanko backend.
|
||||
|
||||
:::
|
||||
|
||||
```mdx-code-block
|
||||
<Tabs>
|
||||
<TabItem value="html" label="profile.component.html">
|
||||
```
|
||||
|
||||
```html showLineNumbers
|
||||
<hanko-profile [api]="hankoApi" />
|
||||
```
|
||||
|
||||
```mdx-code-block
|
||||
</TabItem>
|
||||
<TabItem value="ts" label="profile.component.ts">
|
||||
```
|
||||
|
||||
```js showLineNumbers
|
||||
import { Component } from '@angular/core';
|
||||
import { register } from '@teamhanko/hanko-elements';
|
||||
|
||||
@Component({
|
||||
selector: 'app-profile',
|
||||
templateUrl: './profile.component.html',
|
||||
styleUrls: ['./profile.component.css']
|
||||
})
|
||||
export class ProfileComponent {
|
||||
hankoApi = "<YOUR_API_URL>";
|
||||
|
||||
constructor() {
|
||||
// register the component
|
||||
// see: https://github.com/teamhanko/hanko/blob/main/frontend/elements/README.md#script
|
||||
register({ shadow: true })
|
||||
.catch((error) => {
|
||||
// handle error
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```mdx-code-block
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
```
|
||||
|
||||
## Customize component styles
|
||||
|
||||
The styles of the `hanko-auth` and `hanko-profile` elements can be customized using CSS variables and parts. See our
|
||||
guide on customization [here](https://github.com/teamhanko/hanko/tree/main/frontend/elements#ui-customization).
|
||||
|
||||
## Authenticate backend requests
|
||||
|
||||
If you want to authenticate requests in your own backend, please view our [backend guide](/guides/backend).
|
||||
|
||||
@ -8,7 +8,9 @@ sidebar_custom_props:
|
||||
|
||||
# Next.js
|
||||
|
||||
In this guide you will learn how to add authentication to your Next.js application using the Hanko custom element.
|
||||
In this guide you will learn how to use the
|
||||
[hanko-elements](https://github.com/teamhanko/hanko/blob/main/frontend/elements/README.md) web components to
|
||||
add authentication and a user profile to your Next.js application.
|
||||
|
||||
## Install dependencies
|
||||
|
||||
@ -18,12 +20,12 @@ Install the `@teamhanko/hanko-elements` package:
|
||||
npm install @teamhanko/hanko-elements
|
||||
```
|
||||
|
||||
## Import & use custom element
|
||||
## Add `<hanko-auth>` component
|
||||
|
||||
Import the `register` function from `@teamhanko/hanko-elements` in the component where you want to use the
|
||||
Hanko custom element. Call `register` to register the `<hanko-auth>` element with the browser's
|
||||
[`CustomElementRegistry`](https://developer.mozilla.org/de/docs/Web/API/CustomElementRegistry).
|
||||
Then use the `<hanko-auth>` element in your JSX.
|
||||
To provide a login interface in your app, use the `<hanko-auth>` web component. To do so, first import the `register`
|
||||
function from `@teamhanko/hanko-elements` in your Next.js component. Then call `register` to register the `<hanko-auth>`
|
||||
element with the browser's [`CustomElementRegistry`](https://developer.mozilla.org/de/docs/Web/API/CustomElementRegistry)
|
||||
and use the `<hanko-auth>` element in your JSX.
|
||||
|
||||
:::info
|
||||
|
||||
@ -36,11 +38,9 @@ If you are self-hosting you need to provide the URL of your running Hanko backen
|
||||
```jsx title="HankoAuth.jsx" showLineNumbers
|
||||
import { register } from "@teamhanko/hanko-elements";
|
||||
|
||||
const api = process.env.NEXT_PUBLIC_HANKO_API!;
|
||||
const lang = process.env.NEXT_PUBLIC_HANKO_LANG;
|
||||
const hankoApi = "<YOUR_API_URL>";
|
||||
|
||||
export default function HankoAuth() {
|
||||
|
||||
useEffect(() => {
|
||||
// register the component
|
||||
// see: https://github.com/teamhanko/hanko/blob/main/frontend/elements/README.md#script
|
||||
@ -51,7 +51,7 @@ export default function HankoAuth() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<hanko-auth api={api} lang={lang} />
|
||||
<hanko-auth api={hankoApi} />
|
||||
);
|
||||
}
|
||||
```
|
||||
@ -82,20 +82,20 @@ export default function Home() {
|
||||
}
|
||||
```
|
||||
|
||||
## Defining login callbacks
|
||||
### Define login callbacks
|
||||
|
||||
The `<hanko-auth>` element dispatches a custom `hankoAuthSuccess` event on successful login. React to this
|
||||
event in order to, for example, redirect your users to protected pages in your application.
|
||||
event in order to, for example, redirect your users to protected pages in your application,
|
||||
e.g. a [user profile page](#hanko-profile).
|
||||
|
||||
To do so, apply an event listener with an appropriate redirect callback:
|
||||
|
||||
```jsx {2,9-20} title="HankoAuth.jsx" showLineNumbers
|
||||
import React, { useEffect } from "react";
|
||||
```jsx {2,10-19} title="HankoAuth.jsx" showLineNumbers
|
||||
import React, { useEffect, useCallback } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { register } from "@teamhanko/hanko-elements";
|
||||
|
||||
const api = process.env.NEXT_PUBLIC_HANKO_API!;
|
||||
const lang = process.env.NEXT_PUBLIC_HANKO_LANG;
|
||||
const hankoApi = "<YOUR_API_URL>";
|
||||
|
||||
export default function HankoAuth() {
|
||||
const router = useRouter();
|
||||
@ -121,17 +121,56 @@ export default function HankoAuth() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<hanko-auth api={api} lang={lang} />
|
||||
<hanko-auth api={hankoApi} />
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## UI customization
|
||||
## Add `<hanko-profile>` component {#hanko-profile}
|
||||
|
||||
The styles of the `hanko-auth` element can be customized using CSS variables and parts. See our guide
|
||||
on customization [here](https://github.com/teamhanko/hanko/tree/main/frontend/elements#ui-customization).
|
||||
To provide a page where users can manage their email addresses, password and passkeys, use the `<hanko-profile>` web
|
||||
component. Just as with the `<hanko-auth>` component, import the `register` function from `@teamhanko/hanko-elements` in
|
||||
your Next.js component. Then call `register` to register the `<hanko-profile>` element with the browser's
|
||||
[`CustomElementRegistry`](https://developer.mozilla.org/de/docs/Web/API/CustomElementRegistry) and use
|
||||
the element in your JSX.
|
||||
|
||||
## Backend request authentication
|
||||
|
||||
:::info
|
||||
|
||||
When adding the `<hanko-profile>` element to your template you must provide the URL of the Hanko API via the `api`
|
||||
attribute. If you are using [Hanko Cloud](https://cloud.hanko.io), you can find the API URL on your project dashboard.
|
||||
If you are self-hosting you need to provide the URL of your running Hanko backend.
|
||||
|
||||
:::
|
||||
|
||||
```jsx title="HankoProfile.jsx" showLineNumbers
|
||||
import { useEffect } from "react";
|
||||
import { register } from "@teamhanko/hanko-elements";
|
||||
|
||||
const hankoApi = "<YOUR_API_URL>";
|
||||
|
||||
export default function HankoProfile() {
|
||||
useEffect(() => {
|
||||
// register the component
|
||||
// see: https://github.com/teamhanko/hanko/blob/main/frontend/elements/README.md#script
|
||||
register({ shadow: true })
|
||||
.catch((error) => {
|
||||
// handle error
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<hanko-profile api={hankoApi} />
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Customize component styles
|
||||
|
||||
The styles of the `hanko-auth` and `hanko-profile` elements can be customized using CSS variables and parts. See our
|
||||
guide on customization [here](https://github.com/teamhanko/hanko/tree/main/frontend/elements#ui-customization).
|
||||
|
||||
## Authenticate backend requests
|
||||
|
||||
If you want to authenticate requests in your own backend, please view our [backend guide](/guides/backend).
|
||||
|
||||
|
||||
@ -8,7 +8,9 @@ sidebar_custom_props:
|
||||
|
||||
# React
|
||||
|
||||
In this guide you will learn how to add authentication to your React application using the Hanko custom element.
|
||||
In this guide you will learn how to use the
|
||||
[hanko-elements](https://github.com/teamhanko/hanko/blob/main/frontend/elements/README.md) web components to
|
||||
add authentication and a user profile to your React application.
|
||||
|
||||
## Install dependencies
|
||||
Install the `@teamhanko/hanko-elements` package:
|
||||
@ -17,12 +19,12 @@ Install the `@teamhanko/hanko-elements` package:
|
||||
npm install @teamhanko/hanko-elements
|
||||
```
|
||||
|
||||
## Import & use custom element
|
||||
## Add `<hanko-auth>` component
|
||||
|
||||
Import the `register` function from `@teamhanko/hanko-elements` in the component where you want to use the
|
||||
Hanko custom element. Call `register` to register the `<hanko-auth>` element with the browser's
|
||||
[`CustomElementRegistry`](https://developer.mozilla.org/de/docs/Web/API/CustomElementRegistry).
|
||||
Then use the `<hanko-auth>` element in your JSX.
|
||||
To provide a login interface in your app, use the `<hanko-auth>` web component. To do so, first import the `register`
|
||||
function from `@teamhanko/hanko-elements` in your React component. Then call `register` to register the `<hanko-auth>`
|
||||
element with the browser's [`CustomElementRegistry`](https://developer.mozilla.org/de/docs/Web/API/CustomElementRegistry)
|
||||
and use the `<hanko-auth>` element in your JSX.
|
||||
|
||||
:::info
|
||||
|
||||
@ -35,11 +37,9 @@ If you are self-hosting you need to provide the URL of your running Hanko backen
|
||||
```jsx title="HankoAuth.jsx" showLineNumbers
|
||||
import { register } from "@teamhanko/hanko-elements";
|
||||
|
||||
const api = process.env.REACT_APP_HANKO_API;
|
||||
const lang = process.env.REACT_APP_HANKO_LANG;
|
||||
const hankoApi = "<YOUR_API_URL>";
|
||||
|
||||
export default function HankoAuth() {
|
||||
|
||||
useEffect(() => {
|
||||
// register the component
|
||||
// see: https://github.com/teamhanko/hanko/blob/main/frontend/elements/README.md#script
|
||||
@ -50,30 +50,31 @@ export default function HankoAuth() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<hanko-auth api={api} lang={lang} />
|
||||
<hanko-auth api={hankoApi} />
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Defining login callbacks
|
||||
### Define login callbacks
|
||||
|
||||
The `<hanko-auth>` element dispatches a custom `hankoAuthSuccess` event on successful login. React to this
|
||||
event in order to, for example, redirect your users to protected pages in your application.
|
||||
event in order to, for example, redirect your users to protected pages in your application,
|
||||
e.g. a [user profile page](#hanko-profile).
|
||||
|
||||
To do so, apply an event listener with an appropriate redirect callback:
|
||||
|
||||
```jsx {2,9-20} title="HankoAuth.jsx" showLineNumbers
|
||||
```jsx {2,10-19} title="HankoAuth.jsx" showLineNumbers
|
||||
import React, { useEffect, useCallback } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { register } from "@teamhanko/hanko-elements";
|
||||
|
||||
const api = process.env.REACT_APP_HANKO_API;
|
||||
const lang = process.env.REACT_APP_HANKO_LANG;
|
||||
const hankoApi = "<YOUR_API_URL>";
|
||||
|
||||
export default function HankoAuth() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const redirectAfterLogin = useCallback(() => {
|
||||
// successfully logged in, redirect to a page in your application
|
||||
navigate("...", { replace: true });
|
||||
}, [navigate]);
|
||||
|
||||
@ -93,16 +94,54 @@ export default function HankoAuth() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<hanko-auth api={api} lang={lang} />
|
||||
<hanko-auth api={hankoApi} />
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## UI customization
|
||||
## Add `<hanko-profile>` component {#hanko-profile}
|
||||
|
||||
The styles of the `hanko-auth` element can be customized using CSS variables and parts. See our guide
|
||||
on customization [here](https://github.com/teamhanko/hanko/tree/main/frontend/elements#ui-customization).
|
||||
To provide a page where users can manage their email addresses, password and passkeys, use the `<hanko-profile>` web
|
||||
component. Just as with the `<hanko-auth>` component, import the `register` function from `@teamhanko/hanko-elements` in
|
||||
your React component. Then call `register` to register the `<hanko-profile>` element with the browser's
|
||||
[`CustomElementRegistry`](https://developer.mozilla.org/de/docs/Web/API/CustomElementRegistry) and use
|
||||
the element in your JSX.
|
||||
|
||||
## Backend request authentication
|
||||
:::info
|
||||
|
||||
When adding the `<hanko-profile>` element to your template you must provide the URL of the Hanko API via the `api`
|
||||
attribute. If you are using [Hanko Cloud](https://cloud.hanko.io), you can find the API URL on your project dashboard.
|
||||
If you are self-hosting you need to provide the URL of your running Hanko backend.
|
||||
|
||||
:::
|
||||
|
||||
```jsx title="HankoProfile.jsx" showLineNumbers
|
||||
import { useEffect } from "react";
|
||||
import { register } from "@teamhanko/hanko-elements";
|
||||
|
||||
const hankoApi = "<YOUR_API_URL>";
|
||||
|
||||
export default function HankoProfile() {
|
||||
useEffect(() => {
|
||||
// register the component
|
||||
// see: https://github.com/teamhanko/hanko/blob/main/frontend/elements/README.md#script
|
||||
register({ shadow: true })
|
||||
.catch((error) => {
|
||||
// handle error
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<hanko-profile api={hankoApi} />
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Customize component styles
|
||||
|
||||
The styles of the `hanko-auth` and `hanko-profile` elements can be customized using CSS variables and parts. See our
|
||||
guide on customization [here](https://github.com/teamhanko/hanko/tree/main/frontend/elements#ui-customization).
|
||||
|
||||
## Authenticate backend requests
|
||||
|
||||
If you want to authenticate requests in your own backend, please view our [backend guide](/guides/backend).
|
||||
|
||||
@ -11,7 +11,9 @@ sidebar_custom_props:
|
||||
|
||||
# Svelte
|
||||
|
||||
In this guide you will learn how to add authentication to your Svelte application using the Hanko custom element.
|
||||
In this guide you will learn how to use the
|
||||
[hanko-elements](https://github.com/teamhanko/hanko/blob/main/frontend/elements/README.md) web components to
|
||||
add authentication and a user profile to your Svelte application.
|
||||
|
||||
## Install dependencies
|
||||
Install the `@teamhanko/hanko-elements` package:
|
||||
@ -20,12 +22,12 @@ Install the `@teamhanko/hanko-elements` package:
|
||||
npm install @teamhanko/hanko-elements
|
||||
```
|
||||
|
||||
## Import & use custom element
|
||||
## Add `<hanko-auth>` component
|
||||
|
||||
Import the `register` function from `@teamhanko/hanko-elements` in the component where you want to use the
|
||||
Hanko custom element. Call `register` to register the `<hanko-auth>` element with the browser's
|
||||
[`CustomElementRegistry`](https://developer.mozilla.org/de/docs/Web/API/CustomElementRegistry).
|
||||
Then use the `<hanko-auth>` element in your component template.
|
||||
To provide a login interface in your app, use the `<hanko-auth>` web component. To do so, first import the `register`
|
||||
function from `@teamhanko/hanko-elements` in your Svelte component. Then call `register` to register the `<hanko-auth>`
|
||||
element with the browser's [`CustomElementRegistry`](https://developer.mozilla.org/de/docs/Web/API/CustomElementRegistry)
|
||||
and use the `<hanko-auth>` element in your component.
|
||||
|
||||
:::info
|
||||
|
||||
@ -35,56 +37,55 @@ If you are self-hosting you need to provide the URL of your running Hanko backen
|
||||
|
||||
:::
|
||||
|
||||
```js title="Login.svelte" showLineNumbers
|
||||
```js title="HankoAuth.svelte" showLineNumbers
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { register } from '@teamhanko/hanko-elements';
|
||||
|
||||
const api = import.meta.env.VITE_HANKO_API;
|
||||
const lang = import.meta.env.VITE_HANKO_LANG;
|
||||
const hankoApi = "<YOUR_API_URL>";
|
||||
|
||||
onMount(async () => {
|
||||
// register the component
|
||||
// see: https://github.com/teamhanko/hanko/blob/main/frontend/elements/README.md#script
|
||||
register({ shadow: true }).catch((e) => {
|
||||
console.error(e)
|
||||
register({ shadow: true })
|
||||
.catch((error) => {
|
||||
// handle error
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="content">
|
||||
<hanko-auth {api} {lang}/>
|
||||
</div>
|
||||
<hanko-auth api={hankoApi} />
|
||||
```
|
||||
|
||||
## Defining login callbacks
|
||||
### Define login callbacks
|
||||
|
||||
The `<hanko-auth>` element dispatches a custom `hankoAuthSuccess` event on successful login. React to this
|
||||
event in order to, for example, redirect your users to protected pages in your application.
|
||||
event in order to, for example, redirect your users to protected pages in your application,
|
||||
e.g. a [user profile page](#hanko-profile).
|
||||
|
||||
To do so, apply an event listener with an appropriate redirect callback:
|
||||
|
||||
```js {2-3,9-14,23,26-28,32} title="Login.svelte" showLineNumbers
|
||||
```js {2-3,7-13,23,26-28,31} title="HankoAuth.svelte" showLineNumbers
|
||||
<script>
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { useNavigate } from "svelte-navigator";
|
||||
import { register } from '@teamhanko/hanko-elements';
|
||||
|
||||
const api = import.meta.env.VITE_HANKO_API;
|
||||
const lang = import.meta.env.VITE_HANKO_LANG;
|
||||
|
||||
const hankoApi = "<YOUR_API_URL>";
|
||||
const navigate = useNavigate();
|
||||
let element;
|
||||
|
||||
const redirectToTodos = () => {
|
||||
navigate('/todo');
|
||||
const redirectAfterLogin = () => {
|
||||
// successfully logged in, redirect to a page in your application
|
||||
navigate('...');
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
// register the component
|
||||
// see: https://github.com/teamhanko/hanko/blob/main/frontend/elements/README.md#script
|
||||
register({ shadow: true }).catch((e) => {
|
||||
console.error(e)
|
||||
register({ shadow: true })
|
||||
.catch((error) => {
|
||||
// handle error
|
||||
});
|
||||
|
||||
element.addEventListener('hankoAuthSuccess', redirectToTodos);
|
||||
@ -95,16 +96,49 @@ To do so, apply an event listener with an appropriate redirect callback:
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="content">
|
||||
<hanko-auth bind:this={element} {api} {lang}/>
|
||||
</div>
|
||||
<hanko-auth bind:this={element} api={hankoApi} />
|
||||
```
|
||||
|
||||
## UI customization
|
||||
## Add `<hanko-profile>` component {#hanko-profile}
|
||||
|
||||
The styles of the `hanko-auth` element can be customized using CSS variables and parts. See our guide
|
||||
To provide a page where users can manage their email addresses, password and passkeys, use the `<hanko-profile>` web
|
||||
component. Just as with the `<hanko-auth>` component, import the `register` function from `@teamhanko/hanko-elements` in
|
||||
your Svelte component. Then call `register` to register the `<hanko-profile>` element with the browser's
|
||||
[`CustomElementRegistry`](https://developer.mozilla.org/de/docs/Web/API/CustomElementRegistry) and use
|
||||
the element in your component.
|
||||
|
||||
:::info
|
||||
|
||||
When adding the `<hanko-profile>` element to your template you must provide the URL of the Hanko API via the `api`
|
||||
attribute. If you are using [Hanko Cloud](https://cloud.hanko.io), you can find the API URL on your project dashboard.
|
||||
If you are self-hosting you need to provide the URL of your running Hanko backend.
|
||||
|
||||
:::
|
||||
|
||||
```js title="HankoProfile.svelte" showLineNumbers
|
||||
<script>
|
||||
import { register } from "@teamhanko/hanko-elements";
|
||||
|
||||
const hankoApi = "<YOUR_API_URL>";
|
||||
|
||||
onMount(async () => {
|
||||
// register the component
|
||||
// see: https://github.com/teamhanko/hanko/blob/main/frontend/elements/README.md#script
|
||||
register({ shadow: true })
|
||||
.catch((error) => {
|
||||
// handle error
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<hanko-profile api={hankoApi}/>
|
||||
```
|
||||
|
||||
## Customize component styles
|
||||
|
||||
The styles of the `hanko-auth` and `hanko-profile` elements can be customized using CSS variables and parts. See our guide
|
||||
on customization [here](https://github.com/teamhanko/hanko/tree/main/frontend/elements#ui-customization).
|
||||
|
||||
## Backend request authentication
|
||||
## Authenticate backend requests
|
||||
|
||||
If you want to authenticate requests in your own backend, please view our [backend guide](/guides/backend).
|
||||
|
||||
@ -11,8 +11,9 @@ import TabItem from '@theme/TabItem';
|
||||
|
||||
# Vue
|
||||
|
||||
In this guide you will learn how to add authentication to your Vue application using the Hanko custom element.
|
||||
|
||||
In this guide you will learn how to use the
|
||||
[hanko-elements](https://github.com/teamhanko/hanko/blob/main/frontend/elements/README.md) web components to
|
||||
add authentication and a user profile to your Vue application.
|
||||
## Install dependencies
|
||||
|
||||
Install the `@teamhanko/hanko-elements` package:
|
||||
@ -21,11 +22,12 @@ Install the `@teamhanko/hanko-elements` package:
|
||||
npm install @teamhanko/hanko-elements
|
||||
```
|
||||
|
||||
## Register custom element with Vue
|
||||
## Configure component resolution
|
||||
|
||||
Vue needs to know which elements to treat as custom elements, otherwise it will issue a warning regarding component
|
||||
resolution. To do so, provide a predicate function that determines which elements are to be considered custom elements
|
||||
to `compilerOptions.isCustomElement` in your configuration:
|
||||
to [`compilerOptions.isCustomElement`](https://vuejs.org/guide/extras/web-components.html#using-custom-elements-in-vue)
|
||||
in your configuration:
|
||||
|
||||
```mdx-code-block
|
||||
<Tabs>
|
||||
@ -40,7 +42,7 @@ export default {
|
||||
vue({
|
||||
template: {
|
||||
compilerOptions: {
|
||||
isCustomElement: (tag) => tag === "hanko-auth"
|
||||
isCustomElement: (tag) => tag.startsWith("hanko-")
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -62,7 +64,7 @@ module.exports = {
|
||||
.tap(options => ({
|
||||
...options,
|
||||
compilerOptions: {
|
||||
isCustomElement: (tag) => tag === "hanko-auth"
|
||||
isCustomElement: (tag) => tag.startsWith("hanko-")
|
||||
}
|
||||
}))
|
||||
}
|
||||
@ -74,11 +76,12 @@ module.exports = {
|
||||
</Tabs>
|
||||
```
|
||||
|
||||
## Import & use custom element
|
||||
## Add `<hanko-auth>` component
|
||||
|
||||
Import the `register` function from `@teamhanko/hanko-elements` in the component where you want to use the
|
||||
Hanko custom element. Call `register` to register the `<hanko-auth>` element with the browser's
|
||||
[`CustomElementRegistry`](https://developer.mozilla.org/de/docs/Web/API/CustomElementRegistry). Then use the
|
||||
To provide a login interface in your app, use the `<hanko-auth>` web component. To do so, first import the
|
||||
`register` function from `@teamhanko/hanko-elements` in your Vue component. Then call `register` to register the
|
||||
`<hanko-auth>` element with the browser's
|
||||
[`CustomElementRegistry`](https://developer.mozilla.org/de/docs/Web/API/CustomElementRegistry) and use the
|
||||
element in your component template.
|
||||
|
||||
:::info
|
||||
@ -94,8 +97,7 @@ If you are self-hosting you need to provide the URL of your running Hanko backen
|
||||
import { onMounted } from "vue";
|
||||
import { register } from "@teamhanko/hanko-elements";
|
||||
|
||||
const api = import.meta.env.VITE_HANKO_API;
|
||||
const lang = import.meta.env.VITE_HANKO_LANG;
|
||||
const hankoApi = "<YOUR_API_URL>";
|
||||
|
||||
onMounted(() => {
|
||||
// register the component
|
||||
@ -105,30 +107,29 @@ onMounted(() => {
|
||||
// handle error
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<hanko-auth :api="api" :lang="lang" />
|
||||
<hanko-auth :api="hankoApi" />
|
||||
</template>
|
||||
```
|
||||
|
||||
## Defining login callbacks
|
||||
### Define login callbacks
|
||||
|
||||
The `<hanko-auth>` element dispatches a custom `hankoAuthSuccess` event on successful login. React to this
|
||||
event in order to, for example, redirect your users to protected pages in your application.
|
||||
event in order to, for example, redirect your users to protected pages in your application,
|
||||
e.g. a [user profile page](#hanko-profile).
|
||||
|
||||
To do so, you can use Vue's [`v-on`](https://vuejs.org/guide/essentials/event-handling.html#listening-to-events)
|
||||
directive (shorthand: `@`) and supply a callback directly on the `<hanko-auth>` element:
|
||||
|
||||
```js {2,18-23,27} title="HankoAuth.vue" showLineNumbers
|
||||
```js {2,17-22,26} title="HankoAuth.vue" showLineNumbers
|
||||
<script setup>
|
||||
import { useRouter } from "vue-router";
|
||||
import { onMounted } from "vue";
|
||||
import { register } from "@teamhanko/hanko-elements";
|
||||
|
||||
const api = import.meta.env.VITE_HANKO_API;
|
||||
const lang = import.meta.env.VITE_HANKO_LANG;
|
||||
const hankoApi = "<YOUR_API_URL>";
|
||||
|
||||
onMounted(() => {
|
||||
// register the component
|
||||
@ -148,15 +149,54 @@ const redirectAfterLogin = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<hanko-auth @hankoAuthSuccess="redirectAfterLogin" :api="api" :lang="lang" />
|
||||
<hanko-auth @hankoAuthSuccess="redirectAfterLogin" :api="hankoApi" />
|
||||
</template>
|
||||
```
|
||||
|
||||
## UI customization
|
||||
## Add `<hanko-profile>` component {#hanko-profile}
|
||||
|
||||
The styles of the `hanko-auth` element can be customized using CSS variables and parts. See our guide
|
||||
To provide a page where users can manage their email addresses, password and passkeys, use the `<hanko-profile>` web
|
||||
component. Just as with the `<hanko-auth>` component, import the `register` function from `@teamhanko/hanko-elements` in
|
||||
your Vue component. Then call `register` to register the `<hanko-profile>` element with the browser's
|
||||
[`CustomElementRegistry`](https://developer.mozilla.org/de/docs/Web/API/CustomElementRegistry) and use
|
||||
the element in your component.
|
||||
|
||||
:::info
|
||||
|
||||
When adding the `<hanko-profile>` element to your template you must provide the URL of the Hanko API via the `api`
|
||||
attribute. If you are using [Hanko Cloud](https://cloud.hanko.io), you can find the API URL on your project dashboard.
|
||||
If you are self-hosting you need to provide the URL of your running Hanko backend.
|
||||
|
||||
:::
|
||||
|
||||
```js title="HankoProfile.vue" showLineNumbers
|
||||
<script setup>
|
||||
import { onMounted } from "vue";
|
||||
import { register } from "@teamhanko/hanko-elements";
|
||||
|
||||
const hankoApi = "<YOUR_API_URL>";
|
||||
|
||||
onMounted(() => {
|
||||
// register the component
|
||||
// see: https://github.com/teamhanko/hanko/blob/main/frontend/elements/README.md#script
|
||||
register({ shadow: true })
|
||||
.catch((error) => {
|
||||
// handle error
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<hanko-profile :api="hankoApi" />
|
||||
</template>
|
||||
|
||||
```
|
||||
|
||||
## Customize component styles
|
||||
|
||||
The styles of the `hanko-auth` and `hanko-profile` can be customized using CSS variables and parts. See our guide
|
||||
on customization [here](https://github.com/teamhanko/hanko/tree/main/frontend/elements#ui-customization).
|
||||
|
||||
## Backend request authentication
|
||||
## Authenticate backend requests
|
||||
|
||||
If you want to authenticate requests in your own backend, please view our [backend guide](/guides/backend).
|
||||
|
||||
9840
docs/package-lock.json
generated
9840
docs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -15,8 +15,8 @@
|
||||
"typecheck": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "^2.2.0",
|
||||
"@docusaurus/preset-classic": "^2.2.0",
|
||||
"@docusaurus/core": "^2.3.1",
|
||||
"@docusaurus/preset-classic": "^2.3.1",
|
||||
"@docusaurus/remark-plugin-npm2yarn": "^2.2.0",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"clsx": "^1.2.1",
|
||||
|
||||
@ -398,7 +398,7 @@
|
||||
<p class="tag-source">
|
||||
<a href="lib_client_HttpClient.ts.html" class="button">View Source</a>
|
||||
<span>
|
||||
<a href="lib_client_HttpClient.ts.html">lib/client/HttpClient.ts</a>, <a href="lib_client_HttpClient.ts.html#line241">line 241</a>
|
||||
<a href="lib_client_HttpClient.ts.html">lib/client/HttpClient.ts</a>, <a href="lib_client_HttpClient.ts.html#line265">line 265</a>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
|
||||
442
docs/static/jsdoc/hanko-frontend-sdk/HttpClient.html
vendored
442
docs/static/jsdoc/hanko-frontend-sdk/HttpClient.html
vendored
@ -326,6 +326,290 @@ we can easily return to the fetch API.</div>
|
||||
|
||||
|
||||
|
||||
<h4 class="name" id="_getAuthCookie">
|
||||
<a class="href-link" href="#_getAuthCookie">#</a>
|
||||
|
||||
<span class="code-name">
|
||||
|
||||
_getAuthCookie<span class="signature">()</span><span class="type-signature"> → {string|string}</span>
|
||||
|
||||
</span>
|
||||
</h4>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="description">
|
||||
Returns the authentication token that was stored in the cookie.
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<dl class="details">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<p class="tag-source">
|
||||
<a href="lib_client_HttpClient.ts.html" class="button">View Source</a>
|
||||
<span>
|
||||
<a href="lib_client_HttpClient.ts.html">lib/client/HttpClient.ts</a>, <a href="lib_client_HttpClient.ts.html#line289">line 289</a>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
</dl>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class='columns method-parameter'>
|
||||
<div class="column is-2"><label>Returns:</label></div>
|
||||
<div class="column is-10">
|
||||
|
||||
|
||||
|
||||
<div class="columns">
|
||||
|
||||
|
||||
<div class='column is-5 has-text-left'>
|
||||
<label>Type: </label>
|
||||
|
||||
<code class="param-type">string</code>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="columns">
|
||||
|
||||
|
||||
<div class='column is-5 has-text-left'>
|
||||
<label>Type: </label>
|
||||
|
||||
<code class="param-type">string</code>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="member">
|
||||
|
||||
|
||||
|
||||
<h4 class="name" id="_setAuthCookie">
|
||||
<a class="href-link" href="#_setAuthCookie">#</a>
|
||||
|
||||
<span class="code-name">
|
||||
|
||||
_setAuthCookie<span class="signature">(token)</span><span class="type-signature"></span>
|
||||
|
||||
</span>
|
||||
</h4>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="description">
|
||||
Stores the authentication token to the cookie.
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<h5>Parameters:</h5>
|
||||
|
||||
<div class="table-container">
|
||||
<table class="params table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
<th>Name</th>
|
||||
|
||||
|
||||
<th>Type</th>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<th class="last">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
|
||||
|
||||
|
||||
<tr class="deep-level-0">
|
||||
|
||||
<td class="name"><code>token</code></td>
|
||||
|
||||
|
||||
<td class="type">
|
||||
|
||||
|
||||
<code class="param-type">string</code>
|
||||
|
||||
|
||||
|
||||
</td>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<td class="description last">The authentication token to be stored.</td>
|
||||
</tr>
|
||||
|
||||
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<dl class="details">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<p class="tag-source">
|
||||
<a href="lib_client_HttpClient.ts.html" class="button">View Source</a>
|
||||
<span>
|
||||
<a href="lib_client_HttpClient.ts.html">lib/client/HttpClient.ts</a>, <a href="lib_client_HttpClient.ts.html#line296">line 296</a>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
</dl>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="member">
|
||||
|
||||
|
||||
|
||||
<h4 class="name" id="delete">
|
||||
<a class="href-link" href="#delete">#</a>
|
||||
|
||||
@ -442,7 +726,7 @@ we can easily return to the fetch API.</div>
|
||||
<p class="tag-source">
|
||||
<a href="lib_client_HttpClient.ts.html" class="button">View Source</a>
|
||||
<span>
|
||||
<a href="lib_client_HttpClient.ts.html">lib/client/HttpClient.ts</a>, <a href="lib_client_HttpClient.ts.html#line310">line 310</a>
|
||||
<a href="lib_client_HttpClient.ts.html">lib/client/HttpClient.ts</a>, <a href="lib_client_HttpClient.ts.html#line356">line 356</a>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
@ -650,7 +934,7 @@ we can easily return to the fetch API.</div>
|
||||
<p class="tag-source">
|
||||
<a href="lib_client_HttpClient.ts.html" class="button">View Source</a>
|
||||
<span>
|
||||
<a href="lib_client_HttpClient.ts.html">lib/client/HttpClient.ts</a>, <a href="lib_client_HttpClient.ts.html#line267">line 267</a>
|
||||
<a href="lib_client_HttpClient.ts.html">lib/client/HttpClient.ts</a>, <a href="lib_client_HttpClient.ts.html#line313">line 313</a>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
@ -903,7 +1187,7 @@ we can easily return to the fetch API.</div>
|
||||
<p class="tag-source">
|
||||
<a href="lib_client_HttpClient.ts.html" class="button">View Source</a>
|
||||
<span>
|
||||
<a href="lib_client_HttpClient.ts.html">lib/client/HttpClient.ts</a>, <a href="lib_client_HttpClient.ts.html#line300">line 300</a>
|
||||
<a href="lib_client_HttpClient.ts.html">lib/client/HttpClient.ts</a>, <a href="lib_client_HttpClient.ts.html#line346">line 346</a>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
@ -1156,7 +1440,7 @@ we can easily return to the fetch API.</div>
|
||||
<p class="tag-source">
|
||||
<a href="lib_client_HttpClient.ts.html" class="button">View Source</a>
|
||||
<span>
|
||||
<a href="lib_client_HttpClient.ts.html">lib/client/HttpClient.ts</a>, <a href="lib_client_HttpClient.ts.html#line278">line 278</a>
|
||||
<a href="lib_client_HttpClient.ts.html">lib/client/HttpClient.ts</a>, <a href="lib_client_HttpClient.ts.html#line324">line 324</a>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
@ -1409,7 +1693,7 @@ we can easily return to the fetch API.</div>
|
||||
<p class="tag-source">
|
||||
<a href="lib_client_HttpClient.ts.html" class="button">View Source</a>
|
||||
<span>
|
||||
<a href="lib_client_HttpClient.ts.html">lib/client/HttpClient.ts</a>, <a href="lib_client_HttpClient.ts.html#line289">line 289</a>
|
||||
<a href="lib_client_HttpClient.ts.html">lib/client/HttpClient.ts</a>, <a href="lib_client_HttpClient.ts.html#line335">line 335</a>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
@ -1494,6 +1778,154 @@ we can easily return to the fetch API.</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="member">
|
||||
|
||||
|
||||
|
||||
<h4 class="name" id="removeAuthCookie">
|
||||
<a class="href-link" href="#removeAuthCookie">#</a>
|
||||
|
||||
<span class="code-name">
|
||||
|
||||
removeAuthCookie<span class="signature">(token)</span><span class="type-signature"></span>
|
||||
|
||||
</span>
|
||||
</h4>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="description">
|
||||
Removes the cookie used for authentication.
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<h5>Parameters:</h5>
|
||||
|
||||
<div class="table-container">
|
||||
<table class="params table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
<th>Name</th>
|
||||
|
||||
|
||||
<th>Type</th>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<th class="last">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
|
||||
|
||||
|
||||
<tr class="deep-level-0">
|
||||
|
||||
<td class="name"><code>token</code></td>
|
||||
|
||||
|
||||
<td class="type">
|
||||
|
||||
|
||||
<code class="param-type">string</code>
|
||||
|
||||
|
||||
|
||||
</td>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<td class="description last">The authorization token to be stored.</td>
|
||||
</tr>
|
||||
|
||||
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<dl class="details">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<p class="tag-source">
|
||||
<a href="lib_client_HttpClient.ts.html" class="button">View Source</a>
|
||||
<span>
|
||||
<a href="lib_client_HttpClient.ts.html">lib/client/HttpClient.ts</a>, <a href="lib_client_HttpClient.ts.html#line303">line 303</a>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
</dl>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@ -719,7 +719,7 @@
|
||||
<p class="tag-source">
|
||||
<a href="lib_client_HttpClient.ts.html" class="button">View Source</a>
|
||||
<span>
|
||||
<a href="lib_client_HttpClient.ts.html">lib/client/HttpClient.ts</a>, <a href="lib_client_HttpClient.ts.html#line249">line 249</a>
|
||||
<a href="lib_client_HttpClient.ts.html">lib/client/HttpClient.ts</a>, <a href="lib_client_HttpClient.ts.html#line273">line 273</a>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
@ -839,7 +839,7 @@
|
||||
<p class="tag-source">
|
||||
<a href="lib_client_HttpClient.ts.html" class="button">View Source</a>
|
||||
<span>
|
||||
<a href="lib_client_HttpClient.ts.html">lib/client/HttpClient.ts</a>, <a href="lib_client_HttpClient.ts.html#line256">line 256</a>
|
||||
<a href="lib_client_HttpClient.ts.html">lib/client/HttpClient.ts</a>, <a href="lib_client_HttpClient.ts.html#line280">line 280</a>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
|
||||
152
docs/static/jsdoc/hanko-frontend-sdk/UserClient.html
vendored
152
docs/static/jsdoc/hanko-frontend-sdk/UserClient.html
vendored
@ -448,7 +448,7 @@ occurred, you may want to prompt the user to log in.
|
||||
<p class="tag-source">
|
||||
<a href="lib_client_UserClient.ts.html" class="button">View Source</a>
|
||||
<span>
|
||||
<a href="lib_client_UserClient.ts.html">lib/client/UserClient.ts</a>, <a href="lib_client_UserClient.ts.html#line122">line 122</a>
|
||||
<a href="lib_client_UserClient.ts.html">lib/client/UserClient.ts</a>, <a href="lib_client_UserClient.ts.html#line142">line 142</a>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
@ -633,7 +633,7 @@ occurred, you may want to prompt the user to log in.
|
||||
<p class="tag-source">
|
||||
<a href="lib_client_UserClient.ts.html" class="button">View Source</a>
|
||||
<span>
|
||||
<a href="lib_client_UserClient.ts.html">lib/client/UserClient.ts</a>, <a href="lib_client_UserClient.ts.html#line135">line 135</a>
|
||||
<a href="lib_client_UserClient.ts.html">lib/client/UserClient.ts</a>, <a href="lib_client_UserClient.ts.html#line155">line 155</a>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
@ -869,7 +869,7 @@ want to log in with a passcode, or if no WebAuthn credentials are registered, yo
|
||||
<p class="tag-source">
|
||||
<a href="lib_client_UserClient.ts.html" class="button">View Source</a>
|
||||
<span>
|
||||
<a href="lib_client_UserClient.ts.html">lib/client/UserClient.ts</a>, <a href="lib_client_UserClient.ts.html#line108">line 108</a>
|
||||
<a href="lib_client_UserClient.ts.html">lib/client/UserClient.ts</a>, <a href="lib_client_UserClient.ts.html#line128">line 128</a>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
@ -970,6 +970,152 @@ want to log in with a passcode, or if no WebAuthn credentials are registered, yo
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="member">
|
||||
|
||||
|
||||
|
||||
<h4 class="name" id="logout">
|
||||
<a class="href-link" href="#logout">#</a>
|
||||
|
||||
|
||||
<span class='tag'>async</span>
|
||||
|
||||
|
||||
<span class="code-name">
|
||||
|
||||
logout<span class="signature">()</span><span class="type-signature"> → {Promise.<void>}</span>
|
||||
|
||||
</span>
|
||||
</h4>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="description">
|
||||
Logs out the current user and expires the existing session cookie. A valid session cookie is required to call the logout endpoint.
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<dl class="details">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<p class="tag-source">
|
||||
<a href="lib_client_UserClient.ts.html" class="button">View Source</a>
|
||||
<span>
|
||||
<a href="lib_client_UserClient.ts.html">lib/client/UserClient.ts</a>, <a href="lib_client_UserClient.ts.html#line164">line 164</a>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
</dl>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class='columns method-parameter'>
|
||||
<div class="column is-2"><label>Throws:</label></div>
|
||||
<div class="column is-10">
|
||||
|
||||
|
||||
<div class="columns">
|
||||
|
||||
|
||||
<div class='column is-12 has-text-left'>
|
||||
<label>Type: </label>
|
||||
<code class="param-type"><a href="TechnicalError.html">TechnicalError</a></code>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class='columns method-parameter'>
|
||||
<div class="column is-2"><label>Returns:</label></div>
|
||||
<div class="column is-10">
|
||||
|
||||
|
||||
|
||||
<div class="columns">
|
||||
|
||||
|
||||
<div class='column is-5 has-text-left'>
|
||||
<label>Type: </label>
|
||||
|
||||
<code class="param-type">Promise.<void></code>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@ -206,6 +206,7 @@ class Response {
|
||||
class HttpClient {
|
||||
timeout: number;
|
||||
api: string;
|
||||
authCookieName = "hanko";
|
||||
|
||||
// eslint-disable-next-line require-jsdoc
|
||||
constructor(api: string, timeout = 13000) {
|
||||
@ -215,11 +216,10 @@ class HttpClient {
|
||||
|
||||
// eslint-disable-next-line require-jsdoc
|
||||
_fetch(path: string, options: RequestInit, xhr = new XMLHttpRequest()) {
|
||||
const api = this.api;
|
||||
const url = api + path;
|
||||
const self = this;
|
||||
const url = this.api + path;
|
||||
const timeout = this.timeout;
|
||||
const cookieName = "hanko";
|
||||
const bearerToken = Cookies.get(cookieName);
|
||||
const bearerToken = this._getAuthCookie();
|
||||
|
||||
return new Promise<Response>(function (resolve, reject) {
|
||||
xhr.open(options.method, url, true);
|
||||
@ -240,11 +240,7 @@ class HttpClient {
|
||||
|
||||
if (headers.length) {
|
||||
const authToken = xhr.getResponseHeader("X-Auth-Token");
|
||||
|
||||
if (authToken) {
|
||||
const secure = !!api.match("^https://");
|
||||
Cookies.set(cookieName, authToken, { secure });
|
||||
}
|
||||
if (authToken) self._setAuthCookie(authToken);
|
||||
}
|
||||
|
||||
resolve(new Response(xhr));
|
||||
@ -262,6 +258,35 @@ class HttpClient {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the authentication token that was stored in the cookie.
|
||||
*
|
||||
* @return {string}
|
||||
* @return {string}
|
||||
*/
|
||||
_getAuthCookie(): string {
|
||||
return Cookies.get(this.authCookieName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the authentication token to the cookie.
|
||||
*
|
||||
* @param {string} token - The authentication token to be stored.
|
||||
*/
|
||||
_setAuthCookie(token: string) {
|
||||
const secure = !!this.api.match("^https://");
|
||||
Cookies.set(this.authCookieName, token, { secure });
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the cookie used for authentication.
|
||||
*
|
||||
* @param {string} token - The authorization token to be stored.
|
||||
*/
|
||||
removeAuthCookie() {
|
||||
Cookies.remove(this.authCookieName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a GET request.
|
||||
*
|
||||
|
||||
@ -93,6 +93,7 @@ import {
|
||||
UnauthorizedError,
|
||||
} from "../Errors";
|
||||
import { Client } from "./Client";
|
||||
import Cookies from "js-cookie";
|
||||
|
||||
/**
|
||||
* A class to manage user information.
|
||||
@ -187,6 +188,27 @@ class UserClient extends Client {
|
||||
|
||||
return userResponse.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs out the current user and expires the existing session cookie. A valid session cookie is required to call the logout endpoint.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
* @throws {TechnicalError}
|
||||
*/
|
||||
async logout(): Promise<void> {
|
||||
const logoutResponse = await this.client.post("/logout");
|
||||
|
||||
// For cross-domain operations, the frontend SDK creates the cookie by reading the "X-Auth-Token" header, and
|
||||
// "Set-Cookie" headers sent by the backend have no effect due to the browser's security policy, which means that
|
||||
// the cookie must also be removed client-side in that case.
|
||||
this.client.removeAuthCookie();
|
||||
|
||||
if (logoutResponse.status === 401) {
|
||||
return; // The user is logged out already
|
||||
} else if (!logoutResponse.ok) {
|
||||
throw new TechnicalError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { UserClient };
|
||||
|
||||
17
docs/static/spec/public.yaml
vendored
17
docs/static/spec/public.yaml
vendored
@ -713,6 +713,23 @@ paths:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
/logout:
|
||||
post:
|
||||
summary: "Log out the current user"
|
||||
description: "Logs out the user by removing the authorization cookie."
|
||||
operationId: logout
|
||||
tags:
|
||||
- User Management
|
||||
security:
|
||||
- CookieAuth: [ ]
|
||||
- BearerTokenAuth: [ ]
|
||||
responses:
|
||||
'204':
|
||||
description: 'The user has been logged out'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
/users:
|
||||
post:
|
||||
summary: 'Create a user'
|
||||
|
||||
@ -5,7 +5,7 @@ This project was generated with [Angular CLI](https://github.com/angular/angular
|
||||
## Starting the app
|
||||
### Prerequisites
|
||||
|
||||
- a running Hanko API (see the instructions on how to run the API [in Docker](../backend/README.md#Docker) or [from Source](../backend/README.md#from-source))
|
||||
- a running Hanko API (see the instructions on how to run the API [in Docker](../../backend/README.md#Docker) or [from Source](../../backend/README.md#from-source))
|
||||
- a running express backend (see the [README](../express) for the express backend)
|
||||
|
||||
### Set up environment variables
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
],
|
||||
"scripts": [],
|
||||
"allowedCommonJsDependencies": [
|
||||
"@teamhanko/hanko-elements/hanko-auth"
|
||||
"@teamhanko/hanko-elements"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
|
||||
@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { LoginComponent } from './login/login.component';
|
||||
import { TodoComponent } from './todo/todo.component';
|
||||
import { ProfileComponent } from "./profile/profile.component";
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@ -14,6 +15,11 @@ const routes: Routes = [
|
||||
component: TodoComponent,
|
||||
data: { title: 'Todo' },
|
||||
},
|
||||
{
|
||||
path: 'profile',
|
||||
component: ProfileComponent,
|
||||
data: { title: 'Profile' },
|
||||
},
|
||||
{ path: '', redirectTo: '/login', pathMatch: 'full' },
|
||||
{ path: '**', component: LoginComponent },
|
||||
];
|
||||
|
||||
@ -0,0 +1,38 @@
|
||||
.nav {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.button {
|
||||
font-size: 1rem;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
color: grey!important;
|
||||
cursor: default;
|
||||
text-decoration: none!important;
|
||||
}
|
||||
|
||||
.nav .button:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.nav .button {
|
||||
color: white;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24px;
|
||||
border-radius: 17px;
|
||||
color: black;
|
||||
background-color: white;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
min-width: 330px;
|
||||
margin: 10vh auto;
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
@ -6,9 +6,10 @@ import { AppComponent } from './app.component';
|
||||
|
||||
import { LoginComponent } from './login/login.component';
|
||||
import { TodoComponent } from './todo/todo.component';
|
||||
import { ProfileComponent } from "./profile/profile.component";
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent, LoginComponent, TodoComponent],
|
||||
declarations: [AppComponent, LoginComponent, TodoComponent, ProfileComponent],
|
||||
imports: [BrowserModule, AppRoutingModule],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent],
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
.content {
|
||||
padding: 24px;
|
||||
border-radius: 17px;
|
||||
color: black;
|
||||
background-color: white;
|
||||
width: 500px;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
<div class="content">
|
||||
<div class="error">{{ error?.message }}</div>
|
||||
<hanko-auth
|
||||
(hankoAuthSuccess)="redirectToTodo()"
|
||||
[api]="api"
|
||||
|
||||
@ -6,16 +6,17 @@ import { register } from '@teamhanko/hanko-elements';
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
templateUrl: './login.component.html',
|
||||
styleUrls: ['./login.component.css'],
|
||||
styleUrls: ['../app.component.css']
|
||||
})
|
||||
export class LoginComponent {
|
||||
api = environment.hankoApi;
|
||||
error: Error | undefined;
|
||||
|
||||
constructor(private router: Router) {
|
||||
register({ shadow: true }).catch((e) => console.error(e));
|
||||
register({shadow: true}).catch((e) => this.error = e);
|
||||
}
|
||||
|
||||
redirectToTodo() {
|
||||
this.router.navigate(['/todo']);
|
||||
this.router.navigate(['/todo']).catch((e) => this.error = e);
|
||||
}
|
||||
}
|
||||
|
||||
9
examples/angular/src/app/profile/profile.component.html
Normal file
9
examples/angular/src/app/profile/profile.component.html
Normal file
@ -0,0 +1,9 @@
|
||||
<nav class="nav">
|
||||
<button (click)="logout()" class="button">Logout</button>
|
||||
<button disabled class="button">Profile</button>
|
||||
<button (click)="todos()" class="button">Todos</button>
|
||||
</nav>
|
||||
<div class="content">
|
||||
<div class="error">{{ error?.message }}</div>
|
||||
<hanko-profile [api]="api"></hanko-profile>
|
||||
</div>
|
||||
33
examples/angular/src/app/profile/profile.component.ts
Normal file
33
examples/angular/src/app/profile/profile.component.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { Router } from '@angular/router';
|
||||
import { register } from '@teamhanko/hanko-elements';
|
||||
import { TodoService } from '../services/todo.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-profile',
|
||||
templateUrl: './profile.component.html',
|
||||
styleUrls: ['../app.component.css'],
|
||||
})
|
||||
export class ProfileComponent {
|
||||
api = environment.hankoApi;
|
||||
error: Error | undefined;
|
||||
|
||||
constructor(private todoService: TodoService, private router: Router) {
|
||||
register({ shadow: true }).catch((e) => (this.error = e));
|
||||
}
|
||||
|
||||
todos() {
|
||||
this.router.navigate(['/todo']).catch((e) => (this.error = e));
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.todoService
|
||||
.logout()
|
||||
.then(() => {
|
||||
this.router.navigate(['/']).catch((e) => (this.error = e));
|
||||
return;
|
||||
})
|
||||
.catch((e) => (this.error = e));
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {environment} from '../../environments/environment';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { environment } from '../../environments/environment';
|
||||
|
||||
export interface Todo {
|
||||
todoID?: string;
|
||||
|
||||
@ -1,39 +1,3 @@
|
||||
.nav {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
padding: 10px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.button {
|
||||
font-size: 1rem;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav .button:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.nav .button {
|
||||
color: white;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24px;
|
||||
border-radius: 17px;
|
||||
color: black;
|
||||
background-color: white;
|
||||
width: 500px;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.headline {
|
||||
text-align: center;
|
||||
margin-top: 0;
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
<nav class="nav">
|
||||
<button (click)="logout()" class="button">Logout</button>
|
||||
<button (click)="profile()" class="button">Profile</button>
|
||||
<button disabled class="button">Todos</button>
|
||||
</nav>
|
||||
<div class="content">
|
||||
<h1 class="headline">Todos</h1>
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { Todos, TodoService } from '../services/todo.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-todo',
|
||||
templateUrl: './todo.component.html',
|
||||
styleUrls: ['./todo.component.css'],
|
||||
styleUrls: ['../app.component.css', './todo.component.css'],
|
||||
})
|
||||
export class TodoComponent implements OnInit {
|
||||
todos: Todos = [];
|
||||
@ -14,7 +14,6 @@ export class TodoComponent implements OnInit {
|
||||
|
||||
changeDescription(event: any) {
|
||||
this.description = event.target.value;
|
||||
console.log(this.description);
|
||||
}
|
||||
|
||||
changeCheckbox(event: any) {
|
||||
@ -114,8 +113,10 @@ export class TodoComponent implements OnInit {
|
||||
this.router.navigate(['/']).catch((e) => (this.error = e));
|
||||
return;
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
.catch((e) => this.error = e);
|
||||
}
|
||||
|
||||
profile() {
|
||||
this.router.navigate(['/profile']).catch((e) => (this.error = e));
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,3 +14,7 @@ body {
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
hanko-auth::part(form-item) {
|
||||
min-width: 100%; /* input fields and buttons are on top of each other */
|
||||
}
|
||||
|
||||
1657
examples/express/package-lock.json
generated
1657
examples/express/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -6,7 +6,7 @@ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- a running Hanko API (see the instructions on how to run the API [in Docker](../backend/README.md#Docker) or [from Source](../backend/README.md#from-source))
|
||||
- a running Hanko API (see the instructions on how to run the API [in Docker](../../backend/README.md#Docker) or [from Source](../../backend/README.md#from-source))
|
||||
- a running express backend (see the [README](../express) for the express backend)
|
||||
|
||||
### Set up environment variables
|
||||
|
||||
@ -4,16 +4,20 @@ import { useRouter } from "next/router";
|
||||
|
||||
const api = process.env.NEXT_PUBLIC_HANKO_API!;
|
||||
|
||||
function HankoAuth() {
|
||||
interface Props {
|
||||
setError(error: Error): void;
|
||||
}
|
||||
|
||||
function HankoAuth({ setError }: Props) {
|
||||
const router = useRouter();
|
||||
|
||||
const redirectToTodos = useCallback(() => {
|
||||
router.replace("/todo");
|
||||
}, [router]);
|
||||
router.replace("/todo").catch(setError);
|
||||
}, [router, setError]);
|
||||
|
||||
useEffect(() => {
|
||||
register({ shadow: true }).catch((e) => console.error(e));
|
||||
}, []);
|
||||
register({ shadow: true }).catch(setError);
|
||||
}, [setError]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("hankoAuthSuccess", redirectToTodos);
|
||||
|
||||
18
examples/nextjs/components/HankoProfile.tsx
Normal file
18
examples/nextjs/components/HankoProfile.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { register } from "@teamhanko/hanko-elements";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const api = process.env.NEXT_PUBLIC_HANKO_API!;
|
||||
|
||||
interface Props {
|
||||
setError(error: Error): void;
|
||||
}
|
||||
|
||||
function HankoProfile({ setError }: Props) {
|
||||
useEffect(() => {
|
||||
register({ shadow: true }).catch(setError);
|
||||
}, [setError]);
|
||||
|
||||
return <hanko-profile api={api} />;
|
||||
}
|
||||
|
||||
export default HankoProfile;
|
||||
@ -1,15 +1,18 @@
|
||||
import type { NextPage } from "next";
|
||||
import dynamic from "next/dynamic";
|
||||
import styles from "../styles/Todo.module.css";
|
||||
import React, { useState } from "react";
|
||||
|
||||
const HankoAuth = dynamic(() => import("../components/HankoAuth"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const Home: NextPage = () => {
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<HankoAuth />
|
||||
<div className={styles.error}>{error?.message}</div>
|
||||
<HankoAuth setError={setError} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
57
examples/nextjs/pages/profile.tsx
Normal file
57
examples/nextjs/pages/profile.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import { TodoClient } from "../util/TodoClient";
|
||||
import styles from "../styles/Todo.module.css";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const todoApi = process.env.NEXT_PUBLIC_TODO_API!;
|
||||
|
||||
const HankoProfile = dynamic(() => import("../components/HankoProfile"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const Todo: NextPage = () => {
|
||||
const router = useRouter();
|
||||
const client = useMemo(() => new TodoClient(todoApi), []);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const logout = () => {
|
||||
client
|
||||
.logout()
|
||||
.then(() => {
|
||||
router.push("/").catch((e) => setError(e));
|
||||
return;
|
||||
})
|
||||
.catch((e) => {
|
||||
setError(e);
|
||||
});
|
||||
};
|
||||
|
||||
const todos = () => {
|
||||
router.push("/todo").catch((e) => setError(e));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav className={styles.nav}>
|
||||
<button onClick={logout} className={styles.button}>
|
||||
Logout
|
||||
</button>
|
||||
<button disabled className={styles.button}>
|
||||
Profile
|
||||
</button>
|
||||
<button onClick={todos} className={styles.button}>
|
||||
Todos
|
||||
</button>
|
||||
</nav>
|
||||
<div className={styles.content}>
|
||||
<h1 className={styles.headline}>Profile</h1>
|
||||
<div className={styles.error}>{error?.message}</div>
|
||||
<HankoProfile setError={setError} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Todo;
|
||||
@ -105,6 +105,10 @@ const Todo: NextPage = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const profile = () => {
|
||||
router.push("/profile").catch(setError)
|
||||
}
|
||||
|
||||
const changeDescription = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setDescription(event.currentTarget.value);
|
||||
};
|
||||
@ -124,6 +128,12 @@ const Todo: NextPage = () => {
|
||||
<button onClick={logout} className={styles.button}>
|
||||
Logout
|
||||
</button>
|
||||
<button onClick={profile} className={styles.button}>
|
||||
Profile
|
||||
</button>
|
||||
<button disabled className={styles.button}>
|
||||
Todos
|
||||
</button>
|
||||
</nav>
|
||||
<div className={styles.content}>
|
||||
<h1 className={styles.headline}>Todos</h1>
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
.nav {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
padding: 10px;
|
||||
opacity: .9;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.button {
|
||||
@ -13,6 +11,12 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
color: grey!important;
|
||||
cursor: default;
|
||||
text-decoration: none!important;
|
||||
}
|
||||
|
||||
.nav .button:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@ -27,11 +31,10 @@
|
||||
border-radius: 17px;
|
||||
color: black;
|
||||
background-color: white;
|
||||
width: 500px;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
min-width: 330px;
|
||||
margin: 10vh auto;
|
||||
}
|
||||
|
||||
.headline {
|
||||
|
||||
@ -13,3 +13,7 @@ body {
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
hanko-auth::part(form-item) {
|
||||
min-width: 100%; /* input fields and buttons are on top of each other */
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ This is a [React](https://reactjs.org/) project bootstrapped with [Create React
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- a running Hanko API (see the instructions on how to run the API [in Docker](../backend/README.md#Docker) or [from Source](../backend/README.md#from-source))
|
||||
- a running Hanko API (see the instructions on how to run the API [in Docker](../../backend/README.md#Docker) or [from Source](../../backend/README.md#from-source))
|
||||
- a running express backend (see the [README](../express) for the express backend)
|
||||
|
||||
### Set up environment variables
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { register } from "@teamhanko/hanko-elements";
|
||||
import styles from "./Todo.module.css";
|
||||
@ -7,20 +7,22 @@ const api = process.env.REACT_APP_HANKO_API!;
|
||||
|
||||
function HankoAuth() {
|
||||
const navigate = useNavigate();
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const redirectToTodos = useCallback(() => {
|
||||
navigate("/todo", { replace: true });
|
||||
}, [navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
register({ shadow: true }).catch((e) => console.error(e));
|
||||
register({ shadow: true }).catch(setError);
|
||||
document.addEventListener("hankoAuthSuccess", redirectToTodos);
|
||||
return () =>
|
||||
document?.removeEventListener("hankoAuthSuccess", redirectToTodos);
|
||||
}, [redirectToTodos]);
|
||||
}, [redirectToTodos, setError]);
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<div className={styles.error}>{error?.message}</div>
|
||||
<hanko-auth api={api} />
|
||||
</div>
|
||||
);
|
||||
|
||||
55
examples/react/src/HankoProfile.tsx
Normal file
55
examples/react/src/HankoProfile.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { register } from "@teamhanko/hanko-elements";
|
||||
import styles from "./Todo.module.css";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { TodoClient } from "./TodoClient";
|
||||
|
||||
const hankoApi = process.env.REACT_APP_HANKO_API!
|
||||
const todoApi = process.env.REACT_APP_TODO_API!
|
||||
|
||||
function HankoProfile() {
|
||||
const navigate = useNavigate();
|
||||
const client = useMemo(() => new TodoClient(todoApi), []);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const logout = () => {
|
||||
client
|
||||
.logout()
|
||||
.then(() => {
|
||||
navigate("/");
|
||||
return;
|
||||
})
|
||||
.catch(setError);
|
||||
};
|
||||
|
||||
const todo = () => {
|
||||
navigate("/todo");
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
register({ shadow: true }).catch(setError);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav className={styles.nav}>
|
||||
<button onClick={logout} className={styles.button}>
|
||||
Logout
|
||||
</button>
|
||||
<button disabled className={styles.button}>
|
||||
Profile
|
||||
</button>
|
||||
<button onClick={todo} className={styles.button}>
|
||||
Todos
|
||||
</button>
|
||||
</nav>
|
||||
<div className={styles.content}>
|
||||
<h1 className={styles.headline}>Profile</h1>
|
||||
<div className={styles.error}>{error?.message}</div>
|
||||
<hanko-profile api={hankoApi} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default HankoProfile;
|
||||
@ -1,7 +1,5 @@
|
||||
.nav {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
padding: 10px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
@ -13,6 +11,12 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
color: grey!important;
|
||||
cursor: default;
|
||||
text-decoration: none!important;
|
||||
}
|
||||
|
||||
.nav .button:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@ -27,11 +31,10 @@
|
||||
border-radius: 17px;
|
||||
color: black;
|
||||
background-color: white;
|
||||
width: 500px;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
min-width: 330px;
|
||||
margin: 10vh auto;
|
||||
}
|
||||
|
||||
.headline {
|
||||
|
||||
@ -29,9 +29,7 @@ function Todo() {
|
||||
|
||||
return;
|
||||
})
|
||||
.catch((e) => {
|
||||
setError(e);
|
||||
});
|
||||
.catch(setError);
|
||||
};
|
||||
|
||||
const listTodos = useCallback(() => {
|
||||
@ -50,9 +48,7 @@ function Todo() {
|
||||
setTodos(todo);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
setError(e);
|
||||
});
|
||||
.catch(setError);
|
||||
}, [client, navigate]);
|
||||
|
||||
const patchTodo = (id: string, checked: boolean) => {
|
||||
@ -68,9 +64,7 @@ function Todo() {
|
||||
|
||||
return;
|
||||
})
|
||||
.catch((e) => {
|
||||
setError(e);
|
||||
});
|
||||
.catch(setError);
|
||||
};
|
||||
|
||||
const deleteTodo = (id: string) => {
|
||||
@ -86,9 +80,7 @@ function Todo() {
|
||||
|
||||
return;
|
||||
})
|
||||
.catch((e) => {
|
||||
setError(e);
|
||||
});
|
||||
.catch(setError);
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
@ -98,11 +90,13 @@ function Todo() {
|
||||
navigate("/");
|
||||
return;
|
||||
})
|
||||
.catch((e) => {
|
||||
setError(e);
|
||||
});
|
||||
.catch(setError);
|
||||
};
|
||||
|
||||
const profile = () => {
|
||||
navigate("/profile");
|
||||
}
|
||||
|
||||
const changeDescription = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setDescription(event.currentTarget.value);
|
||||
};
|
||||
@ -122,6 +116,12 @@ function Todo() {
|
||||
<button onClick={logout} className={styles.button}>
|
||||
Logout
|
||||
</button>
|
||||
<button onClick={profile} className={styles.button}>
|
||||
Profile
|
||||
</button>
|
||||
<button disabled className={styles.button}>
|
||||
Todos
|
||||
</button>
|
||||
</nav>
|
||||
<div className={styles.content}>
|
||||
<h1 className={styles.headline}>Todos</h1>
|
||||
|
||||
@ -13,3 +13,7 @@ body {
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
hanko-auth::part(form-item) {
|
||||
min-width: 100%; /* input fields and buttons are on top of each other */
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||
import HankoAuth from "./HankoAuth";
|
||||
import Todo from "./Todo";
|
||||
import "./index.css";
|
||||
import HankoProfile from "./HankoProfile";
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById("root") as HTMLElement
|
||||
@ -15,6 +16,7 @@ root.render(
|
||||
<Routes>
|
||||
<Route path="/" element={<HankoAuth />} />
|
||||
<Route path="/todo" element={<Todo />} />
|
||||
<Route path="/profile" element={<HankoProfile />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- a running Hanko API (see the instructions on how to run the API [in Docker](../backend/README.md#Docker) or [from Source](../backend/README.md#from-source))
|
||||
- a running Hanko API (see the instructions on how to run the API [in Docker](../../backend/README.md#Docker) or [from Source](../../backend/README.md#from-source))
|
||||
- a running express backend (see the [README](../express) for the express backend)
|
||||
|
||||
### Set up environment variables
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import { Route, Router } from "svelte-navigator";
|
||||
import Login from "./lib/Login.svelte";
|
||||
import Todo from "./lib/Todo.svelte";
|
||||
import Profile from "./lib/Profile.svelte";
|
||||
</script>
|
||||
|
||||
<Router>
|
||||
@ -9,9 +10,64 @@
|
||||
<Route path="/">
|
||||
<Login/>
|
||||
</Route>
|
||||
|
||||
<Route path="todo">
|
||||
<Todo/>
|
||||
</Route>
|
||||
<Route path="profile">
|
||||
<Profile/>
|
||||
</Route>
|
||||
</main>
|
||||
</Router>
|
||||
|
||||
<style>
|
||||
:global(.nav) {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
:global(.button) {
|
||||
font-size: 1rem;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:global(.button):disabled {
|
||||
color: grey!important;
|
||||
cursor: default;
|
||||
text-decoration: none!important;
|
||||
}
|
||||
|
||||
:global(.nav .button):hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
:global(.nav .button) {
|
||||
color: white;
|
||||
float: right;
|
||||
}
|
||||
|
||||
:global(.content) {
|
||||
padding: 24px;
|
||||
border-radius: 17px;
|
||||
color: black;
|
||||
background-color: white;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
min-width: 330px;
|
||||
margin: 10vh auto;
|
||||
}
|
||||
|
||||
|
||||
:global(.headline) {
|
||||
text-align: center;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
:global(.error) {
|
||||
color: red;
|
||||
padding: 0 0 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@ -13,3 +13,7 @@ body {
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
hanko-auth::part(form-item) {
|
||||
min-width: 100%; /* input fields and buttons are on top of each other */
|
||||
}
|
||||
|
||||
@ -1 +1,2 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6"
|
||||
height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
@ -12,11 +12,10 @@
|
||||
navigate('/todo');
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
register({ shadow: true }).catch((e) => {
|
||||
console.error(e)
|
||||
});
|
||||
let error: Error | null = null;
|
||||
|
||||
onMount(async () => {
|
||||
register({ shadow: true }).catch((e) => error = e);
|
||||
element?.addEventListener('hankoAuthSuccess', redirectToTodos);
|
||||
});
|
||||
|
||||
@ -26,19 +25,9 @@
|
||||
</script>
|
||||
|
||||
<div class="content">
|
||||
{#if error}
|
||||
<div class="error">{ error?.message }</div>
|
||||
{/if}
|
||||
<hanko-auth bind:this={element} {api}/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.content {
|
||||
padding: 24px;
|
||||
border-radius: 17px;
|
||||
color: black;
|
||||
background-color: white;
|
||||
width: 500px;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
</style>
|
||||
|
||||
45
examples/svelte/src/lib/Profile.svelte
Normal file
45
examples/svelte/src/lib/Profile.svelte
Normal file
@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { useNavigate } from "svelte-navigator";
|
||||
import { TodoClient } from "./TodoClient";
|
||||
import { register } from "@teamhanko/hanko-elements";
|
||||
|
||||
const hankoAPI = import.meta.env.VITE_HANKO_API;
|
||||
const todoAPI = import.meta.env.VITE_TODO_API;
|
||||
const todoClient = new TodoClient(todoAPI);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
let error: Error | null = null;
|
||||
|
||||
onMount(async () => {
|
||||
register({ shadow: true }).catch((e) => error = e);
|
||||
});
|
||||
|
||||
const logout = () => {
|
||||
todoClient
|
||||
.logout()
|
||||
.then(() => {
|
||||
navigate("/");
|
||||
return;
|
||||
})
|
||||
.catch((e) => error = e);
|
||||
}
|
||||
|
||||
const todos = () => {
|
||||
navigate("/todo");
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav class="nav">
|
||||
<button class="button" on:click|preventDefault={logout}>Logout</button>
|
||||
<button class="button" disabled>Profile</button>
|
||||
<button class="button" on:click|preventDefault={todos}>Todos</button>
|
||||
</nav>
|
||||
<div class="content">
|
||||
<h1 class="headline">Profile</h1>
|
||||
{#if error}
|
||||
<div class="error">{ error?.message }</div>
|
||||
{/if}
|
||||
<hanko-profile api={hankoAPI}/>
|
||||
</div>
|
||||
@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { useNavigate } from "svelte-navigator";
|
||||
import { TodoClient } from "./TodoClient";
|
||||
import type { Todos } from "./TodoClient"
|
||||
import { TodoClient } from "./TodoClient";
|
||||
|
||||
const api = import.meta.env.VITE_TODO_API;
|
||||
const todoClient = new TodoClient(api);
|
||||
@ -115,10 +115,16 @@
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
|
||||
const profile = () => {
|
||||
navigate("/profile");
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav class="nav">
|
||||
<button class="button" on:click={logout}>Logout</button>
|
||||
<button class="button" on:click|preventDefault={logout}>Logout</button>
|
||||
<button class="button" on:click|preventDefault={profile}>Profile</button>
|
||||
<button class="button" disabled>Todos</button>
|
||||
</nav>
|
||||
<div class="content">
|
||||
<h1 class="headline">Todos</h1>
|
||||
@ -155,47 +161,6 @@
|
||||
|
||||
|
||||
<style>
|
||||
.nav {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
padding: 10px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.button {
|
||||
font-size: 1rem;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav .button:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.nav .button {
|
||||
color: white;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24px;
|
||||
border-radius: 17px;
|
||||
color: black;
|
||||
background-color: white;
|
||||
width: 500px;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.headline {
|
||||
text-align: center;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
margin-bottom: 17px;
|
||||
@ -228,11 +193,6 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
padding: 0 0 10px;
|
||||
}
|
||||
|
||||
.input {
|
||||
border: 1px solid black;
|
||||
border-radius: 2.4px;
|
||||
|
||||
@ -6,7 +6,7 @@ This is a [Vue](https://vuejs.org/) project bootstrapped with Vue version 3.2.39
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- a running Hanko API (see the instructions on how to run the API [in Docker](../backend/README.md#Docker) or [from Source](../backend/README.md#from-source))
|
||||
- a running Hanko API (see the instructions on how to run the API [in Docker](../../backend/README.md#Docker) or [from Source](../../backend/README.md#from-source))
|
||||
- a running express backend (see the [README](../express) for the express backend)
|
||||
|
||||
### Set up environment variables
|
||||
|
||||
@ -6,8 +6,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@teamhanko/hanko-elements": "^0.2.0-alpha",
|
||||
"vue": "^3.2.38",
|
||||
"vue-router": "^4.1.5"
|
||||
"vue": "^3.2.47",
|
||||
"vue-router": "^4.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.1.4",
|
||||
|
||||
@ -5,3 +5,51 @@ import { RouterView } from "vue-router";
|
||||
<template>
|
||||
<RouterView />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.content {
|
||||
padding: 24px;
|
||||
border-radius: 17px;
|
||||
color: black;
|
||||
background-color: white;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
min-width: 330px;
|
||||
margin: 10vh auto;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
padding: 0 0 10px;
|
||||
}
|
||||
|
||||
|
||||
.button {
|
||||
font-size: 1rem;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
color: grey !important;
|
||||
cursor: default;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.nav {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
|
||||
.nav .button:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.nav .button {
|
||||
color: white;
|
||||
float: right;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -13,3 +13,7 @@ body {
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
hanko-auth::part(form-item) {
|
||||
min-width: 100%; /* input fields and buttons are on top of each other */
|
||||
}
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from "vue-router";
|
||||
import { register } from "@teamhanko/hanko-elements";
|
||||
import { onMounted } from "vue";
|
||||
|
||||
onMounted(() => {
|
||||
register({ shadow: true }).catch((e) => console.error(e));
|
||||
});
|
||||
|
||||
const api = import.meta.env.VITE_HANKO_API;
|
||||
|
||||
const router = useRouter();
|
||||
const api = import.meta.env.VITE_HANKO_API;
|
||||
const emit = defineEmits(["on-error"]);
|
||||
|
||||
const redirectToTodo = () => {
|
||||
router.push({ path: "/todo" });
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
register({ shadow: true }).catch((e) => emit("on-error", e));
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
15
examples/vue/src/components/HankoProfile.vue
Normal file
15
examples/vue/src/components/HankoProfile.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { register } from "@teamhanko/hanko-elements";
|
||||
import { onMounted } from "vue";
|
||||
|
||||
const api = import.meta.env.VITE_HANKO_API;
|
||||
const emit = defineEmits(["on-error"]);
|
||||
|
||||
onMounted(() => {
|
||||
register({ shadow: true }).catch((e) => emit("on-error", e));
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<hanko-profile :api="api" />
|
||||
</template>
|
||||
@ -1,6 +1,7 @@
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import LoginView from "../views/LoginView.vue";
|
||||
import TodoView from "../views/TodoView.vue";
|
||||
import ProfileView from "../views/ProfileView.vue";
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
@ -8,14 +9,19 @@ const router = createRouter({
|
||||
{
|
||||
path: "/",
|
||||
name: "login",
|
||||
component: LoginView
|
||||
component: LoginView,
|
||||
},
|
||||
{
|
||||
path: "/todo",
|
||||
name: "todo",
|
||||
component: TodoView
|
||||
}
|
||||
]
|
||||
component: TodoView,
|
||||
},
|
||||
{
|
||||
path: "/profile",
|
||||
name: "profile",
|
||||
component: ProfileView,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@ -1,23 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import HankoAuth from "@/components/HankoAuth.vue";
|
||||
import type { Ref } from "vue";
|
||||
import { ref } from "vue";
|
||||
|
||||
const error: Ref<Error | null> = ref(null);
|
||||
|
||||
function setError(e: Error) {
|
||||
error.value = e;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="content">
|
||||
<HankoAuth />
|
||||
<div class="error">{{ error?.message }}</div>
|
||||
<HankoAuth @on-error="setError"/>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.content {
|
||||
padding: 24px;
|
||||
border-radius: 17px;
|
||||
color: black;
|
||||
background-color: white;
|
||||
width: 500px;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
</style>
|
||||
|
||||
46
examples/vue/src/views/ProfileView.vue
Normal file
46
examples/vue/src/views/ProfileView.vue
Normal file
@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import HankoProfile from "@/components/HankoProfile.vue";
|
||||
|
||||
import { useRouter } from "vue-router";
|
||||
import { TodoClient } from "@/utils/TodoClient";
|
||||
import type { Ref } from "vue";
|
||||
import { ref } from "vue";
|
||||
|
||||
const router = useRouter();
|
||||
const api = import.meta.env.VITE_TODO_API;
|
||||
const client = new TodoClient(api);
|
||||
const error: Ref<Error | null> = ref(null);
|
||||
|
||||
function setError(e: Error) {
|
||||
error.value = e;
|
||||
}
|
||||
|
||||
function todos() {
|
||||
router.push("/todo");
|
||||
}
|
||||
|
||||
function logout() {
|
||||
client
|
||||
.logout()
|
||||
.then(() => {
|
||||
router.push("/");
|
||||
return;
|
||||
})
|
||||
.catch((e) => {
|
||||
error.value = e;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="nav">
|
||||
<button @click.prevent="logout" class="button">Logout</button>
|
||||
<button disabled class="button">Profile</button>
|
||||
<button @click.prevent="todos" class="button">Todos</button>
|
||||
</nav>
|
||||
<main class="content">
|
||||
<div class="error">{{ error?.message }}</div>
|
||||
<HankoProfile @on-error="setError" />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { TodoClient } from "@/utils/TodoClient";
|
||||
import type { Todos } from "@/utils/TodoClient";
|
||||
import { TodoClient } from "@/utils/TodoClient";
|
||||
import { useRouter } from "vue-router";
|
||||
import { onMounted, ref } from "vue";
|
||||
import type { Ref } from "vue";
|
||||
import { onMounted, ref } from "vue";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
@ -105,6 +105,10 @@ const deleteTodo = (id: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
function profile() {
|
||||
router.push("/profile");
|
||||
}
|
||||
|
||||
function logout() {
|
||||
client
|
||||
.logout()
|
||||
@ -120,7 +124,9 @@ function logout() {
|
||||
|
||||
<template>
|
||||
<nav class="nav">
|
||||
<button @click="logout" class="button">Logout</button>
|
||||
<button @click.prevent="logout" class="button">Logout</button>
|
||||
<button @click.prevent="profile" class="button">Profile</button>
|
||||
<button disabled class="button">Todos</button>
|
||||
</nav>
|
||||
<div class="content">
|
||||
<h1 class="headline">Todos</h1>
|
||||
@ -157,8 +163,6 @@ function logout() {
|
||||
<style scoped>
|
||||
.nav {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
padding: 10px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
@ -170,6 +174,12 @@ function logout() {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
color: grey !important;
|
||||
cursor: default;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.nav .button:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@ -184,11 +194,10 @@ function logout() {
|
||||
border-radius: 17px;
|
||||
color: black;
|
||||
background-color: white;
|
||||
width: 500px;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
min-width: 330px;
|
||||
margin: 10vh auto;
|
||||
}
|
||||
|
||||
.headline {
|
||||
|
||||
@ -8,7 +8,7 @@ export default defineConfig({
|
||||
plugins: [
|
||||
vue({
|
||||
template: {
|
||||
compilerOptions: { isCustomElement: (tag) => tag === "hanko-auth" },
|
||||
compilerOptions: { isCustomElement: (tag) => tag.startsWith("hanko-") },
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@teamhanko/hanko-elements",
|
||||
"version": "0.2.0-alpha",
|
||||
"version": "0.2.1-alpha",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
@ -47,10 +47,10 @@
|
||||
],
|
||||
"homepage": "https://hanko.io",
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^5.51.0",
|
||||
"@typescript-eslint/parser": "^5.53.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.54.0",
|
||||
"@typescript-eslint/parser": "^5.54.0",
|
||||
"css-loader": "^6.7.3",
|
||||
"eslint": "^8.33.0",
|
||||
"eslint": "^8.35.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-preact": "^1.3.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
|
||||
@ -5,9 +5,8 @@ import { TranslateContext } from "@denysvuika/preact-translate";
|
||||
|
||||
import { HankoError, TechnicalError } from "@teamhanko/hanko-frontend-sdk";
|
||||
|
||||
import ExclamationMark from "../icons/ExclamationMark";
|
||||
|
||||
import styles from "./styles.sass";
|
||||
import Icon from "../icons/Icon";
|
||||
|
||||
type Props = {
|
||||
error?: Error;
|
||||
@ -28,7 +27,7 @@ const ErrorMessage = ({ error = defaultError }: Props) => {
|
||||
hidden={!error}
|
||||
>
|
||||
<span>
|
||||
<ExclamationMark />
|
||||
<Icon name={"exclamation"} />
|
||||
</span>
|
||||
<span
|
||||
id="errorMessage"
|
||||
|
||||
@ -16,5 +16,8 @@
|
||||
align-items: center
|
||||
box-sizing: border-box
|
||||
|
||||
&>span:first-child
|
||||
display: inline-flex
|
||||
|
||||
&[hidden]
|
||||
display: none
|
||||
|
||||
@ -7,9 +7,10 @@ import cx from "classnames";
|
||||
import styles from "./styles.sass";
|
||||
|
||||
import LoadingSpinner from "../icons/LoadingSpinner";
|
||||
import Icon, { IconName } from "../icons/Icon";
|
||||
|
||||
type Props = {
|
||||
title?: string
|
||||
title?: string;
|
||||
children: ComponentChildren;
|
||||
secondary?: boolean;
|
||||
isLoading?: boolean;
|
||||
@ -17,6 +18,7 @@ type Props = {
|
||||
disabled?: boolean;
|
||||
autofocus?: boolean;
|
||||
onClick?: (event: Event) => void;
|
||||
icon?: IconName;
|
||||
};
|
||||
|
||||
const Button = ({
|
||||
@ -28,6 +30,7 @@ const Button = ({
|
||||
isSuccess,
|
||||
autofocus,
|
||||
onClick,
|
||||
icon,
|
||||
}: Props) => {
|
||||
const ref = useRef(null);
|
||||
|
||||
@ -56,7 +59,15 @@ const Button = ({
|
||||
isLoading={isLoading}
|
||||
isSuccess={isSuccess}
|
||||
secondary={true}
|
||||
hasIcon={!!icon}
|
||||
>
|
||||
{icon ? (
|
||||
<Icon
|
||||
name={icon}
|
||||
secondary={secondary}
|
||||
disabled={disabled || isLoading || isSuccess}
|
||||
/>
|
||||
) : null}
|
||||
{children}
|
||||
</LoadingSpinner>
|
||||
</button>
|
||||
|
||||
@ -1,21 +1,25 @@
|
||||
import * as preact from "preact";
|
||||
|
||||
import { IconProps } from "./Icon";
|
||||
import styles from "./styles.sass";
|
||||
import cx from "classnames";
|
||||
|
||||
import styles from "./styles.sass";
|
||||
|
||||
type Props = {
|
||||
fadeOut?: boolean;
|
||||
secondary?: boolean;
|
||||
};
|
||||
|
||||
const Checkmark = ({ fadeOut, secondary }: Props) => {
|
||||
const Checkmark = ({ secondary, size, fadeOut, disabled }: IconProps) => {
|
||||
return (
|
||||
<div className={cx(styles.checkmark, fadeOut && styles.fadeOut)}>
|
||||
<div className={cx(styles.circle, secondary && styles.secondary)} />
|
||||
<div className={cx(styles.stem, secondary && styles.secondary)} />
|
||||
<div className={cx(styles.kick, secondary && styles.secondary)} />
|
||||
</div>
|
||||
<svg
|
||||
id="icon-checkmark"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="4 4 40 40"
|
||||
width={size}
|
||||
height={size}
|
||||
className={cx(
|
||||
styles.checkmark,
|
||||
secondary && styles.secondary,
|
||||
fadeOut && styles.fadeOut,
|
||||
disabled && styles.disabled
|
||||
)}
|
||||
>
|
||||
<path d="M21.05 33.1 35.2 18.95l-2.3-2.25-11.85 11.85-6-6-2.25 2.25ZM24 44q-4.1 0-7.75-1.575-3.65-1.575-6.375-4.3-2.725-2.725-4.3-6.375Q4 28.1 4 24q0-4.15 1.575-7.8 1.575-3.65 4.3-6.35 2.725-2.7 6.375-4.275Q19.9 4 24 4q4.15 0 7.8 1.575 3.65 1.575 6.35 4.275 2.7 2.7 4.275 6.35Q44 19.85 44 24q0 4.1-1.575 7.75-1.575 3.65-4.275 6.375t-6.35 4.3Q28.15 44 24 44Zm0-3q7.1 0 12.05-4.975Q41 31.05 41 24q0-7.1-4.95-12.05Q31.1 7 24 7q-7.05 0-12.025 4.95Q7 16.9 7 24q0 7.05 4.975 12.025Q16.95 41 24 41Zm0-17Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,14 +1,24 @@
|
||||
import * as preact from "preact";
|
||||
|
||||
import styles from "./styles.sass";
|
||||
import { IconProps } from "./Icon";
|
||||
import cx from "classnames";
|
||||
|
||||
const ExclamationMark = () => {
|
||||
const ExclamationMark = ({ size, secondary, disabled }: IconProps) => {
|
||||
return (
|
||||
<div className={styles.exclamationMark}>
|
||||
<div className={styles.circle} />
|
||||
<div className={styles.stem} />
|
||||
<div className={styles.dot} />
|
||||
</div>
|
||||
<svg
|
||||
id="icon-exclamation"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width={size}
|
||||
height={size}
|
||||
className={cx(
|
||||
styles.exclamationMark,
|
||||
secondary && styles.secondary,
|
||||
disabled && styles.disabled
|
||||
)}
|
||||
>
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
26
frontend/elements/src/components/icons/GitHub.tsx
Normal file
26
frontend/elements/src/components/icons/GitHub.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import * as preact from "preact";
|
||||
import { IconProps } from "./Icon";
|
||||
import cx from "classnames";
|
||||
import styles from "./styles.sass";
|
||||
|
||||
const GitHub = ({ size, secondary, disabled }: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
id="icon-github"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="#fff"
|
||||
viewBox="0 0 97.63 96"
|
||||
width={size}
|
||||
height={size}
|
||||
className={cx(
|
||||
styles.icon,
|
||||
secondary && styles.secondary,
|
||||
disabled && styles.disabled
|
||||
)}
|
||||
>
|
||||
<path d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" />{" "}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default GitHub;
|
||||
49
frontend/elements/src/components/icons/Google.tsx
Normal file
49
frontend/elements/src/components/icons/Google.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import * as preact from "preact";
|
||||
import styles from "./styles.sass";
|
||||
import { IconProps } from "./Icon";
|
||||
import cx from "classnames";
|
||||
|
||||
const Google = ({ size, disabled }: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
id="icon-google"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width={size}
|
||||
height={size}
|
||||
className={styles.googleIcon}
|
||||
>
|
||||
<path
|
||||
className={cx(
|
||||
styles.googleIcon,
|
||||
disabled ? styles.disabled : styles.blue
|
||||
)}
|
||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||
/>
|
||||
<path
|
||||
className={cx(
|
||||
styles.googleIcon,
|
||||
disabled ? styles.disabled : styles.green
|
||||
)}
|
||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||
/>
|
||||
<path
|
||||
className={cx(
|
||||
styles.googleIcon,
|
||||
disabled ? styles.disabled : styles.yellow
|
||||
)}
|
||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||
/>
|
||||
<path
|
||||
className={cx(
|
||||
styles.googleIcon,
|
||||
disabled ? styles.disabled : styles.red
|
||||
)}
|
||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||
/>
|
||||
<path d="M1 1h22v22H1z" fill="none" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Google;
|
||||
36
frontend/elements/src/components/icons/Icon.tsx
Normal file
36
frontend/elements/src/components/icons/Icon.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import * as preact from "preact";
|
||||
import * as icons from "./icons";
|
||||
|
||||
export type IconName = keyof typeof icons;
|
||||
|
||||
export type IconProps = {
|
||||
secondary?: boolean;
|
||||
fadeOut?: boolean;
|
||||
disabled?: boolean;
|
||||
size?: number;
|
||||
};
|
||||
|
||||
type Props = IconProps & {
|
||||
name: IconName;
|
||||
};
|
||||
|
||||
const Icon = ({
|
||||
name,
|
||||
secondary,
|
||||
size = 18,
|
||||
fadeOut,
|
||||
disabled,
|
||||
}: Props) => {
|
||||
const Ico = icons[name];
|
||||
|
||||
return (
|
||||
<Ico
|
||||
size={size}
|
||||
secondary={secondary}
|
||||
fadeOut={fadeOut}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Icon;
|
||||
@ -1,11 +1,7 @@
|
||||
import * as preact from "preact";
|
||||
import { ComponentChildren } from "preact";
|
||||
|
||||
import cx from "classnames";
|
||||
|
||||
import Checkmark from "./Checkmark";
|
||||
|
||||
import { ComponentChildren, Fragment } from "preact";
|
||||
import styles from "./styles.sass";
|
||||
import Icon from "./Icon";
|
||||
|
||||
export type Props = {
|
||||
children?: ComponentChildren;
|
||||
@ -13,6 +9,7 @@ export type Props = {
|
||||
isSuccess?: boolean;
|
||||
fadeOut?: boolean;
|
||||
secondary?: boolean;
|
||||
hasIcon?: boolean;
|
||||
};
|
||||
|
||||
const LoadingSpinner = ({
|
||||
@ -21,19 +18,30 @@ const LoadingSpinner = ({
|
||||
isSuccess,
|
||||
fadeOut,
|
||||
secondary,
|
||||
hasIcon,
|
||||
}: Props) => {
|
||||
return (
|
||||
<div className={styles.loadingSpinnerWrapper}>
|
||||
<Fragment>
|
||||
{isLoading ? (
|
||||
<div
|
||||
className={cx(styles.loadingSpinner, secondary && styles.secondary)}
|
||||
/>
|
||||
) : isSuccess ? (
|
||||
<Checkmark fadeOut={fadeOut} secondary={secondary} />
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
<div className={styles.loadingSpinnerWrapper}>
|
||||
<Icon name={"spinner"} secondary={secondary} />
|
||||
</div>
|
||||
) : isSuccess ? (
|
||||
<div className={styles.loadingSpinnerWrapper}>
|
||||
<Icon name={"checkmark"} secondary={secondary} fadeOut={fadeOut} />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={
|
||||
hasIcon
|
||||
? styles.loadingSpinnerWrapperIcon
|
||||
: styles.loadingSpinnerWrapper
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
35
frontend/elements/src/components/icons/Passkey.tsx
Normal file
35
frontend/elements/src/components/icons/Passkey.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import * as preact from "preact";
|
||||
import { IconProps } from "./Icon";
|
||||
import styles from "./styles.sass";
|
||||
import cx from "classnames";
|
||||
|
||||
const Passkey = ({ size, secondary, disabled }: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
id="icon-passkey"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="3 1.5 19.5 19"
|
||||
width={size}
|
||||
height={size}
|
||||
className={cx(
|
||||
styles.icon,
|
||||
secondary && styles.secondary,
|
||||
disabled && styles.disabled
|
||||
)}
|
||||
>
|
||||
<g id="icon-passkey-all">
|
||||
<circle id="icon-passkey-head" cx="10.5" cy="6" r="4.5" />
|
||||
<path
|
||||
id="icon-passkey-key"
|
||||
d="M22.5,10.5a3.5,3.5,0,1,0-5,3.15V19L19,20.5,21.5,18,20,16.5,21.5,15l-1.24-1.24A3.5,3.5,0,0,0,22.5,10.5Zm-3.5,0a1,1,0,1,1,1-1A1,1,0,0,1,19,10.5Z"
|
||||
/>
|
||||
<path
|
||||
id="icon-passkey-body"
|
||||
d="M14.44,12.52A6,6,0,0,0,12,12H9a6,6,0,0,0-6,6v2H16V14.49A5.16,5.16,0,0,1,14.44,12.52Z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Passkey;
|
||||
25
frontend/elements/src/components/icons/Spinner.tsx
Normal file
25
frontend/elements/src/components/icons/Spinner.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import * as preact from "preact";
|
||||
import { IconProps } from "./Icon";
|
||||
import styles from "./styles.sass";
|
||||
import cx from "classnames";
|
||||
|
||||
const Spinner = ({ size, disabled }: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
id="icon-spinner"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width={size}
|
||||
height={size}
|
||||
className={cx(styles.loadingSpinner, disabled && styles.disabled)}
|
||||
>
|
||||
<path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/>
|
||||
<path d="M10.72,19.9a8,8,0,0,1-6.5-9.79A7.77,7.77,0,0,1,10.4,4.16a8,8,0,0,1,9.49,6.52A1.54,1.54,0,0,0,21.38,12h.13a1.37,1.37,0,0,0,1.38-1.54,11,11,0,1,0-12.7,12.39A1.54,1.54,0,0,0,12,21.34h0A1.47,1.47,0,0,0,10.72,19.9Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Spinner;
|
||||
8
frontend/elements/src/components/icons/icons.ts
Normal file
8
frontend/elements/src/components/icons/icons.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { default as passkey } from "./Passkey";
|
||||
import { default as spinner } from "./Spinner";
|
||||
import { default as checkmark } from "./Checkmark";
|
||||
import { default as exclamation } from "./ExclamationMark";
|
||||
import { default as google } from "./Google";
|
||||
import { default as github } from "./GitHub";
|
||||
|
||||
export { passkey, spinner, checkmark, exclamation, google, github };
|
||||
@ -1,50 +1,24 @@
|
||||
@use '../../variables'
|
||||
|
||||
.icon
|
||||
display: inline-block
|
||||
fill: variables.$brand-contrast-color
|
||||
width: 18px
|
||||
|
||||
&.secondary
|
||||
fill: variables.$color
|
||||
|
||||
&.disabled
|
||||
fill: variables.$color-shade-1
|
||||
|
||||
// Checkmark Styles
|
||||
|
||||
.checkmark
|
||||
display: inline-block
|
||||
width: 16px
|
||||
height: 16px
|
||||
transform: rotate(45deg)
|
||||
|
||||
.circle
|
||||
box-sizing: border-box
|
||||
display: inline-block
|
||||
border-width: 2px
|
||||
border-style: solid
|
||||
border-color: variables.$brand-color
|
||||
position: absolute
|
||||
width: 16px
|
||||
height: 16px
|
||||
border-radius: 11px
|
||||
left: 0
|
||||
top: 0
|
||||
@extend .icon
|
||||
fill: variables.$brand-color
|
||||
|
||||
&.secondary
|
||||
border-color: variables.$color-shade-1
|
||||
|
||||
.stem
|
||||
position: absolute
|
||||
width: 2px
|
||||
height: 7px
|
||||
background-color: variables.$brand-color
|
||||
left: 8px
|
||||
top: 3px
|
||||
|
||||
&.secondary
|
||||
background-color: variables.$color-shade-1
|
||||
|
||||
.kick
|
||||
position: absolute
|
||||
width: 5px
|
||||
height: 2px
|
||||
background-color: variables.$brand-color
|
||||
left: 5px
|
||||
top: 10px
|
||||
|
||||
&.secondary
|
||||
background-color: variables.$color-shade-1
|
||||
fill: variables.$color-shade-1
|
||||
|
||||
&.fadeOut
|
||||
animation: fadeOut ease-out 1.5s forwards !important
|
||||
@ -59,59 +33,32 @@
|
||||
// ExclamationMark Styles
|
||||
|
||||
.exclamationMark
|
||||
width: 16px
|
||||
height: 16px
|
||||
position: relative
|
||||
margin: 5px
|
||||
|
||||
.circle
|
||||
box-sizing: border-box
|
||||
display: inline-block
|
||||
background-color: variables.$error-color
|
||||
position: absolute
|
||||
width: 16px
|
||||
height: 16px
|
||||
border-radius: 11px
|
||||
left: 0
|
||||
top: 0
|
||||
|
||||
.stem
|
||||
position: absolute
|
||||
width: 2px
|
||||
height: 6px
|
||||
background: variables.$background-color
|
||||
left: 7px
|
||||
top: 3px
|
||||
|
||||
.dot
|
||||
position: absolute
|
||||
width: 2px
|
||||
height: 2px
|
||||
background: variables.$background-color
|
||||
left: 7px
|
||||
top: 10px
|
||||
@extend .icon
|
||||
fill: variables.$error-color
|
||||
padding-right: 5px
|
||||
|
||||
// Loading Spinner Styles
|
||||
|
||||
.loadingSpinnerWrapperIcon
|
||||
@extend .loadingSpinnerWrapper
|
||||
justify-content: flex-start
|
||||
width: 100%
|
||||
column-gap: 10px
|
||||
margin-left: 10px
|
||||
|
||||
.loadingSpinnerWrapper
|
||||
display: inline-block
|
||||
display: inline-flex
|
||||
align-items: center
|
||||
height: 100%
|
||||
margin: 0 5px
|
||||
|
||||
.loadingSpinner
|
||||
box-sizing: border-box
|
||||
display: inline-block
|
||||
border-width: 2px
|
||||
border-style: solid
|
||||
border-color: variables.$background-color
|
||||
border-top: 2px solid variables.$brand-color
|
||||
border-radius: 50%
|
||||
width: 16px
|
||||
height: 16px
|
||||
@extend .icon
|
||||
fill: variables.$brand-color
|
||||
animation: spin 500ms ease-in-out infinite
|
||||
|
||||
&.secondary
|
||||
border-color: variables.$color-shade-1
|
||||
border-top: 2px solid variables.$color-shade-2
|
||||
fill: variables.$color-shade-1
|
||||
|
||||
@keyframes spin
|
||||
0%
|
||||
@ -119,3 +66,18 @@
|
||||
|
||||
100%
|
||||
transform: rotate(360deg)
|
||||
|
||||
// Google Styles
|
||||
|
||||
.googleIcon
|
||||
&.disabled
|
||||
fill: variables.$color-shade-1
|
||||
|
||||
&.blue
|
||||
fill: #4285F4
|
||||
&.green
|
||||
fill: #34A853
|
||||
&.yellow
|
||||
fill: #FBBC05
|
||||
&.red
|
||||
fill: #EA4335
|
||||
|
||||
@ -77,7 +77,7 @@ const AppProvider = ({
|
||||
const ref = useRef<HTMLElement>(null);
|
||||
|
||||
const hanko = useMemo(() => {
|
||||
if (api.length) {
|
||||
if (api) {
|
||||
return new Hanko(api, 13000);
|
||||
}
|
||||
return null;
|
||||
|
||||
@ -31,7 +31,7 @@ const InitPage = () => {
|
||||
.then((shouldRegister) =>
|
||||
shouldRegister ? <RegisterPasskeyPage /> : <LoginFinishedPage />
|
||||
),
|
||||
[hanko.webauthn]
|
||||
[hanko]
|
||||
);
|
||||
|
||||
const initHankoAuth = useCallback(() => {
|
||||
@ -56,7 +56,7 @@ const InitPage = () => {
|
||||
}
|
||||
return <LoginEmailPage />;
|
||||
});
|
||||
}, [afterLogin, hanko.config, hanko.user, setConfig, setUser]);
|
||||
}, [afterLogin, hanko, setConfig, setUser]);
|
||||
|
||||
const initHankoProfile = useCallback(
|
||||
() =>
|
||||
@ -81,13 +81,14 @@ const InitPage = () => {
|
||||
}, [componentName, initHankoAuth, initHankoProfile]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hanko) return;
|
||||
const initializer = getInitializer();
|
||||
if (initializer) {
|
||||
initializer()
|
||||
.then(setPage)
|
||||
.catch((e) => setPage(<ErrorPage initialError={e} />));
|
||||
}
|
||||
}, [getInitializer, setPage]);
|
||||
}, [hanko, getInitializer, setPage]);
|
||||
|
||||
return <LoadingSpinner isLoading />;
|
||||
};
|
||||
|
||||
@ -30,6 +30,7 @@ import Form from "../components/form/Form";
|
||||
import Divider from "../components/divider/Divider";
|
||||
import ErrorMessage from "../components/error/ErrorMessage";
|
||||
import Headline1 from "../components/headline/Headline1";
|
||||
import { IconName } from "../components/icons/Icon";
|
||||
|
||||
import LoginPasscodePage from "./LoginPasscodePage";
|
||||
import RegisterConfirmPage from "./RegisterConfirmPage";
|
||||
@ -405,6 +406,7 @@ const LoginEmailPage = (props: Props) => {
|
||||
isLoading={isPasskeyLoginLoading}
|
||||
isSuccess={isPasskeyLoginSuccess}
|
||||
disabled={disabled}
|
||||
icon={"passkey"}
|
||||
>
|
||||
{t("labels.signInPasskey")}
|
||||
</Button>
|
||||
@ -421,6 +423,7 @@ const LoginEmailPage = (props: Props) => {
|
||||
secondary
|
||||
isLoading={isThirdPartyLoginLoading === provider}
|
||||
disabled={disabled}
|
||||
icon={provider.toLowerCase() as IconName}
|
||||
>
|
||||
{t("labels.signInWith", {
|
||||
provider,
|
||||
|
||||
@ -82,6 +82,7 @@ const RegisterPasskeyPage = () => {
|
||||
isSuccess={isSuccess}
|
||||
isLoading={isPasskeyLoading}
|
||||
disabled={disabled}
|
||||
icon={"passkey"}
|
||||
>
|
||||
{t("labels.registerAuthenticator")}
|
||||
</Button>
|
||||
|
||||
@ -44,9 +44,9 @@
|
||||
"devDependencies": {
|
||||
"@github/webauthn-json": "^2.1.1",
|
||||
"@types/jest": "^29.4.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.51.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.54.0",
|
||||
"better-docs": "^2.7.2",
|
||||
"eslint": "^8.33.0",
|
||||
"eslint": "^8.35.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-preact": "^1.3.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
@ -62,6 +62,6 @@
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/js-cookie": "^3.0.2"
|
||||
"@types/js-cookie": "^3.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@ -119,6 +119,7 @@ class Response {
|
||||
class HttpClient {
|
||||
timeout: number;
|
||||
api: string;
|
||||
authCookieName = "hanko";
|
||||
|
||||
// eslint-disable-next-line require-jsdoc
|
||||
constructor(api: string, timeout = 13000) {
|
||||
@ -128,11 +129,10 @@ class HttpClient {
|
||||
|
||||
// eslint-disable-next-line require-jsdoc
|
||||
_fetch(path: string, options: RequestInit, xhr = new XMLHttpRequest()) {
|
||||
const api = this.api;
|
||||
const url = api + path;
|
||||
const self = this;
|
||||
const url = this.api + path;
|
||||
const timeout = this.timeout;
|
||||
const cookieName = "hanko";
|
||||
const bearerToken = Cookies.get(cookieName);
|
||||
const bearerToken = this._getAuthCookie();
|
||||
|
||||
return new Promise<Response>(function (resolve, reject) {
|
||||
xhr.open(options.method, url, true);
|
||||
@ -153,11 +153,7 @@ class HttpClient {
|
||||
|
||||
if (headers.length) {
|
||||
const authToken = xhr.getResponseHeader("X-Auth-Token");
|
||||
|
||||
if (authToken) {
|
||||
const secure = !!api.match("^https://");
|
||||
Cookies.set(cookieName, authToken, { secure });
|
||||
}
|
||||
if (authToken) self._setAuthCookie(authToken);
|
||||
}
|
||||
|
||||
resolve(new Response(xhr));
|
||||
@ -175,6 +171,35 @@ class HttpClient {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the authentication token that was stored in the cookie.
|
||||
*
|
||||
* @return {string}
|
||||
* @return {string}
|
||||
*/
|
||||
_getAuthCookie(): string {
|
||||
return Cookies.get(this.authCookieName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the authentication token to the cookie.
|
||||
*
|
||||
* @param {string} token - The authentication token to be stored.
|
||||
*/
|
||||
_setAuthCookie(token: string) {
|
||||
const secure = !!this.api.match("^https://");
|
||||
Cookies.set(this.authCookieName, token, { secure });
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the cookie used for authentication.
|
||||
*
|
||||
* @param {string} token - The authorization token to be stored.
|
||||
*/
|
||||
removeAuthCookie() {
|
||||
Cookies.remove(this.authCookieName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a GET request.
|
||||
*
|
||||
|
||||
@ -100,6 +100,27 @@ class UserClient extends Client {
|
||||
|
||||
return userResponse.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs out the current user and expires the existing session cookie. A valid session cookie is required to call the logout endpoint.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
* @throws {TechnicalError}
|
||||
*/
|
||||
async logout(): Promise<void> {
|
||||
const logoutResponse = await this.client.post("/logout");
|
||||
|
||||
// For cross-domain operations, the frontend SDK creates the cookie by reading the "X-Auth-Token" header, and
|
||||
// "Set-Cookie" headers sent by the backend have no effect due to the browser's security policy, which means that
|
||||
// the cookie must also be removed client-side in that case.
|
||||
this.client.removeAuthCookie();
|
||||
|
||||
if (logoutResponse.status === 401) {
|
||||
return; // The user is logged out already
|
||||
} else if (!logoutResponse.ok) {
|
||||
throw new TechnicalError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { UserClient };
|
||||
|
||||
@ -13,15 +13,15 @@ import {
|
||||
InvalidWebauthnCredentialError,
|
||||
TechnicalError,
|
||||
UnauthorizedError,
|
||||
WebauthnRequestCancelledError,
|
||||
UserVerificationError,
|
||||
WebauthnRequestCancelledError,
|
||||
} from "../Errors";
|
||||
|
||||
import {
|
||||
Attestation,
|
||||
User,
|
||||
WebauthnFinalized,
|
||||
WebauthnCredentials,
|
||||
WebauthnFinalized,
|
||||
} from "../Dto";
|
||||
|
||||
/**
|
||||
|
||||
@ -62,7 +62,7 @@ describe("httpClient._fetch()", () => {
|
||||
this.onload();
|
||||
});
|
||||
|
||||
Cookies.get = jest.fn().mockReturnValue(jwt);
|
||||
jest.spyOn(httpClient, "_getAuthCookie").mockReturnValue(jwt);
|
||||
|
||||
await httpClient._fetch("/test", { method: "GET" }, xhr);
|
||||
|
||||
@ -84,31 +84,12 @@ describe("httpClient._fetch()", () => {
|
||||
});
|
||||
|
||||
jest.spyOn(xhr, "getResponseHeader").mockReturnValue(jwt);
|
||||
|
||||
Cookies.set = jest.fn();
|
||||
jest.spyOn(client, "_setAuthCookie");
|
||||
|
||||
await client._fetch("/test", { method: "GET" }, xhr);
|
||||
|
||||
expect(xhr.getResponseHeader).toHaveBeenCalledWith("X-Auth-Token");
|
||||
expect(Cookies.set).toHaveBeenCalledWith("hanko", jwt, { secure: false });
|
||||
});
|
||||
|
||||
it("should set a secure cookie if x-auth-token response header is available and https is used", async () => {
|
||||
httpClient = new HttpClient("https://test.api");
|
||||
|
||||
jest.spyOn(xhr, "send").mockImplementation(function () {
|
||||
// eslint-disable-next-line no-invalid-this
|
||||
this.onload();
|
||||
});
|
||||
|
||||
jest.spyOn(xhr, "getResponseHeader").mockReturnValue(jwt);
|
||||
|
||||
Cookies.set = jest.fn();
|
||||
|
||||
await httpClient._fetch("/test", { method: "GET" }, xhr);
|
||||
|
||||
expect(xhr.getResponseHeader).toHaveBeenCalledWith("X-Auth-Token");
|
||||
expect(Cookies.set).toHaveBeenCalledWith("hanko", jwt, { secure: true });
|
||||
expect(client._setAuthCookie).toHaveBeenCalledWith(jwt);
|
||||
});
|
||||
|
||||
it("should handle onerror", async () => {
|
||||
@ -134,6 +115,49 @@ describe("httpClient._fetch()", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("httpClient._setAuthCookie()", () => {
|
||||
it("should set a new cookie", async () => {
|
||||
httpClient = new HttpClient("http://test.api");
|
||||
jest.spyOn(Cookies, "set");
|
||||
httpClient._setAuthCookie("test-token");
|
||||
|
||||
expect(Cookies.set).toHaveBeenCalledWith("hanko", "test-token", {
|
||||
secure: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("should set a new secure cookie", async () => {
|
||||
httpClient = new HttpClient("https://test.api");
|
||||
jest.spyOn(Cookies, "set");
|
||||
httpClient._setAuthCookie("test-token");
|
||||
|
||||
expect(Cookies.set).toHaveBeenCalledWith("hanko", "test-token", {
|
||||
secure: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("httpClient._getAuthCookie()", () => {
|
||||
it("should return the contents of the authorization cookie", async () => {
|
||||
httpClient = new HttpClient("https://test.api");
|
||||
Cookies.get = jest.fn().mockReturnValue("test-token");
|
||||
const token = httpClient._getAuthCookie();
|
||||
|
||||
expect(Cookies.get).toHaveBeenCalledWith("hanko");
|
||||
expect(token).toBe("test-token");
|
||||
});
|
||||
});
|
||||
|
||||
describe("httpClient._removeAuthCookie()", () => {
|
||||
it("should return the contents of the authorization cookie", async () => {
|
||||
httpClient = new HttpClient("https://test.api");
|
||||
jest.spyOn(Cookies, "remove");
|
||||
httpClient.removeAuthCookie();
|
||||
|
||||
expect(Cookies.remove).toHaveBeenCalledWith("hanko");
|
||||
});
|
||||
});
|
||||
|
||||
describe("httpClient.get()", () => {
|
||||
it("should call get with correct args", async () => {
|
||||
httpClient._fetch = jest.fn();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user