mirror of
https://github.com/teamhanko/hanko.git
synced 2025-11-01 22:28:27 +08:00
feat: add configuration to disable user registration
This commit is contained in:
28
.github/workflows/e2e.yml
vendored
28
.github/workflows/e2e.yml
vendored
@ -55,3 +55,31 @@ jobs:
|
||||
if: always()
|
||||
working-directory: ./deploy/docker-compose
|
||||
run: docker compose -f "quickstart.yaml" down
|
||||
nosignup:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Copy config
|
||||
working-directory: ./deploy/docker-compose
|
||||
run: cp config-disable-signup.yaml config.yaml
|
||||
|
||||
- name: Start containers
|
||||
working-directory: ./deploy/docker-compose
|
||||
run: docker compose -f "quickstart.yaml" up -d --build
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./e2e
|
||||
run: |
|
||||
npm install
|
||||
npx playwright install chromium
|
||||
|
||||
- name: Run tests
|
||||
working-directory: ./e2e
|
||||
run: npm run 'test:nosignup'
|
||||
|
||||
- name: Stop containers
|
||||
if: always()
|
||||
working-directory: ./deploy/docker-compose
|
||||
run: docker compose -f "quickstart.yaml" down
|
||||
|
||||
@ -140,6 +140,10 @@ func DefaultConfig() *Config {
|
||||
Interval: 1 * time.Minute,
|
||||
},
|
||||
},
|
||||
Account: Account{
|
||||
AllowDeletion: false,
|
||||
AllowSignup: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -626,4 +630,5 @@ type LoggerConfig struct {
|
||||
type Account struct {
|
||||
// Allow Deletion indicates if a user can perform self-service deletion
|
||||
AllowDeletion bool `yaml:"allow_deletion" json:"allow_deletion,omitempty" koanf:"allow_deletion" jsonschema:"default=false"`
|
||||
AllowSignup bool `yaml:"allow_signup" json:"allow_signup,omitempty" koanf:"allow_signup" jsonschema:"default=true"`
|
||||
}
|
||||
|
||||
@ -15,6 +15,12 @@ func TestDefaultConfigNotEnoughForValidation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultConfigAccountParameters(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
assert.Equal(t, cfg.Account.AllowDeletion, false)
|
||||
assert.Equal(t, cfg.Account.AllowSignup, true)
|
||||
}
|
||||
|
||||
func TestParseValidConfig(t *testing.T) {
|
||||
configPath := "./config.yaml"
|
||||
cfg, err := Load(&configPath)
|
||||
|
||||
@ -579,4 +579,11 @@ account:
|
||||
# Default: false
|
||||
#
|
||||
allow_deletion: false
|
||||
## allow_signup
|
||||
#
|
||||
# Users are able to sign up new accounts.
|
||||
#
|
||||
# Default: true
|
||||
#
|
||||
allow_signup: true
|
||||
```
|
||||
|
||||
@ -38,6 +38,10 @@ type UserCreateBody struct {
|
||||
}
|
||||
|
||||
func (h *UserHandler) Create(c echo.Context) error {
|
||||
if !h.cfg.Account.AllowSignup {
|
||||
return echo.NewHTTPError(http.StatusForbidden).SetInternal(errors.New("account signup is disabled"))
|
||||
}
|
||||
|
||||
var body UserCreateBody
|
||||
if err := (&echo.DefaultBinder{}).BindBody(c, &body); err != nil {
|
||||
return dto.ToHttpError(err)
|
||||
|
||||
@ -220,6 +220,27 @@ func (s *userSuite) TestUserHandler_Create_EmailMissing() {
|
||||
s.Equal(http.StatusBadRequest, rec.Code)
|
||||
}
|
||||
|
||||
func (s *userSuite) TestUserHandler_Create_AccountCreationDisabled() {
|
||||
if testing.Short() {
|
||||
s.T().Skip("skipping test in short mode.")
|
||||
}
|
||||
testConfig := test.DefaultConfig
|
||||
testConfig.Account.AllowSignup = false
|
||||
e := NewPublicRouter(&testConfig, s.Storage, nil)
|
||||
|
||||
body := UserCreateBody{Email: "jane.doe@example.com"}
|
||||
bodyJson, err := json.Marshal(body)
|
||||
s.NoError(err)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/users", bytes.NewReader(bodyJson))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
e.ServeHTTP(rec, req)
|
||||
|
||||
s.Equal(http.StatusForbidden, rec.Code)
|
||||
}
|
||||
|
||||
func (s *userSuite) TestUserHandler_Get() {
|
||||
if testing.Short() {
|
||||
s.T().Skip("skipping test in short mode.")
|
||||
|
||||
@ -9,6 +9,11 @@
|
||||
"type": "boolean",
|
||||
"description": "Allow Deletion indicates if a user can perform self-service deletion",
|
||||
"default": false
|
||||
},
|
||||
"allow_signup": {
|
||||
"type": "boolean",
|
||||
"description": "Allow Signup indicates if a user can sign up with service",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -36,4 +36,8 @@ var DefaultConfig = config.Config{
|
||||
Service: config.Service{
|
||||
Name: "Test",
|
||||
},
|
||||
Account: config.Account{
|
||||
AllowSignup: true,
|
||||
AllowDeletion: false,
|
||||
},
|
||||
}
|
||||
|
||||
31
deploy/docker-compose/config-disable-signup.yaml
Normal file
31
deploy/docker-compose/config-disable-signup.yaml
Normal file
@ -0,0 +1,31 @@
|
||||
database:
|
||||
user: hanko
|
||||
password: hanko
|
||||
host: postgresd
|
||||
port: 5432
|
||||
dialect: postgres
|
||||
passcode:
|
||||
email:
|
||||
from_address: no-reply@hanko.io
|
||||
smtp:
|
||||
host: "mailslurper"
|
||||
port: "2500"
|
||||
secrets:
|
||||
keys:
|
||||
- abcedfghijklmnopqrstuvwxyz
|
||||
service:
|
||||
name: Hanko Authentication Service
|
||||
webauthn:
|
||||
relying_party:
|
||||
origins:
|
||||
- "http://localhost:8888"
|
||||
session:
|
||||
cookie:
|
||||
secure: false # is needed for safari, because safari does not store secure cookies on localhost
|
||||
server:
|
||||
public:
|
||||
cors:
|
||||
allow_origins:
|
||||
- "http://localhost:8888"
|
||||
account:
|
||||
allow_signup: false
|
||||
@ -125,6 +125,7 @@
|
||||
<tr class="deep-level-0">
|
||||
|
||||
<td class="name"><code>allow_deletion</code></td>
|
||||
<td class="name"><code>allow_signup</code></td>
|
||||
|
||||
|
||||
<td class="type">
|
||||
|
||||
6
docs/static/spec/public.yaml
vendored
6
docs/static/spec/public.yaml
vendored
@ -819,6 +819,7 @@ paths:
|
||||
/users:
|
||||
post:
|
||||
summary: 'Create a user'
|
||||
description: Used to create a new user. To disable this endpoint, `config.account.allow_signup` must be set to false.
|
||||
operationId: createUser
|
||||
tags:
|
||||
- User Management
|
||||
@ -842,6 +843,8 @@ paths:
|
||||
$ref: '#/components/schemas/CreateUserResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'409':
|
||||
$ref: '#/components/responses/Conflict'
|
||||
'500':
|
||||
@ -1097,6 +1100,9 @@ components:
|
||||
allow_deletion:
|
||||
description: Indicates the user account can be deleted by the current user.
|
||||
type: boolean
|
||||
allow_signup:
|
||||
description: Indicates users are able to create new accounts.
|
||||
type: boolean
|
||||
CookieSession:
|
||||
type: string
|
||||
description: Value `<JWT>` is a [JSON Web Token](https://www.rfc-editor.org/rfc/rfc7519.html)
|
||||
|
||||
@ -6,6 +6,8 @@ import { RegisterAuthenticator } from "../pages/RegisterAuthenticator.js";
|
||||
import { SecuredContent } from "../pages/SecuredContent.js";
|
||||
import { LoginPassword } from "../pages/LoginPassword.js";
|
||||
import { RegisterPassword } from "../pages/RegisterPassword.js";
|
||||
import { LoginEmailNoSignup } from "../pages/LoginEmailNoSignUp.js";
|
||||
import { NoAccountFound } from "../pages/NoAccountFound.js";
|
||||
import { MailSlurper } from "../helper/MailSlurper.js";
|
||||
import * as Matchers from "../helper/Matchers.js";
|
||||
import { Error } from "../pages/Error.js";
|
||||
@ -15,6 +17,8 @@ import Setup from "../helper/Setup.js";
|
||||
export type Pages = {
|
||||
errorPage: Error;
|
||||
loginEmailPage: LoginEmail;
|
||||
loginEmailNoSignupPage: LoginEmailNoSignup;
|
||||
noAccountFoundPage: NoAccountFound,
|
||||
registerConfirmPage: RegisterConfirm;
|
||||
loginPasscodePage: LoginPasscode;
|
||||
loginPasswordPage: LoginPassword;
|
||||
@ -59,6 +63,19 @@ export const test = base.extend<TestOptions & Pages>({
|
||||
await use(loginEmailPage);
|
||||
},
|
||||
|
||||
loginEmailNoSignupPage: async ({ baseURL, page }, use) => {
|
||||
await Promise.all([
|
||||
page.waitForResponse(Endpoints.API.WELL_KNOWN_CONFIG),
|
||||
page.goto(baseURL!),
|
||||
]);
|
||||
const loginEmailPageNoSignup: LoginEmailNoSignup = new LoginEmailNoSignup(page);
|
||||
await use(loginEmailPageNoSignup);
|
||||
},
|
||||
|
||||
noAccountFoundPage: async ({ page }, use) => {
|
||||
await use(new NoAccountFound(page));
|
||||
},
|
||||
|
||||
registerConfirmPage: async ({ page }, use) => {
|
||||
await use(new RegisterConfirm(page));
|
||||
},
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
"scripts": {
|
||||
"test:nopw": "npx playwright test passwordless common",
|
||||
"test:pw": "npx playwright test passwords common",
|
||||
"test:nosignup": "npx playwright test nosignup",
|
||||
"test:report": "npx playwright show-report"
|
||||
},
|
||||
"repository": {
|
||||
|
||||
38
e2e/pages/LoginEmailNoSignUp.ts
Normal file
38
e2e/pages/LoginEmailNoSignUp.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import type { Locator, Page } from "@playwright/test";
|
||||
import { BasePage } from "./BasePage.js";
|
||||
import Endpoints from "../helper/Endpoints.js";
|
||||
|
||||
export class LoginEmailNoSignup extends BasePage {
|
||||
readonly emailInput: Locator;
|
||||
readonly continueButton: Locator;
|
||||
readonly signInPasskeyButton: Locator;
|
||||
readonly headline: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
this.emailInput = page.locator("input[name=email]");
|
||||
this.continueButton = page.locator("button[type=submit]", {
|
||||
hasText: "Continue",
|
||||
});
|
||||
this.signInPasskeyButton = page.locator("button[type=submit]", {
|
||||
hasText: "Sign in with a passkey",
|
||||
});
|
||||
this.headline = page.locator("h1", { hasText: "Sign in" });
|
||||
}
|
||||
|
||||
async continueUsingEmail(email: string) {
|
||||
await this.emailInput.fill(email);
|
||||
await Promise.all([
|
||||
this.page.waitForResponse(Endpoints.API.USER),
|
||||
this.continueButton.click(),
|
||||
]);
|
||||
}
|
||||
|
||||
async signInWithPasskey() {
|
||||
await Promise.all([
|
||||
this.page.waitForResponse(Endpoints.API.WEBAUTHN_LOGIN_INITIALIZE),
|
||||
this.page.waitForResponse(Endpoints.API.WEBAUTHN_LOGIN_FINALIZE),
|
||||
this.signInPasskeyButton.click(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
31
e2e/pages/NoAccountFound.ts
Normal file
31
e2e/pages/NoAccountFound.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import type { Locator, Page } from "@playwright/test";
|
||||
import { BasePage } from "./BasePage.js";
|
||||
import { expect } from "../fixtures/Pages.js";
|
||||
|
||||
export class NoAccountFound extends BasePage {
|
||||
readonly backLink: Locator;
|
||||
readonly signUpButton: Locator;
|
||||
readonly headline: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
this.backLink = page.locator("button", { hasText: "Back" });
|
||||
this.signUpButton = page.locator("button[type=submit]", {
|
||||
hasText: "Sign up",
|
||||
});
|
||||
this.headline = page.locator("h1", { hasText: "No account found" });
|
||||
}
|
||||
|
||||
async assertSignupButtonNotVisible() {
|
||||
await expect(this.signUpButton).not.toBeVisible();
|
||||
}
|
||||
|
||||
async assertNoAccountFoundText(email: string) {
|
||||
const text = this.page.locator("p", {hasText: `No account exists for "${email}".`});
|
||||
await expect(text).toBeVisible();
|
||||
}
|
||||
|
||||
async back() {
|
||||
await this.backLink.click();
|
||||
}
|
||||
}
|
||||
34
e2e/tests/nosignup.spec.ts
Normal file
34
e2e/tests/nosignup.spec.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { test, expect } from "../fixtures/Pages.js";
|
||||
import { faker } from "@faker-js/faker";
|
||||
|
||||
test.describe("@nosignup", () => {
|
||||
test("Login with account not found", async ({
|
||||
loginEmailNoSignupPage,
|
||||
noAccountFoundPage
|
||||
}) => {
|
||||
const email = faker.internet.email();
|
||||
|
||||
await test.step("When I visit the baseURL, the LoginEmailNoSignup page should be shown", async () => {
|
||||
await expect(loginEmailNoSignupPage.headline).toBeVisible();
|
||||
await expect(loginEmailNoSignupPage.signInPasskeyButton).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step("And when I submit an email address", async () => {
|
||||
await loginEmailNoSignupPage.continueUsingEmail(email);
|
||||
});
|
||||
|
||||
await test.step("No account should be found", async () => {
|
||||
await noAccountFoundPage.assertNoAccountFoundText(email);
|
||||
});
|
||||
|
||||
await test.step("Signup button should not be visible", async() => {
|
||||
await noAccountFoundPage.assertSignupButtonNotVisible();
|
||||
});
|
||||
|
||||
await test.step("Navigating back should take me back to LoginEmailNoSignup page", async () => {
|
||||
await noAccountFoundPage.back();
|
||||
await expect(loginEmailNoSignupPage.headline).toBeVisible();
|
||||
await expect(loginEmailNoSignupPage.signInPasskeyButton).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -4,6 +4,7 @@ export const de: Translation = {
|
||||
headlines: {
|
||||
error: "Ein Fehler ist aufgetreten",
|
||||
loginEmail: "Anmelden / Registrieren",
|
||||
loginEmailNoSignup: "Anmelden",
|
||||
loginFinished: "Login erfolgreich",
|
||||
loginPasscode: "Passcode eingeben",
|
||||
loginPassword: "Passwort eingeben",
|
||||
@ -24,6 +25,7 @@ export const de: Translation = {
|
||||
createdAt: "Erstellt am",
|
||||
connectedAccounts: "Verbundene Konten",
|
||||
deleteAccount: "Konto löschen",
|
||||
accountNotFound: "Konto nicht gefunden"
|
||||
},
|
||||
texts: {
|
||||
enterPasscode:
|
||||
@ -57,6 +59,8 @@ export const de: Translation = {
|
||||
"Löschen Sie diesen Passkey aus Ihrem Konto. Beachten Sie, dass der Passkey noch auf Ihren Geräten vorhanden ist und auch dort gelöscht werden muss.",
|
||||
deleteAccount:
|
||||
"Sind Sie sicher, dass Sie Ihr Konto löschen wollen? Alle Daten werden sofort gelöscht und können nicht wiederhergestellt werden.",
|
||||
noAccountExists:
|
||||
'Es existiert kein Konto für "{emailAddress}".',
|
||||
},
|
||||
labels: {
|
||||
or: "oder",
|
||||
|
||||
@ -4,6 +4,7 @@ export const en: Translation = {
|
||||
headlines: {
|
||||
error: "An error has occurred",
|
||||
loginEmail: "Sign in or sign up",
|
||||
loginEmailNoSignup: "Sign in",
|
||||
loginFinished: "Login successful",
|
||||
loginPasscode: "Enter passcode",
|
||||
loginPassword: "Enter password",
|
||||
@ -24,6 +25,7 @@ export const en: Translation = {
|
||||
createdAt: "Created at",
|
||||
connectedAccounts: "Connected accounts",
|
||||
deleteAccount: "Delete account",
|
||||
accountNotFound: "Account not found",
|
||||
},
|
||||
texts: {
|
||||
enterPasscode: 'Enter the passcode that was sent to "{emailAddress}".',
|
||||
@ -55,6 +57,8 @@ export const en: Translation = {
|
||||
"Delete this passkey from your account. Note that the passkey will still exist on your devices and needs to be deleted there as well.",
|
||||
deleteAccount:
|
||||
"Are you sure you want to delete this account? All data will be deleted immediately and cannot be recovered.",
|
||||
noAccountExists:
|
||||
'No account exists for "{emailAddress}".',
|
||||
},
|
||||
labels: {
|
||||
or: "or",
|
||||
|
||||
@ -4,6 +4,7 @@ export const fr: Translation = {
|
||||
headlines: {
|
||||
error: "Une erreur s'est produite",
|
||||
loginEmail: "Se connecter ou s'inscrire",
|
||||
loginEmailNoSignup: "Se connecter",
|
||||
loginFinished: "Connexion réussie",
|
||||
loginPasscode: "Entrez le code d'accès",
|
||||
loginPassword: "Entrez le mot de passe",
|
||||
@ -24,6 +25,7 @@ export const fr: Translation = {
|
||||
createdAt: "Créé le",
|
||||
connectedAccounts: "Comptes connectés",
|
||||
deleteAccount: "Supprimer le compte",
|
||||
accountNotFound: "Compte non trouvé",
|
||||
},
|
||||
texts: {
|
||||
enterPasscode:
|
||||
@ -57,6 +59,8 @@ export const fr: Translation = {
|
||||
"Supprimez cette clé d'identification de votre compte. Notez que la clé d'identification continuera d'exister sur vos appareils et devra également y être supprimée.",
|
||||
deleteAccount:
|
||||
"Êtes-vous sûr de vouloir supprimer ce compte ? Toutes les données seront supprimées immédiatement et ne pourront pas être récupérées.",
|
||||
noAccountExists:
|
||||
'Aucun compte n\'existe pour "{emailAddress}".',
|
||||
},
|
||||
labels: {
|
||||
or: "ou",
|
||||
|
||||
@ -7,7 +7,9 @@ export interface Translations {
|
||||
export interface Translation {
|
||||
headlines: {
|
||||
error: string;
|
||||
accountNotFound: string;
|
||||
loginEmail: string;
|
||||
loginEmailNoSignup: string;
|
||||
loginFinished: string;
|
||||
loginPasscode: string;
|
||||
loginPassword: string;
|
||||
@ -33,6 +35,7 @@ export interface Translation {
|
||||
enterPasscode: string;
|
||||
setupPasskey: string;
|
||||
createAccount: string;
|
||||
noAccountExists: string;
|
||||
passwordFormatHint: string;
|
||||
manageEmails: string;
|
||||
changePassword: string;
|
||||
|
||||
45
frontend/elements/src/pages/AccountNotFoundPage.tsx
Normal file
45
frontend/elements/src/pages/AccountNotFoundPage.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { Fragment } from "preact";
|
||||
import { useContext } from "preact/compat";
|
||||
|
||||
import { TranslateContext } from "@denysvuika/preact-translate";
|
||||
|
||||
import Content from "../components/wrapper/Content";
|
||||
import Footer from "../components/wrapper/Footer";
|
||||
import ErrorMessage from "../components/error/ErrorMessage";
|
||||
import Paragraph from "../components/paragraph/Paragraph";
|
||||
import Headline1 from "../components/headline/Headline1";
|
||||
import Link from "../components/link/Link";
|
||||
|
||||
interface Props {
|
||||
emailAddress: string;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
const AccountNotFoundPage = ({
|
||||
emailAddress,
|
||||
onBack,
|
||||
}: Props) => {
|
||||
const { t } = useContext(TranslateContext);
|
||||
|
||||
const onBackClick = (event: Event) => {
|
||||
event.preventDefault();
|
||||
onBack();
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Content>
|
||||
<Headline1>{t("headlines.accountNotFound")}</Headline1>
|
||||
<Paragraph>{t("texts.noAccountExists", { emailAddress })}</Paragraph>
|
||||
</Content>
|
||||
<Footer>
|
||||
<span hidden />
|
||||
<Link onClick={onBackClick}>
|
||||
{t("labels.back")}
|
||||
</Link>
|
||||
</Footer>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountNotFoundPage;
|
||||
@ -37,6 +37,7 @@ import LoginPasswordPage from "./LoginPasswordPage";
|
||||
import RegisterPasskeyPage from "./RegisterPasskeyPage";
|
||||
import RegisterPasswordPage from "./RegisterPasswordPage";
|
||||
import ErrorPage from "./ErrorPage";
|
||||
import AccountNotFoundPage from "./AccountNotFoundPage";
|
||||
|
||||
interface Props {
|
||||
emailAddress?: string;
|
||||
@ -199,6 +200,15 @@ const LoginEmailPage = (props: Props) => {
|
||||
]
|
||||
);
|
||||
|
||||
const renderAccountNotFound = useCallback(
|
||||
() => setPage(<AccountNotFoundPage emailAddress={emailAddress} onBack={onBackHandler}/>),
|
||||
[
|
||||
emailAddress,
|
||||
onBackHandler,
|
||||
setPage
|
||||
]
|
||||
);
|
||||
|
||||
const loginWithEmailAndWebAuthn = () => {
|
||||
let _userInfo: UserInfo;
|
||||
let _webauthnFinalizedResponse: WebauthnFinalized;
|
||||
@ -237,10 +247,16 @@ const LoginEmailPage = (props: Props) => {
|
||||
})
|
||||
.catch((e) => {
|
||||
if (e instanceof NotFoundError) {
|
||||
|
||||
if (config.account.allow_signup) {
|
||||
renderRegistrationConfirm();
|
||||
return;
|
||||
}
|
||||
|
||||
renderAccountNotFound();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e instanceof WebauthnRequestCancelledError) {
|
||||
return renderAlternateLoginMethod(_userInfo);
|
||||
}
|
||||
@ -397,7 +413,7 @@ const LoginEmailPage = (props: Props) => {
|
||||
|
||||
return (
|
||||
<Content>
|
||||
<Headline1>{t("headlines.loginEmail")}</Headline1>
|
||||
<Headline1>{config.account.allow_signup ? t("headlines.loginEmail") : t("headlines.loginEmailNoSignup")}</Headline1>
|
||||
<ErrorMessage error={error} />
|
||||
<Form onSubmit={onEmailSubmit}>
|
||||
<Input
|
||||
|
||||
1
frontend/frontend-sdk/.gitignore
vendored
Normal file
1
frontend/frontend-sdk/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
coverage
|
||||
@ -83,6 +83,7 @@ export type {
|
||||
import {
|
||||
HankoError,
|
||||
ConflictError,
|
||||
ForbiddenError,
|
||||
EmailAddressAlreadyExistsError,
|
||||
InvalidPasswordError,
|
||||
InvalidPasscodeError,
|
||||
@ -103,6 +104,7 @@ import {
|
||||
export {
|
||||
HankoError,
|
||||
ConflictError,
|
||||
ForbiddenError,
|
||||
EmailAddressAlreadyExistsError,
|
||||
InvalidPasswordError,
|
||||
InvalidPasscodeError,
|
||||
|
||||
@ -29,9 +29,11 @@ export interface EmailConfig {
|
||||
* @category SDK
|
||||
* @subcategory DTO
|
||||
* @property {boolean} allow_deletion - Indicates the current user is allowed to delete the account.
|
||||
* @property {boolean} allow_signup - Indicates the current user is allowed to sign up.
|
||||
*/
|
||||
export interface AccountConfig {
|
||||
allow_deletion: boolean;
|
||||
allow_signup: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -221,6 +221,21 @@ class UnauthorizedError extends HankoError {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A 'ForbiddenError' occurs when the user is not allowed to perform the requested action.
|
||||
*
|
||||
* @category SDK
|
||||
* @subcategory Errors
|
||||
* @extends {HankoError}
|
||||
*/
|
||||
class ForbiddenError extends HankoError {
|
||||
// eslint-disable-next-line require-jsdoc
|
||||
constructor(cause?: Error) {
|
||||
super("Forbidden error", "forbidden", cause);
|
||||
Object.setPrototypeOf(this, ForbiddenError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A 'UserVerificationError' occurs when the user verification requirements
|
||||
* for a WebAuthn ceremony are not met.
|
||||
@ -306,6 +321,7 @@ export {
|
||||
NotFoundError,
|
||||
TooManyRequestsError,
|
||||
UnauthorizedError,
|
||||
ForbiddenError,
|
||||
UserVerificationError,
|
||||
MaxNumOfEmailAddressesReachedError,
|
||||
EmailAddressAlreadyExistsError,
|
||||
|
||||
@ -4,6 +4,7 @@ import {
|
||||
NotFoundError,
|
||||
TechnicalError,
|
||||
UnauthorizedError,
|
||||
ForbiddenError,
|
||||
} from "../Errors";
|
||||
import { Client } from "./Client";
|
||||
|
||||
@ -55,6 +56,8 @@ class UserClient extends Client {
|
||||
|
||||
if (response.status === 409) {
|
||||
throw new ConflictError();
|
||||
} if (response.status === 403) {
|
||||
throw new ForbiddenError();
|
||||
} else if (!response.ok) {
|
||||
throw new TechnicalError();
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import {
|
||||
ConflictError,
|
||||
NotFoundError,
|
||||
TechnicalError,
|
||||
ForbiddenError,
|
||||
UserClient,
|
||||
} from "../../../src";
|
||||
import { Response } from "../../../src/lib/client/HttpClient";
|
||||
@ -179,6 +180,15 @@ describe("UserClient.create()", () => {
|
||||
await expect(user).rejects.toThrow(ConflictError);
|
||||
});
|
||||
|
||||
it("should throw error when signup is disabled", async () => {
|
||||
const response = new Response(new XMLHttpRequest());
|
||||
response.status = 403;
|
||||
jest.spyOn(userClient.client, "post").mockResolvedValue(response);
|
||||
|
||||
const user = userClient.create(email);
|
||||
await expect(user).rejects.toThrow(ForbiddenError);
|
||||
});
|
||||
|
||||
it("should throw error if API response is not ok (no 2xx, no 4xx)", async () => {
|
||||
const response = new Response(new XMLHttpRequest());
|
||||
jest.spyOn(userClient.client, "post").mockResolvedValue(response);
|
||||
|
||||
Reference in New Issue
Block a user