diff --git a/next-ui/src/mocks/api/handlers.ts b/next-ui/src/mocks/api/handlers.ts
index 6292f56e..836750ad 100644
--- a/next-ui/src/mocks/api/handlers.ts
+++ b/next-ui/src/mocks/api/handlers.ts
@@ -6,15 +6,17 @@ import { librariesHandlers } from '@/mocks/api/handlers/libraries'
import { referentialHandlers } from '@/mocks/api/handlers/referential'
import { usersHandlers } from '@/mocks/api/handlers/users'
import { settingsHandlers } from '@/mocks/api/handlers/settings'
+import { claimHandlers } from '@/mocks/api/handlers/claim'
export const handlers = [
- ...librariesHandlers,
- ...referentialHandlers,
...actuatorHandlers,
...announcementHandlers,
+ ...claimHandlers,
+ ...librariesHandlers,
+ ...referentialHandlers,
...releasesHandlers,
- ...usersHandlers,
...settingsHandlers,
+ ...usersHandlers,
]
export const response401Unauthorized = () =>
diff --git a/next-ui/src/mocks/api/handlers/claim.ts b/next-ui/src/mocks/api/handlers/claim.ts
new file mode 100644
index 00000000..9a82b188
--- /dev/null
+++ b/next-ui/src/mocks/api/handlers/claim.ts
@@ -0,0 +1,5 @@
+import { httpTyped } from '@/mocks/api/httpTyped'
+
+export const claimHandlers = [
+ httpTyped.get('/api/v1/claim', ({ response }) => response(200).json({ isClaimed: true })),
+]
diff --git a/next-ui/src/pages/claim.mdx b/next-ui/src/pages/claim.mdx
new file mode 100644
index 00000000..281f4d2a
--- /dev/null
+++ b/next-ui/src/pages/claim.mdx
@@ -0,0 +1,15 @@
+import { Canvas, Meta } from '@storybook/addon-docs/blocks';
+
+import * as Stories from './claim.stories';
+
+
+
+# Claim
+
+If the server is unclaimed, this page will be used instead of the login page. This is how they differ:
+- information banner displayed at the top
+- password confirmation is required
+- remember me is not shown
+- OAuth2 login is not shown
+
+
diff --git a/next-ui/src/pages/claim.stories.ts b/next-ui/src/pages/claim.stories.ts
new file mode 100644
index 00000000..d6f51b58
--- /dev/null
+++ b/next-ui/src/pages/claim.stories.ts
@@ -0,0 +1,112 @@
+import type { Meta, StoryObj } from '@storybook/vue3-vite'
+
+import claim from './claim.vue'
+import { http, delay } from 'msw'
+
+import { response502BadGateway } from '@/mocks/api/handlers'
+import { expect, waitFor } from 'storybook/test'
+import { useMessagesStore } from '@/stores/messages'
+import { httpTyped } from '@/mocks/api/httpTyped'
+
+const meta = {
+ component: claim,
+ render: (args: object) => ({
+ components: { claim },
+ setup() {
+ return { args }
+ },
+ template: '',
+ }),
+ parameters: {
+ // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
+ msw: {
+ handlers: [
+ httpTyped.get('/api/v1/claim', ({ response }) => response(200).json({ isClaimed: false })),
+ ],
+ },
+ },
+ args: {},
+} satisfies Meta
+
+export default meta
+type Story = StoryObj
+
+export const Default: Story = {
+ args: {},
+}
+
+export const Invalid: Story = {
+ play: async ({ canvas, userEvent }) => {
+ const login = canvas.getByLabelText(/email/i, {
+ selector: 'input',
+ })
+ await userEvent.type(login, 'test@example.org')
+
+ const password1 = canvas.getByLabelText(/^password/i, {
+ selector: 'input',
+ })
+ await userEvent.type(password1, 'abc')
+
+ const password2 = canvas.getByLabelText(/confirm password/i, {
+ selector: 'input',
+ })
+ await userEvent.type(password2, 'def')
+
+ await userEvent.click(canvas.getByRole('button', { name: /create/i }))
+ },
+}
+
+export const Loading: Story = {
+ parameters: {
+ msw: {
+ handlers: [http.all('*', async () => await delay(5_000))],
+ },
+ },
+ play: async ({ canvas, userEvent }) => {
+ const login = canvas.getByLabelText(/email/i, {
+ selector: 'input',
+ })
+ await userEvent.type(login, 'test@example.org')
+
+ const password1 = canvas.getByLabelText(/^password/i, {
+ selector: 'input',
+ })
+ await userEvent.type(password1, 'abc')
+
+ const password2 = canvas.getByLabelText(/confirm password/i, {
+ selector: 'input',
+ })
+ await userEvent.type(password2, 'abc')
+
+ await userEvent.click(canvas.getByRole('button', { name: /create/i }))
+ },
+}
+
+export const Error: Story = {
+ parameters: {
+ msw: {
+ handlers: [http.all('*', response502BadGateway)],
+ },
+ },
+ play: async ({ canvas, userEvent }) => {
+ const login = canvas.getByLabelText(/email/i, {
+ selector: 'input',
+ })
+ await userEvent.type(login, 'test@example.org')
+
+ const password1 = canvas.getByLabelText(/^password/i, {
+ selector: 'input',
+ })
+ await userEvent.type(password1, 'abc')
+
+ const password2 = canvas.getByLabelText(/confirm password/i, {
+ selector: 'input',
+ })
+ await userEvent.type(password2, 'abc')
+
+ await userEvent.click(canvas.getByRole('button', { name: /create/i }))
+
+ const messagesStore = useMessagesStore()
+ await waitFor(() => expect(messagesStore.messages.length).toBe(1))
+ },
+}