fix: merge conflicts. remove import in quickstart

This commit is contained in:
Felix Dubrownik
2023-03-03 12:49:56 +01:00
105 changed files with 12072 additions and 28650 deletions

View File

@ -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 {

View File

@ -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))
}

View File

@ -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

View File

@ -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

View File

@ -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=

View File

@ -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 {

View File

@ -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)
}

View File

@ -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]))
}
}

View File

@ -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
}

View File

@ -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"

View File

@ -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)

View File

@ -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
}

View File

@ -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)
}

View File

@ -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 != "" {

View File

@ -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).

View File

@ -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).

View File

@ -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).

View File

@ -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).

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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>

View File

@ -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"> &rarr; {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>

View File

@ -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>

View File

@ -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"> &rarr; {Promise.&lt;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.&lt;void></code>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -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&lt;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.
*

View File

@ -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&lt;void>}
* @throws {TechnicalError}
*/
async logout(): Promise&lt;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 };

View File

@ -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'

View File

@ -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

View File

@ -27,7 +27,7 @@
],
"scripts": [],
"allowedCommonJsDependencies": [
"@teamhanko/hanko-elements/hanko-auth"
"@teamhanko/hanko-elements"
]
},
"configurations": {

View File

@ -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 },
];

View File

@ -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;
}

View File

@ -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],

View File

@ -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%);
}

View File

@ -1,4 +1,5 @@
<div class="content">
<div class="error">{{ error?.message }}</div>
<hanko-auth
(hankoAuthSuccess)="redirectToTodo()"
[api]="api"

View File

@ -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);
}
}

View 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>

View 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));
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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>

View File

@ -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));
}
}

View File

@ -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 */
}

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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);

View 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;

View File

@ -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>
);
};

View 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;

View File

@ -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>

View File

@ -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 {

View File

@ -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 */
}

View File

@ -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

View File

@ -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>
);

View 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;

View File

@ -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 {

View File

@ -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>

View File

@ -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 */
}

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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 */
}

View File

@ -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

View File

@ -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>

View 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>

View File

@ -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;

View File

@ -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

View File

@ -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",

View File

@ -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>

View File

@ -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 */
}

View File

@ -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>

View 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>

View File

@ -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;

View File

@ -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>

View 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>

View File

@ -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 {

View File

@ -8,7 +8,7 @@ export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: { isCustomElement: (tag) => tag === "hanko-auth" },
compilerOptions: { isCustomElement: (tag) => tag.startsWith("hanko-") },
},
}),
],

View File

@ -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",

View File

@ -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"

View File

@ -16,5 +16,8 @@
align-items: center
box-sizing: border-box
&>span:first-child
display: inline-flex
&[hidden]
display: none

View File

@ -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>

View File

@ -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>
);
};

View File

@ -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>
);
};

View 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;

View 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;

View 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;

View File

@ -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>
);
};

View 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;

View 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;

View 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 };

View File

@ -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

View File

@ -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;

View File

@ -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 />;
};

View File

@ -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,

View File

@ -82,6 +82,7 @@ const RegisterPasskeyPage = () => {
isSuccess={isSuccess}
isLoading={isPasskeyLoading}
disabled={disabled}
icon={"passkey"}
>
{t("labels.registerAuthenticator")}
</Button>

View File

@ -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"
}
}

View File

@ -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.
*

View File

@ -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 };

View File

@ -13,15 +13,15 @@ import {
InvalidWebauthnCredentialError,
TechnicalError,
UnauthorizedError,
WebauthnRequestCancelledError,
UserVerificationError,
WebauthnRequestCancelledError,
} from "../Errors";
import {
Attestation,
User,
WebauthnFinalized,
WebauthnCredentials,
WebauthnFinalized,
} from "../Dto";
/**

View File

@ -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