feat: add local user creation
This commit is contained in:
parent
892f69817b
commit
d4bbde7deb
5 changed files with 177 additions and 12 deletions
|
|
@ -477,6 +477,8 @@
|
|||
"created_on": "Created On",
|
||||
"copy_invite_link": "Copy Invite Link",
|
||||
"copy_link": "Copy Link",
|
||||
"confirm_password": "Confirm Password",
|
||||
"create_local_user": "Create Local User",
|
||||
"date": "Date",
|
||||
"delete": "Delete",
|
||||
"delete_user": "Delete User",
|
||||
|
|
@ -498,6 +500,8 @@
|
|||
"last_active_on": "Last Active",
|
||||
"load_info_error": "Unable to load user info",
|
||||
"load_list_error": "Unable to Load Users List",
|
||||
"local_user_create_failed": "Failed to create local user",
|
||||
"local_user_created": "Local user created",
|
||||
"make_admin": "Make Admin",
|
||||
"name": "Name",
|
||||
"new_user_added": "New User Added",
|
||||
|
|
@ -508,6 +512,9 @@
|
|||
"not_found": "User not found",
|
||||
"pending_invites": "Pending Invites",
|
||||
"pending_invites_description": "Manage and track pending user invitations with clear status and actions.",
|
||||
"password": "Password",
|
||||
"password_min_length": "Password must be at least 12 characters.",
|
||||
"password_mismatch": "Passwords do not match.",
|
||||
"remove_admin_privilege": "Remove Admin Privilege",
|
||||
"remove_admin_status": "Remove Admin Status",
|
||||
"rename": "Rename",
|
||||
|
|
@ -517,6 +524,9 @@
|
|||
"show_more": "Show more",
|
||||
"uid": "UID",
|
||||
"unnamed": "(Unnamed User)",
|
||||
"username": "Username",
|
||||
"username_format": "Username can only contain letters, numbers, dots, dashes, and underscores.",
|
||||
"username_min_length": "Username must be at least 3 characters.",
|
||||
"user_not_found": "User not found in the infra!!",
|
||||
"users": "Users",
|
||||
"valid_email": "Please enter a valid email address"
|
||||
|
|
|
|||
|
|
@ -20,5 +20,17 @@
|
|||
"password": "Mot de passe",
|
||||
"sign_in": "Se connecter",
|
||||
"username": "Identifiant"
|
||||
},
|
||||
"users": {
|
||||
"confirm_password": "Confirmer le mot de passe",
|
||||
"create_local_user": "Créer un utilisateur local",
|
||||
"local_user_create_failed": "Impossible de créer l’utilisateur local",
|
||||
"local_user_created": "Utilisateur local créé",
|
||||
"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.",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -227,6 +227,19 @@ export const auth = {
|
|||
await setInitialUser();
|
||||
},
|
||||
|
||||
createLocalUser: async (
|
||||
username: string,
|
||||
password: string,
|
||||
displayName?: string,
|
||||
) => {
|
||||
const res = await authQuery.createLocalUser(
|
||||
username,
|
||||
password,
|
||||
displayName,
|
||||
);
|
||||
return res.data;
|
||||
},
|
||||
|
||||
performAuthRefresh: async () => {
|
||||
const isRefreshSuccess = await refreshToken();
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,12 @@ export default {
|
|||
username,
|
||||
password,
|
||||
}),
|
||||
createLocalUser: (username: string, password: string, displayName?: string) =>
|
||||
restApi.post('/auth/local/users', {
|
||||
username,
|
||||
password,
|
||||
displayName,
|
||||
}),
|
||||
getFirstTimeInfraSetupStatus: () => restApi.get('/site/setup'),
|
||||
updateFirstTimeInfraSetupStatus: () => restApi.put('/site/setup'),
|
||||
addOnBoardingConfigs: (config: Record<string, any>) =>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,12 @@
|
|||
@click="showInviteUserModal = true"
|
||||
:icon="IconAddUser"
|
||||
/>
|
||||
<HoppButtonPrimary
|
||||
v-if="localAuthEnabled"
|
||||
:label="t('users.create_local_user')"
|
||||
@click="showCreateLocalUserModal = true"
|
||||
:icon="IconKeyRound"
|
||||
/>
|
||||
<div class="flex">
|
||||
<HoppButtonSecondary
|
||||
outline
|
||||
|
|
@ -224,6 +230,54 @@
|
|||
@hide-modal="inviteSuccessModal = false"
|
||||
@copy-invite-link="copyInviteLink"
|
||||
/>
|
||||
<HoppSmartModal
|
||||
v-if="showCreateLocalUserModal"
|
||||
dialog
|
||||
:title="t('users.create_local_user')"
|
||||
@close="hideCreateLocalUserModal"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col space-y-4">
|
||||
<HoppSmartInput
|
||||
v-model="localUserForm.displayName"
|
||||
:label="t('users.name')"
|
||||
input-styles="floating-input"
|
||||
/>
|
||||
<HoppSmartInput
|
||||
v-model="localUserForm.username"
|
||||
:label="t('users.username')"
|
||||
input-styles="floating-input"
|
||||
/>
|
||||
<HoppSmartInput
|
||||
v-model="localUserForm.password"
|
||||
:label="t('users.password')"
|
||||
input-styles="floating-input"
|
||||
type="password"
|
||||
/>
|
||||
<HoppSmartInput
|
||||
v-model="localUserForm.confirmPassword"
|
||||
:label="t('users.confirm_password')"
|
||||
input-styles="floating-input"
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex justify-end space-x-2">
|
||||
<HoppButtonSecondary
|
||||
:label="t('users.cancel')"
|
||||
outline
|
||||
filled
|
||||
@click="hideCreateLocalUserModal"
|
||||
/>
|
||||
<HoppButtonPrimary
|
||||
:label="t('users.create_local_user')"
|
||||
:loading="creatingLocalUser"
|
||||
@click="createLocalUser"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
<HoppSmartConfirmModal
|
||||
:show="confirmUsersToAdmin"
|
||||
:title="
|
||||
|
|
@ -261,11 +315,12 @@
|
|||
import { useMutation, useQuery } from '@urql/vue';
|
||||
import { useTimeAgo } from '@vueuse/core';
|
||||
import { format } from 'date-fns';
|
||||
import { computed, onUnmounted, ref, watch } from 'vue';
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
import { useToast } from '~/composables/toast';
|
||||
import { usePagedQuery } from '~/composables/usePagedQuery';
|
||||
import { auth } from '~/helpers/auth';
|
||||
import {
|
||||
DemoteUsersByAdminDocument,
|
||||
InviteNewUserDocument,
|
||||
|
|
@ -289,11 +344,17 @@ import IconUserCheck from '~icons/lucide/user-check';
|
|||
import IconUserMinus from '~icons/lucide/user-minus';
|
||||
import IconAddUser from '~icons/lucide/user-plus';
|
||||
import IconX from '~icons/lucide/x';
|
||||
import IconKeyRound from '~icons/lucide/key-round';
|
||||
|
||||
// Get Proper Date Formats
|
||||
const t = useI18n();
|
||||
const toast = useToast();
|
||||
|
||||
onMounted(async () => {
|
||||
localAuthEnabled.value =
|
||||
(await auth.getAllowedAuthProviders())?.includes('LOCAL') ?? false;
|
||||
});
|
||||
|
||||
// Time and Date Helpers
|
||||
const getCreatedDate = (date: string) => format(new Date(date), 'dd-MMMM-yyyy');
|
||||
const getCreatedTime = (date: string) => format(new Date(date), 'hh:mm a');
|
||||
|
|
@ -321,14 +382,14 @@ const {
|
|||
UsersListV2Document,
|
||||
(x) => x.infra.allUsersV2,
|
||||
usersPerPage,
|
||||
{ searchString: '', take: usersPerPage, skip: 0 }
|
||||
{ searchString: '', take: usersPerPage, skip: 0 },
|
||||
);
|
||||
|
||||
// Selected Rows
|
||||
const selectedRows = ref<UsersListQuery['infra']['allUsers']>([]);
|
||||
|
||||
const areAdminsSelected = computed(() =>
|
||||
selectedRows.value.some((user) => user.isAdmin)
|
||||
selectedRows.value.some((user) => user.isAdmin),
|
||||
);
|
||||
|
||||
const areNonAdminsSelected = computed(() => {
|
||||
|
|
@ -396,9 +457,9 @@ const finalUsersList = computed(() =>
|
|||
searchQuery.value.length > 0
|
||||
? usersList.value.slice(
|
||||
(page.value - 1) * usersPerPage,
|
||||
page.value * usersPerPage
|
||||
page.value * usersPerPage,
|
||||
)
|
||||
: usersList.value
|
||||
: usersList.value,
|
||||
);
|
||||
|
||||
// Spinner
|
||||
|
|
@ -481,6 +542,69 @@ const copyInviteLink = () => {
|
|||
// Send Invitation through Email
|
||||
const showInviteUserModal = ref(false);
|
||||
const sendInvitation = useMutation(InviteNewUserDocument);
|
||||
const localAuthEnabled = ref(false);
|
||||
const showCreateLocalUserModal = ref(false);
|
||||
const creatingLocalUser = ref(false);
|
||||
const localUserForm = ref({
|
||||
displayName: '',
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
});
|
||||
|
||||
const hideCreateLocalUserModal = () => {
|
||||
showCreateLocalUserModal.value = false;
|
||||
localUserForm.value = {
|
||||
displayName: '',
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
};
|
||||
};
|
||||
|
||||
const createLocalUser = async () => {
|
||||
const username = localUserForm.value.username.trim();
|
||||
const password = localUserForm.value.password;
|
||||
const displayName = localUserForm.value.displayName.trim();
|
||||
|
||||
if (username.length < 3) {
|
||||
toast.error(t('users.username_min_length'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/^[a-zA-Z0-9_.-]+$/.test(username)) {
|
||||
toast.error(t('users.username_format'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 12) {
|
||||
toast.error(t('users.password_min_length'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (password !== localUserForm.value.confirmPassword) {
|
||||
toast.error(t('users.password_mismatch'));
|
||||
return;
|
||||
}
|
||||
|
||||
creatingLocalUser.value = true;
|
||||
|
||||
try {
|
||||
await auth.createLocalUser(username, password, displayName || undefined);
|
||||
toast.success(t('users.local_user_created'));
|
||||
hideCreateLocalUserModal();
|
||||
await refetch({
|
||||
searchString: searchQuery.value,
|
||||
take: usersPerPage,
|
||||
skip: (page.value - 1) * usersPerPage,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error(t('users.local_user_create_failed'));
|
||||
} finally {
|
||||
creatingLocalUser.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const sendInvite = async (email: string) => {
|
||||
const trimmedEmail = email.trim();
|
||||
|
|
@ -538,13 +662,13 @@ const makeUsersToAdmin = async (id: string | null) => {
|
|||
toast.error(
|
||||
areMultipleUsersSelected.value
|
||||
? t('state.users_to_admin_failure')
|
||||
: t('state.admin_failure')
|
||||
: t('state.admin_failure'),
|
||||
);
|
||||
} else {
|
||||
toast.success(
|
||||
areMultipleUsersSelected.value
|
||||
? t('state.users_to_admin_success')
|
||||
: t('state.admin_success')
|
||||
: t('state.admin_success'),
|
||||
);
|
||||
usersList.value = usersList.value.map((user) => ({
|
||||
...user,
|
||||
|
|
@ -573,7 +697,7 @@ const resetConfirmAdminToUser = () => {
|
|||
};
|
||||
|
||||
const areMultipleUsersSelectedToAdmin = computed(
|
||||
() => selectedRows.value.length > 1
|
||||
() => selectedRows.value.length > 1,
|
||||
);
|
||||
|
||||
const makeAdminsToUsers = async (id: string | null) => {
|
||||
|
|
@ -591,13 +715,13 @@ const makeAdminsToUsers = async (id: string | null) => {
|
|||
toast.error(
|
||||
areMultipleUsersSelected.value
|
||||
? t('state.remove_admin_from_users_failure')
|
||||
: t('state.remove_admin_failure')
|
||||
: t('state.remove_admin_failure'),
|
||||
);
|
||||
} else {
|
||||
toast.success(
|
||||
areMultipleUsersSelected.value
|
||||
? t('state.remove_admin_from_users_success')
|
||||
: t('state.remove_admin_success')
|
||||
: t('state.remove_admin_success'),
|
||||
);
|
||||
usersList.value = usersList.value.map((user) => ({
|
||||
...user,
|
||||
|
|
@ -627,7 +751,7 @@ const resetConfirmUserDeletion = () => {
|
|||
};
|
||||
|
||||
const areMultipleUsersSelectedForDeletion = computed(
|
||||
() => selectedRows.value.length > 1
|
||||
() => selectedRows.value.length > 1,
|
||||
);
|
||||
|
||||
const deleteUsers = async (id: string | null) => {
|
||||
|
|
@ -649,7 +773,7 @@ const deleteUsers = async (id: string | null) => {
|
|||
handleUserDeletion(deletedUsers);
|
||||
|
||||
usersList.value = usersList.value.filter(
|
||||
(user) => !deletedUserIDs.includes(user.uid)
|
||||
(user) => !deletedUserIDs.includes(user.uid),
|
||||
);
|
||||
|
||||
selectedRows.value.splice(0, selectedRows.value.length);
|
||||
|
|
|
|||
Loading…
Reference in a new issue