feat: add local auth onboarding
This commit is contained in:
parent
60cf156230
commit
4c30592ae4
8 changed files with 233 additions and 14 deletions
|
|
@ -112,6 +112,40 @@ describe('LocalAuthService', () => {
|
||||||
expect(result).toEqualRight(tokens);
|
expect(result).toEqualRight(tokens);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('reuses an existing first local admin before onboarding is completed', async () => {
|
||||||
|
mockPrisma.infraConfig.findUnique.mockResolvedValue({
|
||||||
|
id: 'onboarding',
|
||||||
|
name: InfraConfigEnum.ONBOARDING_COMPLETED,
|
||||||
|
value: 'false',
|
||||||
|
lastSyncedEnvFileValue: null,
|
||||||
|
isEncrypted: false,
|
||||||
|
createdOn: currentTime,
|
||||||
|
updatedOn: currentTime,
|
||||||
|
});
|
||||||
|
mockPrisma.user.findFirst.mockResolvedValue({
|
||||||
|
...adminUser,
|
||||||
|
localCredential: {
|
||||||
|
id: 'credential-1',
|
||||||
|
userUid: adminUser.uid,
|
||||||
|
passwordHash: 'hashed-password',
|
||||||
|
createdOn: currentTime,
|
||||||
|
updatedOn: currentTime,
|
||||||
|
},
|
||||||
|
} as any);
|
||||||
|
jest.mocked(argon2.verify).mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await localAuthService.setupFirstAdmin({
|
||||||
|
username: 'Admin',
|
||||||
|
password: 'strong-password',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockPrisma.user.create).not.toHaveBeenCalled();
|
||||||
|
expect(mockAuthService.generateAuthTokens).toHaveBeenCalledWith(
|
||||||
|
adminUser.uid,
|
||||||
|
);
|
||||||
|
expect(result).toEqualRight(tokens);
|
||||||
|
});
|
||||||
|
|
||||||
it('refuses setup admin after onboarding is completed', async () => {
|
it('refuses setup admin after onboarding is completed', async () => {
|
||||||
mockPrisma.infraConfig.findUnique.mockResolvedValue({
|
mockPrisma.infraConfig.findUnique.mockResolvedValue({
|
||||||
id: 'onboarding',
|
id: 'onboarding',
|
||||||
|
|
|
||||||
|
|
@ -140,8 +140,25 @@ export class LocalAuthService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const username = this.normalizeUsername(dto.username);
|
const username = this.normalizeUsername(dto.username);
|
||||||
const usernameAvailable = await this.ensureUsernameAvailable(username);
|
const existingUser = await this.findUserByUsername(username);
|
||||||
if (E.isLeft(usernameAvailable)) return usernameAvailable;
|
|
||||||
|
if (existingUser) {
|
||||||
|
if (existingUser.isAdmin && existingUser.localCredential?.passwordHash) {
|
||||||
|
const passwordMatches = await argon2.verify(
|
||||||
|
existingUser.localCredential.passwordHash,
|
||||||
|
dto.password,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (passwordMatches) {
|
||||||
|
return this.authService.generateAuthTokens(existingUser.uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return E.left(<RESTError>{
|
||||||
|
message: AUTH_USERNAME_ALREADY_EXISTS,
|
||||||
|
statusCode: HttpStatus.CONFLICT,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const user = await this.createLocalUserRecord(
|
const user = await this.createLocalUserRecord(
|
||||||
username,
|
username,
|
||||||
|
|
|
||||||
|
|
@ -220,6 +220,19 @@
|
||||||
"description_accordian": "Select the OAuth providers you want to enable and provide the necessary configurations.",
|
"description_accordian": "Select the OAuth providers you want to enable and provide the necessary configurations.",
|
||||||
"title": "OAuth"
|
"title": "OAuth"
|
||||||
},
|
},
|
||||||
|
"local": {
|
||||||
|
"confirm_password": "Confirm password",
|
||||||
|
"description": "Use usernames and passwords without an external service.",
|
||||||
|
"description_accordian": "Create the first local administrator account.",
|
||||||
|
"password": "Password",
|
||||||
|
"password_min_length": "Password must be at least 12 characters.",
|
||||||
|
"password_mismatch": "Passwords do not match.",
|
||||||
|
"setup_failed": "Failed to create the local administrator account.",
|
||||||
|
"title": "Username and password",
|
||||||
|
"username": "Username",
|
||||||
|
"username_format": "Username can only contain letters, numbers, dots, dashes, and underscores.",
|
||||||
|
"username_min_length": "Username must be at least 3 characters."
|
||||||
|
},
|
||||||
"onboarding_incomplete": {
|
"onboarding_incomplete": {
|
||||||
"description": "You have not completed the onboarding process. Please set up at least one authentication provider to continue.",
|
"description": "You have not completed the onboarding process. Please set up at least one authentication provider to continue.",
|
||||||
"title": "Onboarding Incomplete"
|
"title": "Onboarding Incomplete"
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
{
|
{
|
||||||
|
"onboarding": {
|
||||||
|
"local": {
|
||||||
|
"confirm_password": "Confirmer le mot de passe",
|
||||||
|
"description": "Utiliser des identifiants sans service externe.",
|
||||||
|
"description_accordian": "Créer le premier compte administrateur local.",
|
||||||
|
"password": "Mot de passe",
|
||||||
|
"password_min_length": "Le mot de passe doit contenir au moins 12 caractères.",
|
||||||
|
"password_mismatch": "Les mots de passe ne correspondent pas.",
|
||||||
|
"setup_failed": "Impossible de créer le compte administrateur local.",
|
||||||
|
"title": "Identifiant et mot de passe",
|
||||||
|
"username": "Identifiant",
|
||||||
|
"username_format": "L’identifiant ne peut contenir que des lettres, chiffres, points, tirets et underscores.",
|
||||||
|
"username_min_length": "L’identifiant doit contenir au moins 3 caractères."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,17 @@
|
||||||
class="relative inline-block h-6 w-6 rounded-full border-2 border-primary object-cover object-center hover:z-10 focus:z-10"
|
class="relative inline-block h-6 w-6 rounded-full border-2 border-primary object-cover object-center hover:z-10 focus:z-10"
|
||||||
/>
|
/>
|
||||||
</AuthProviderCard>
|
</AuthProviderCard>
|
||||||
|
|
||||||
|
<AuthProviderCard
|
||||||
|
title="onboarding.local.title"
|
||||||
|
description="onboarding.local.description"
|
||||||
|
:selected="isSelected('LOCAL') && isLocalEnabled"
|
||||||
|
@click="toggleSelectedOption('LOCAL')"
|
||||||
|
>
|
||||||
|
<IconLucideKeyRound
|
||||||
|
class="relative inline-block h-6 w-6 rounded-full border-2 border-primary bg-primaryDark p-1 text-accent hover:z-10 focus:z-10"
|
||||||
|
/>
|
||||||
|
</AuthProviderCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<HoppButtonPrimary
|
<HoppButtonPrimary
|
||||||
|
|
@ -115,6 +126,41 @@
|
||||||
v-model:enabledConfigs="enabledConfigs"
|
v-model:enabledConfigs="enabledConfigs"
|
||||||
/>
|
/>
|
||||||
</UiAccordion>
|
</UiAccordion>
|
||||||
|
|
||||||
|
<UiAccordion
|
||||||
|
v-if="isSelected('LOCAL') && isFirstTimeSetup"
|
||||||
|
:initial-open="isLocalEnabled"
|
||||||
|
title="onboarding.local.title"
|
||||||
|
description="onboarding.local.description_accordian"
|
||||||
|
@toggle="toggleConfig('LOCAL')"
|
||||||
|
class="bg-primary rounded-lg border-primaryDark shadow p-4 border flex flex-col"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col space-y-2 py-4">
|
||||||
|
<HoppSmartInput
|
||||||
|
v-model="localAdminCredentials.username"
|
||||||
|
:label="t('onboarding.local.username')"
|
||||||
|
input-styles="floating-input"
|
||||||
|
:autofocus="false"
|
||||||
|
class="!my-2 !bg-primaryLight flex-1"
|
||||||
|
/>
|
||||||
|
<HoppSmartInput
|
||||||
|
v-model="localAdminCredentials.password"
|
||||||
|
:label="t('onboarding.local.password')"
|
||||||
|
input-styles="floating-input"
|
||||||
|
:autofocus="false"
|
||||||
|
type="password"
|
||||||
|
class="!my-2 !bg-primaryLight flex-1"
|
||||||
|
/>
|
||||||
|
<HoppSmartInput
|
||||||
|
v-model="localAdminCredentials.confirmPassword"
|
||||||
|
:label="t('onboarding.local.confirm_password')"
|
||||||
|
input-styles="floating-input"
|
||||||
|
:autofocus="false"
|
||||||
|
type="password"
|
||||||
|
class="!my-2 !bg-primaryLight flex-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</UiAccordion>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center space-x-4 mt-6">
|
<div class="flex items-center space-x-4 mt-6">
|
||||||
|
|
@ -163,6 +209,7 @@ import SmtpSetup from './SmtpSetup.vue';
|
||||||
import IconLucideArrowRight from '~icons/lucide/arrow-right';
|
import IconLucideArrowRight from '~icons/lucide/arrow-right';
|
||||||
import IconLucideArrowLeft from '~icons/lucide/arrow-left';
|
import IconLucideArrowLeft from '~icons/lucide/arrow-left';
|
||||||
import IconLucideSave from '~icons/lucide/save';
|
import IconLucideSave from '~icons/lucide/save';
|
||||||
|
import IconLucideKeyRound from '~icons/lucide/key-round';
|
||||||
import AuthProviderCard from './AuthProviderCard.vue';
|
import AuthProviderCard from './AuthProviderCard.vue';
|
||||||
|
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
@ -176,7 +223,7 @@ const emit = defineEmits<{
|
||||||
): void;
|
): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
type SelectedOption = 'OAUTH' | 'SMTP';
|
type SelectedOption = 'OAUTH' | 'SMTP' | 'LOCAL';
|
||||||
|
|
||||||
const authConfigStep = ref(1);
|
const authConfigStep = ref(1);
|
||||||
const selectedOptions = ref<SelectedOption[]>([]);
|
const selectedOptions = ref<SelectedOption[]>([]);
|
||||||
|
|
@ -184,6 +231,7 @@ const selectedOptions = ref<SelectedOption[]>([]);
|
||||||
const {
|
const {
|
||||||
currentConfigs,
|
currentConfigs,
|
||||||
enabledConfigs,
|
enabledConfigs,
|
||||||
|
localAdminCredentials,
|
||||||
isProvidersLoading,
|
isProvidersLoading,
|
||||||
submittingConfigs,
|
submittingConfigs,
|
||||||
onBoardingSummary,
|
onBoardingSummary,
|
||||||
|
|
@ -240,6 +288,7 @@ const isSmtpEnabled = computed(
|
||||||
enabledConfigs.value.includes('MAILER') ||
|
enabledConfigs.value.includes('MAILER') ||
|
||||||
enabledConfigs.value.includes('EMAIL'),
|
enabledConfigs.value.includes('EMAIL'),
|
||||||
);
|
);
|
||||||
|
const isLocalEnabled = computed(() => enabledConfigs.value.includes('LOCAL'));
|
||||||
|
|
||||||
const updateOAuthEnabled = () => {
|
const updateOAuthEnabled = () => {
|
||||||
isOAuthEnabled.value = OAuthProviders.some((provider) =>
|
isOAuthEnabled.value = OAuthProviders.some((provider) =>
|
||||||
|
|
@ -263,6 +312,10 @@ const toggleSelectedOption = (option: SelectedOption) => {
|
||||||
toggleConfig('EMAIL');
|
toggleConfig('EMAIL');
|
||||||
toggleSmtpConfig();
|
toggleSmtpConfig();
|
||||||
}
|
}
|
||||||
|
} else if (option === 'LOCAL') {
|
||||||
|
if (willBeSelected !== isLocalEnabled.value) {
|
||||||
|
toggleConfig('LOCAL');
|
||||||
|
}
|
||||||
} else if (willBeSelected !== isOAuthEnabled.value) {
|
} else if (willBeSelected !== isOAuthEnabled.value) {
|
||||||
toggleConfig(option);
|
toggleConfig(option);
|
||||||
}
|
}
|
||||||
|
|
@ -277,7 +330,10 @@ const proceedToConfig = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitConfigs = async () => {
|
const submitConfigs = async () => {
|
||||||
const res = await addOnBoardingConfigs();
|
const res = await addOnBoardingConfigs({
|
||||||
|
setupLocalAdmin:
|
||||||
|
props.isFirstTimeSetup === true && enabledConfigs.value.includes('LOCAL'),
|
||||||
|
});
|
||||||
if (res?.token) {
|
if (res?.token) {
|
||||||
emit('complete-onboarding', {
|
emit('complete-onboarding', {
|
||||||
submittingConfigs: submittingConfigs.value,
|
submittingConfigs: submittingConfigs.value,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,18 @@ import { getLocalConfig, setLocalConfig } from '~/helpers/localpersistence';
|
||||||
import { makeReadableKey } from '~/helpers/utils/readableKey';
|
import { makeReadableKey } from '~/helpers/utils/readableKey';
|
||||||
|
|
||||||
export type OAuthProvider = 'GOOGLE' | 'GITHUB' | 'MICROSOFT';
|
export type OAuthProvider = 'GOOGLE' | 'GITHUB' | 'MICROSOFT';
|
||||||
export type EnabledConfig = OAuthProvider | 'OAUTH' | 'MAILER' | 'EMAIL';
|
export type EnabledConfig =
|
||||||
|
| OAuthProvider
|
||||||
|
| 'OAUTH'
|
||||||
|
| 'MAILER'
|
||||||
|
| 'EMAIL'
|
||||||
|
| 'LOCAL';
|
||||||
|
|
||||||
|
export type LocalAdminCredentials = {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
confirmPassword: string;
|
||||||
|
};
|
||||||
|
|
||||||
// common OAuth keys used across providers
|
// common OAuth keys used across providers
|
||||||
type OAuthKeys = 'CLIENT_ID' | 'CLIENT_SECRET' | 'CALLBACK_URL' | 'SCOPE';
|
type OAuthKeys = 'CLIENT_ID' | 'CLIENT_SECRET' | 'CALLBACK_URL' | 'SCOPE';
|
||||||
|
|
@ -124,6 +135,11 @@ export function useOnboardingConfigHandler() {
|
||||||
const enabledConfigs = ref<EnabledConfig[]>([]);
|
const enabledConfigs = ref<EnabledConfig[]>([]);
|
||||||
const isProvidersLoading = ref(false);
|
const isProvidersLoading = ref(false);
|
||||||
const submittingConfigs = ref(false);
|
const submittingConfigs = ref(false);
|
||||||
|
const localAdminCredentials = ref<LocalAdminCredentials>({
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
});
|
||||||
|
|
||||||
const onBoardingSummary = ref<OnBoardingSummary>({
|
const onBoardingSummary = ref<OnBoardingSummary>({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
|
|
@ -334,6 +350,36 @@ export function useOnboardingConfigHandler() {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const validateLocalAdminCredentials = () => {
|
||||||
|
if (!enabledConfigs.value.includes('LOCAL')) return true;
|
||||||
|
|
||||||
|
const username = localAdminCredentials.value.username.trim();
|
||||||
|
const password = localAdminCredentials.value.password;
|
||||||
|
const confirmPassword = localAdminCredentials.value.confirmPassword;
|
||||||
|
|
||||||
|
if (username.length < 3) {
|
||||||
|
toast.error(t('onboarding.local.username_min_length'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^[a-zA-Z0-9_.-]+$/.test(username)) {
|
||||||
|
toast.error(t('onboarding.local.username_format'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 12) {
|
||||||
|
toast.error(t('onboarding.local.password_min_length'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
toast.error(t('onboarding.local.password_mismatch'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds the onboarding configs to the backend.
|
* Adds the onboarding configs to the backend.
|
||||||
* It validates the configs, prepares the payload,
|
* It validates the configs, prepares the payload,
|
||||||
|
|
@ -341,7 +387,9 @@ export function useOnboardingConfigHandler() {
|
||||||
* We set the token in localStorage for re-fetching configs later.
|
* We set the token in localStorage for re-fetching configs later.
|
||||||
* @returns The token for re-fetching configs or undefined if failed
|
* @returns The token for re-fetching configs or undefined if failed
|
||||||
*/
|
*/
|
||||||
const addOnBoardingConfigs = async () => {
|
const addOnBoardingConfigs = async (options?: {
|
||||||
|
setupLocalAdmin?: boolean;
|
||||||
|
}) => {
|
||||||
submittingConfigs.value = true;
|
submittingConfigs.value = true;
|
||||||
const payload = {
|
const payload = {
|
||||||
...currentConfigs.value.oAuthProviders.GOOGLE,
|
...currentConfigs.value.oAuthProviders.GOOGLE,
|
||||||
|
|
@ -350,25 +398,46 @@ export function useOnboardingConfigHandler() {
|
||||||
...currentConfigs.value.mailerConfigs,
|
...currentConfigs.value.mailerConfigs,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const filteredEnabledConfigs = enabledConfigs.value.filter(
|
||||||
|
(config) => config !== 'OAUTH' && config !== 'MAILER',
|
||||||
|
);
|
||||||
|
const hasLocalAuth = filteredEnabledConfigs.includes('LOCAL');
|
||||||
const validated = validateConfigs(payload);
|
const validated = validateConfigs(payload);
|
||||||
|
|
||||||
if (!validated || Object.keys(validated).length === 0) {
|
if (!validated) {
|
||||||
|
submittingConfigs.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(validated).length === 0 && !hasLocalAuth) {
|
||||||
toast.error(t('onboarding.add_atleast_one_auth_provider'));
|
toast.error(t('onboarding.add_atleast_one_auth_provider'));
|
||||||
submittingConfigs.value = false;
|
submittingConfigs.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredEnabledConfigs = enabledConfigs.value.filter(
|
if (options?.setupLocalAdmin && !validateLocalAdminCredentials()) {
|
||||||
(config) => config !== 'OAUTH' && config !== 'MAILER',
|
submittingConfigs.value = false;
|
||||||
);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const configWithAuth = {
|
const configWithAuth = {
|
||||||
...validated,
|
...(validated ?? {}),
|
||||||
[InfraConfigEnum.ViteAllowedAuthProviders]:
|
[InfraConfigEnum.ViteAllowedAuthProviders]:
|
||||||
filteredEnabledConfigs.join(','),
|
filteredEnabledConfigs.join(','),
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (hasLocalAuth && options?.setupLocalAdmin) {
|
||||||
|
const isLocalAdminCreated = await auth.setupLocalAdmin(
|
||||||
|
localAdminCredentials.value.username.trim(),
|
||||||
|
localAdminCredentials.value.password,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isLocalAdminCreated) {
|
||||||
|
throw new Error(t('onboarding.local.setup_failed'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const res = await auth.addOnBoardingConfigs(configWithAuth);
|
const res = await auth.addOnBoardingConfigs(configWithAuth);
|
||||||
if (res?.token) {
|
if (res?.token) {
|
||||||
setLocalConfig('access_token', res.token);
|
setLocalConfig('access_token', res.token);
|
||||||
|
|
@ -453,6 +522,7 @@ export function useOnboardingConfigHandler() {
|
||||||
return {
|
return {
|
||||||
currentConfigs,
|
currentConfigs,
|
||||||
enabledConfigs,
|
enabledConfigs,
|
||||||
|
localAdminCredentials,
|
||||||
isProvidersLoading,
|
isProvidersLoading,
|
||||||
onBoardingSummary,
|
onBoardingSummary,
|
||||||
submittingConfigs,
|
submittingConfigs,
|
||||||
|
|
|
||||||
|
|
@ -212,6 +212,16 @@ export const auth = {
|
||||||
return authQuery.signInWithEmailLink(token, deviceIdentifier);
|
return authQuery.signInWithEmailLink(token, deviceIdentifier);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setupLocalAdmin: async (username: string, password: string) => {
|
||||||
|
try {
|
||||||
|
await authQuery.setupLocalAdmin(username, password);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
performAuthRefresh: async () => {
|
performAuthRefresh: async () => {
|
||||||
const isRefreshSuccess = await refreshToken();
|
const isRefreshSuccess = await refreshToken();
|
||||||
|
|
||||||
|
|
@ -233,7 +243,7 @@ export const auth = {
|
||||||
|
|
||||||
if (!deviceIdentifier) {
|
if (!deviceIdentifier) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Device Identifier not found, you can only signin from the browser you generated the magic link'
|
'Device Identifier not found, you can only signin from the browser you generated the magic link',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,12 +23,17 @@ export default {
|
||||||
}),
|
}),
|
||||||
signInWithEmailLink: (
|
signInWithEmailLink: (
|
||||||
token: string | null,
|
token: string | null,
|
||||||
deviceIdentifier: string | null
|
deviceIdentifier: string | null,
|
||||||
) =>
|
) =>
|
||||||
restApi.post('/auth/verify', {
|
restApi.post('/auth/verify', {
|
||||||
token,
|
token,
|
||||||
deviceIdentifier,
|
deviceIdentifier,
|
||||||
}),
|
}),
|
||||||
|
setupLocalAdmin: (username: string, password: string) =>
|
||||||
|
restApi.post('/auth/local/setup-admin', {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
}),
|
||||||
getFirstTimeInfraSetupStatus: () => restApi.get('/site/setup'),
|
getFirstTimeInfraSetupStatus: () => restApi.get('/site/setup'),
|
||||||
updateFirstTimeInfraSetupStatus: () => restApi.put('/site/setup'),
|
updateFirstTimeInfraSetupStatus: () => restApi.put('/site/setup'),
|
||||||
addOnBoardingConfigs: (config: Record<string, any>) =>
|
addOnBoardingConfigs: (config: Record<string, any>) =>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue