diff --git a/src/app/components/RequiredSteps.tsx b/src/app/components/RequiredSteps.tsx
new file mode 100644
index 0000000..342171f
--- /dev/null
+++ b/src/app/components/RequiredSteps.tsx
@@ -0,0 +1,52 @@
+'use client';
+import { Grid } from '@mui/material';
+import { usePathname } from 'next/navigation';
+import React, { useEffect } from 'react';
+
+import { AddUserDialog } from './dialogs/AddUserDialog';
+import { useAuth } from '../context/AuthProvider';
+import { apiUser } from '../services/endpoints/user.class';
+
+export const RequiredSteps = ({ children }: { children: React.ReactNode }) => {
+ const { user, isAuthenticated } = useAuth();
+ const pathname = usePathname();
+
+ const [isCreateUserDialogOpened, setIsCreateUserDialogOpened] =
+ React.useState(false);
+ const [hasUser, setHasUser] = React.useState(false);
+ const isAnonymous = !isAuthenticated && pathname === '/viewer';
+
+ useEffect(() => {
+ const fetchUser = async (id: string) =>
+ apiUser
+ .getUser(id)
+ .then(({ data }) => {
+ setHasUser(true);
+ return data;
+ })
+ .catch(() => {
+ setHasUser(false);
+ setIsCreateUserDialogOpened(true);
+ });
+
+ if (user?.id) fetchUser(user.id);
+ }, []);
+
+ return (
+
+ {isAnonymous || hasUser ? (
+ children
+ ) : (
+ {
+ setIsCreateUserDialogOpened(false);
+ }}
+ callback={() => {
+ setHasUser(true);
+ }}
+ />
+ )}
+
+ );
+};
diff --git a/src/app/components/dialogs/AddUserDialog.tsx b/src/app/components/dialogs/AddUserDialog.tsx
new file mode 100644
index 0000000..0f5d2ba
--- /dev/null
+++ b/src/app/components/dialogs/AddUserDialog.tsx
@@ -0,0 +1,113 @@
+'use client';
+
+import { Send } from '@mui/icons-material';
+import { Dialog, DialogContent, Grid, Alert, TextField } from '@mui/material';
+import { FC, useEffect, useState } from 'react';
+import { useForm } from 'react-hook-form';
+
+import { useAuth } from '@/app/context/AuthProvider';
+import { apiUser } from '@/app/services/endpoints/user.class';
+import { CreateUserDto } from '@/domain/dtos/user';
+
+import { StyledButton } from '../StyledButton';
+import { StyledDialogActions } from '../StyledDialogActions';
+import { StyledDialogContent } from '../StyledDialogContent';
+import { StyledDialogTitle } from '../StyledDialogTitle';
+
+interface AddUserDialogProps {
+ open?: boolean;
+ onClose?: () => void;
+ callback?: () => void;
+}
+
+export const AddUserDialog: FC = ({
+ open,
+ onClose,
+ callback,
+}) => {
+ const { session } = useAuth();
+ const [hasError, setHasError] = useState(false);
+ const [disableButton, setDisableButton] = useState(false);
+
+ const {
+ reset,
+ register,
+ formState: { errors },
+ resetField,
+ handleSubmit,
+ } = useForm();
+
+ const onSubmitForm = async (data: CreateUserDto) => {
+ setDisableButton(true);
+ try {
+ await apiUser
+ .createUser(data)
+ .then(() => {
+ onClose?.();
+ callback?.();
+ })
+ .catch(() => {
+ setHasError(true);
+ setDisableButton(false);
+ });
+ } catch (error) {
+ console.error('Failed to create user:', error);
+ }
+ };
+
+ useEffect(() => {
+ if (open) {
+ reset();
+ setHasError(false);
+ setDisableButton(false);
+ }
+ }, [open]);
+
+ useEffect(() => {
+ if (session?.token?.sub)
+ resetField('userId', { defaultValue: session.token.sub });
+ }, [session?.token?.sub]);
+
+ return (
+
+ );
+};
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index f615d33..43d6fe5 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,12 +1,13 @@
-import { Grid } from '@mui/material';
import { ThemeProvider } from '@mui/material/styles';
import { AppRouterCacheProvider } from '@mui/material-nextjs/v14-appRouter';
import type { Metadata } from 'next';
import { getServerSession } from 'next-auth';
+import React from 'react';
import { authOptions } from './api/auth/authOptions';
import Footer from './components/Footer';
import Header from './components/Header';
+import { RequiredSteps } from './components/RequiredSteps';
import AuthProvider from './context/AuthProvider';
import theme from './theme';
@@ -32,7 +33,7 @@ export default async function RootLayout({
- {children}
+ {children}
diff --git a/src/app/services/api.types.ts b/src/app/services/api.types.ts
index c078911..9102386 100644
--- a/src/app/services/api.types.ts
+++ b/src/app/services/api.types.ts
@@ -11,6 +11,7 @@ import type {
CreateSHLinkEndpointDto,
} from '@/domain/dtos/shlink-endpoint';
import { SHLinkQRCodeRequestDto } from '@/domain/dtos/shlink-qrcode';
+import { CreateUserDto, UserDto } from '@/domain/dtos/user';
import { type TBundle } from '@/types/fhir.types';
export interface IApi {
@@ -87,6 +88,15 @@ export interface IPathMapTypes {
update: never;
delete: never;
};
+ [EPath.users]: {
+ create: {
+ req: CreateUserDto;
+ res: UserDto;
+ };
+ read: UserDto;
+ update: never;
+ delete: never;
+ };
[EPath.viewer]: {
create: {
req: SHLinkRequestDto;
diff --git a/src/app/services/endpoints/user.class.ts b/src/app/services/endpoints/user.class.ts
new file mode 100644
index 0000000..38b9ae9
--- /dev/null
+++ b/src/app/services/endpoints/user.class.ts
@@ -0,0 +1,34 @@
+import { type AxiosInstance } from 'axios';
+
+import { BaseApi, instance } from '../api.class';
+import {
+ EPath,
+ type IPathMapTypes,
+ type TBaseApiProps,
+ type TPathToOperations,
+} from '../api.types';
+
+export class User<
+ TPath extends keyof IPathMapTypes,
+ TOperations extends TBaseApiProps = TPathToOperations,
+> extends BaseApi {
+ constructor(protected readonly instance: AxiosInstance) {
+ super(instance);
+ }
+
+ async createUser(data: TOperations['create']['req']) {
+ return await this.create({
+ url: `/${EPath.users}`,
+ data,
+ });
+ }
+
+ async getUser(userId: string) {
+ return await this.get({
+ url: `/${EPath.users}/${userId}`,
+ });
+ }
+}
+
+export const createApiUser = () => new User(instance);
+export const apiUser = createApiUser();