Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions web/src/i18n/en/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,9 @@ Licensing information: [https://docs.defguard.net/enterprise/license](https://do
},
form: {
submit: 'Add user',
error: {
emailReserved: 'Email already taken',
},
fields: {
username: {
placeholder: 'login',
Expand Down Expand Up @@ -1991,19 +1994,22 @@ Licensing information: [https://docs.defguard.net/enterprise/license](https://do
'By default, all users will be allowed to connect to this location. If you want to restrict access to this location to a specific group, please select it below.',
aclFeatureDisabled:
"ACL functionality is an enterprise feature and you've exceeded the user, device or network limits to use it. In order to use this feature, purchase an enterprise license or upgrade your existing one.",
peerDisconnectThreshold: 'Clients authorized with MFA will be disconnected from the location once there has been no network activity detected between them and the VPN gateway for a length of time configured below.',
peerDisconnectThreshold:
'Clients authorized with MFA will be disconnected from the location once there has been no network activity detected between them and the VPN gateway for a length of time configured below.',
locationMfaMode: {
description: 'Choose how MFA is enforced when connecting to this location:',
internal: "Internal MFA - MFA is enforced using Defguard's built-in MFA (e.g. TOTP, WebAuthn) with internal identity",
external: 'External MFA - If configured (see [OpenID settings](settings)) this option uses external identity provider for MFA',
description: 'Choose how MFA is enforced when connecting to this location:',
internal:
"Internal MFA - MFA is enforced using Defguard's built-in MFA (e.g. TOTP, WebAuthn) with internal identity",
external:
'External MFA - If configured (see [OpenID settings](settings)) this option uses external identity provider for MFA',
},
},
sections: {
accessControl: {
header: 'Access Control & Firewall'
header: 'Access Control & Firewall',
},
mfa: {
header: 'Multi-Factor Authentication'
header: 'Multi-Factor Authentication',
},
},
messages: {
Expand Down Expand Up @@ -2047,7 +2053,7 @@ Licensing information: [https://docs.defguard.net/enterprise/license](https://do
},
location_mfa_mode: {
label: 'MFA requirement',
}
},
},
controls: {
submit: 'Save changes',
Expand Down
12 changes: 12 additions & 0 deletions web/src/i18n/i18n-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1229,6 +1229,12 @@ type RootTranslation = {
* A​d​d​ ​u​s​e​r
*/
submit: string
error: {
/**
* E​m​a​i​l​ ​a​l​r​e​a​d​y​ ​t​a​k​e​n
*/
emailReserved: string
}
fields: {
username: {
/**
Expand Down Expand Up @@ -7852,6 +7858,12 @@ export type TranslationFunctions = {
* Add user
*/
submit: () => LocalizedString
error: {
/**
* Email already taken
*/
emailReserved: () => LocalizedString
}
fields: {
username: {
/**
Expand Down
10 changes: 5 additions & 5 deletions web/src/pages/auth/Login/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import type { AxiosError } from 'axios';
import { useMemo } from 'react';
import { type SubmitHandler, useForm } from 'react-hook-form';
import { z } from 'zod';

import { useI18nContext } from '../../../i18n/i18n-react';
import { FormInput } from '../../../shared/defguard-ui/components/Form/FormInput/FormInput';
import { Button } from '../../../shared/defguard-ui/components/Layout/Button/Button';
Expand All @@ -15,6 +14,7 @@ import {
ButtonStyleVariant,
} from '../../../shared/defguard-ui/components/Layout/Button/types';
import { LoaderSpinner } from '../../../shared/defguard-ui/components/Layout/LoaderSpinner/LoaderSpinner';
import { isPresent } from '../../../shared/defguard-ui/utils/isPresent';
import { useAppStore } from '../../../shared/hooks/store/useAppStore';
import { useAuthStore } from '../../../shared/hooks/store/useAuthStore';
import useApi from '../../../shared/hooks/useApi';
Expand All @@ -41,6 +41,7 @@ export const Login = () => {
const toaster = useToaster();

const enterpriseEnabled = useAppStore((s) => s.appInfo?.license_info.enterprise);

const { data: openIdInfo, isLoading: openIdLoading } = useQuery({
enabled: enterpriseEnabled,
queryKey: [QueryKeys.FETCH_OPENID_INFO],
Expand Down Expand Up @@ -82,8 +83,9 @@ export const Login = () => {
mutationKey: [MutationKeys.LOG_IN],
onSuccess: (data) => loginSubject.next(data),
onError: (error: AxiosError) => {
if (error.response) {
switch (error.response.status) {
const status = error.response?.status;
if (isPresent(status)) {
switch (status) {
case 401: {
setError(
'password',
Expand All @@ -99,12 +101,10 @@ export const Login = () => {
break;
}
default: {
console.error(error);
toaster.error(LL.messages.error());
}
}
} else {
console.error(error);
toaster.error(LL.messages.error());
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import './style.scss';

import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import parse from 'html-react-parser';
import { omit } from 'lodash-es';
import { useMemo, useRef, useState } from 'react';
Expand All @@ -17,6 +17,7 @@ import {
ButtonSize,
ButtonStyleVariant,
} from '../../../../../../../shared/defguard-ui/components/Layout/Button/types';
import { isPresent } from '../../../../../../../shared/defguard-ui/utils/isPresent';
import { useAppStore } from '../../../../../../../shared/hooks/store/useAppStore';
import { useEnterpriseUpgradeStore } from '../../../../../../../shared/hooks/store/useEnterpriseUpgradeStore';
import useApi from '../../../../../../../shared/hooks/useApi';
Expand All @@ -34,12 +35,21 @@ import { useAddUserModal } from '../../hooks/useAddUserModal';
export const AddUserForm = () => {
const { LL } = useI18nContext();
const {
user: { addUser, usernameAvailable },
user: { addUser, usernameAvailable, getUsers },
getAppInfo,
} = useApi();

const reservedUserNames = useRef<string[]>([]);

const { data: userEmails, isLoading: emailsLoading } = useQuery({
queryKey: ['user'],
queryFn: getUsers,
refetchOnWindowFocus: false,
refetchOnMount: true,
select: (users) => users.map((user) => user.email),
placeholderData: (perv) => perv,
});

const [checkingUsername, setCheckingUsername] = useState(false);

const zodSchema = useMemo(
Expand All @@ -55,8 +65,15 @@ export const AddUserForm = () => {
password: z.string(),
email: z
.string()
.trim()
.min(1, LL.form.error.required())
.email(LL.form.error.invalid()),
.email(LL.form.error.invalid())
.refine((value) => {
if (isPresent(userEmails)) {
return !userEmails.includes(value.toLowerCase());
}
return true;
}, LL.modals.addUser.form.error.emailReserved()),
last_name: z.string().min(1, LL.form.error.required()),
first_name: z.string().min(1, LL.form.error.required()),
phone: z.string(),
Expand Down Expand Up @@ -97,7 +114,7 @@ export const AddUserForm = () => {
});
}
}),
[LL],
[LL, userEmails],
);

type FormFields = z.infer<typeof zodSchema>;
Expand Down Expand Up @@ -164,10 +181,8 @@ export const AddUserForm = () => {
close();
}
},
onError: (err) => {
close();
onError: () => {
toaster.error(LL.messages.error());
console.error(err);
},
});

Expand Down Expand Up @@ -277,7 +292,7 @@ export const AddUserForm = () => {
styleVariant={ButtonStyleVariant.PRIMARY}
text={LL.modals.addUser.form.submit()}
disabled={!isValid}
loading={addUserMutation.isPending || checkingUsername}
loading={addUserMutation.isPending || checkingUsername || emailsLoading}
/>
</div>
</form>
Expand Down
5 changes: 2 additions & 3 deletions web/src/shared/hooks/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,8 @@ import type { UpdateInfo } from '../store/useUpdatesStore';
const unpackRequest = <T>(res: AxiosResponse<T>): T => res.data;

export const buildApi = (client: Axios): Api => {
const addUser = (data: AddUserRequest) => {
return client.post<User>(`/user`, data).then(unpackRequest);
};
const addUser = (data: AddUserRequest) =>
client.post<User>(`/user`, data).then(unpackRequest);

const getMe = () => client.get<User>(`/me`).then(unpackRequest);

Expand Down
5 changes: 2 additions & 3 deletions web/src/shared/hooks/api/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,21 @@ const ApiContextManager = ({ children }: PropsWithChildren) => {
if (client && LL && LL.messages) {
const defaultResponseInterceptor = client.interceptors.response.use(
(res) => {
// API sometimes returns null in optional fields.
// API returns null in optional fields.
if (res.data) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
res.data = removeNulls(res.data);
}
return res;
},
(error) => {
console.error('Axios Error ', error);
throw error;
},
);
return () => {
client.interceptors.response.eject(defaultResponseInterceptor);
};
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [LL?.messages, client]);

if (!client || !endpoints) return null;
Expand Down