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",
|
"created_on": "Created On",
|
||||||
"copy_invite_link": "Copy Invite Link",
|
"copy_invite_link": "Copy Invite Link",
|
||||||
"copy_link": "Copy Link",
|
"copy_link": "Copy Link",
|
||||||
|
"confirm_password": "Confirm Password",
|
||||||
|
"create_local_user": "Create Local User",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"delete_user": "Delete User",
|
"delete_user": "Delete User",
|
||||||
|
|
@ -498,6 +500,8 @@
|
||||||
"last_active_on": "Last Active",
|
"last_active_on": "Last Active",
|
||||||
"load_info_error": "Unable to load user info",
|
"load_info_error": "Unable to load user info",
|
||||||
"load_list_error": "Unable to Load Users List",
|
"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",
|
"make_admin": "Make Admin",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"new_user_added": "New User Added",
|
"new_user_added": "New User Added",
|
||||||
|
|
@ -508,6 +512,9 @@
|
||||||
"not_found": "User not found",
|
"not_found": "User not found",
|
||||||
"pending_invites": "Pending Invites",
|
"pending_invites": "Pending Invites",
|
||||||
"pending_invites_description": "Manage and track pending user invitations with clear status and actions.",
|
"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_privilege": "Remove Admin Privilege",
|
||||||
"remove_admin_status": "Remove Admin Status",
|
"remove_admin_status": "Remove Admin Status",
|
||||||
"rename": "Rename",
|
"rename": "Rename",
|
||||||
|
|
@ -517,6 +524,9 @@
|
||||||
"show_more": "Show more",
|
"show_more": "Show more",
|
||||||
"uid": "UID",
|
"uid": "UID",
|
||||||
"unnamed": "(Unnamed User)",
|
"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!!",
|
"user_not_found": "User not found in the infra!!",
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"valid_email": "Please enter a valid email address"
|
"valid_email": "Please enter a valid email address"
|
||||||
|
|
|
||||||
|
|
@ -20,5 +20,17 @@
|
||||||
"password": "Mot de passe",
|
"password": "Mot de passe",
|
||||||
"sign_in": "Se connecter",
|
"sign_in": "Se connecter",
|
||||||
"username": "Identifiant"
|
"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();
|
await setInitialUser();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
createLocalUser: async (
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
displayName?: string,
|
||||||
|
) => {
|
||||||
|
const res = await authQuery.createLocalUser(
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
displayName,
|
||||||
|
);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
|
||||||
performAuthRefresh: async () => {
|
performAuthRefresh: async () => {
|
||||||
const isRefreshSuccess = await refreshToken();
|
const isRefreshSuccess = await refreshToken();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,12 @@ export default {
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
}),
|
}),
|
||||||
|
createLocalUser: (username: string, password: string, displayName?: string) =>
|
||||||
|
restApi.post('/auth/local/users', {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
displayName,
|
||||||
|
}),
|
||||||
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>) =>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,12 @@
|
||||||
@click="showInviteUserModal = true"
|
@click="showInviteUserModal = true"
|
||||||
:icon="IconAddUser"
|
:icon="IconAddUser"
|
||||||
/>
|
/>
|
||||||
|
<HoppButtonPrimary
|
||||||
|
v-if="localAuthEnabled"
|
||||||
|
:label="t('users.create_local_user')"
|
||||||
|
@click="showCreateLocalUserModal = true"
|
||||||
|
:icon="IconKeyRound"
|
||||||
|
/>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
outline
|
outline
|
||||||
|
|
@ -224,6 +230,54 @@
|
||||||
@hide-modal="inviteSuccessModal = false"
|
@hide-modal="inviteSuccessModal = false"
|
||||||
@copy-invite-link="copyInviteLink"
|
@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
|
<HoppSmartConfirmModal
|
||||||
:show="confirmUsersToAdmin"
|
:show="confirmUsersToAdmin"
|
||||||
:title="
|
:title="
|
||||||
|
|
@ -261,11 +315,12 @@
|
||||||
import { useMutation, useQuery } from '@urql/vue';
|
import { useMutation, useQuery } from '@urql/vue';
|
||||||
import { useTimeAgo } from '@vueuse/core';
|
import { useTimeAgo } from '@vueuse/core';
|
||||||
import { format } from 'date-fns';
|
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 { useRouter } from 'vue-router';
|
||||||
import { useI18n } from '~/composables/i18n';
|
import { useI18n } from '~/composables/i18n';
|
||||||
import { useToast } from '~/composables/toast';
|
import { useToast } from '~/composables/toast';
|
||||||
import { usePagedQuery } from '~/composables/usePagedQuery';
|
import { usePagedQuery } from '~/composables/usePagedQuery';
|
||||||
|
import { auth } from '~/helpers/auth';
|
||||||
import {
|
import {
|
||||||
DemoteUsersByAdminDocument,
|
DemoteUsersByAdminDocument,
|
||||||
InviteNewUserDocument,
|
InviteNewUserDocument,
|
||||||
|
|
@ -289,11 +344,17 @@ import IconUserCheck from '~icons/lucide/user-check';
|
||||||
import IconUserMinus from '~icons/lucide/user-minus';
|
import IconUserMinus from '~icons/lucide/user-minus';
|
||||||
import IconAddUser from '~icons/lucide/user-plus';
|
import IconAddUser from '~icons/lucide/user-plus';
|
||||||
import IconX from '~icons/lucide/x';
|
import IconX from '~icons/lucide/x';
|
||||||
|
import IconKeyRound from '~icons/lucide/key-round';
|
||||||
|
|
||||||
// Get Proper Date Formats
|
// Get Proper Date Formats
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
localAuthEnabled.value =
|
||||||
|
(await auth.getAllowedAuthProviders())?.includes('LOCAL') ?? false;
|
||||||
|
});
|
||||||
|
|
||||||
// Time and Date Helpers
|
// Time and Date Helpers
|
||||||
const getCreatedDate = (date: string) => format(new Date(date), 'dd-MMMM-yyyy');
|
const getCreatedDate = (date: string) => format(new Date(date), 'dd-MMMM-yyyy');
|
||||||
const getCreatedTime = (date: string) => format(new Date(date), 'hh:mm a');
|
const getCreatedTime = (date: string) => format(new Date(date), 'hh:mm a');
|
||||||
|
|
@ -321,14 +382,14 @@ const {
|
||||||
UsersListV2Document,
|
UsersListV2Document,
|
||||||
(x) => x.infra.allUsersV2,
|
(x) => x.infra.allUsersV2,
|
||||||
usersPerPage,
|
usersPerPage,
|
||||||
{ searchString: '', take: usersPerPage, skip: 0 }
|
{ searchString: '', take: usersPerPage, skip: 0 },
|
||||||
);
|
);
|
||||||
|
|
||||||
// Selected Rows
|
// Selected Rows
|
||||||
const selectedRows = ref<UsersListQuery['infra']['allUsers']>([]);
|
const selectedRows = ref<UsersListQuery['infra']['allUsers']>([]);
|
||||||
|
|
||||||
const areAdminsSelected = computed(() =>
|
const areAdminsSelected = computed(() =>
|
||||||
selectedRows.value.some((user) => user.isAdmin)
|
selectedRows.value.some((user) => user.isAdmin),
|
||||||
);
|
);
|
||||||
|
|
||||||
const areNonAdminsSelected = computed(() => {
|
const areNonAdminsSelected = computed(() => {
|
||||||
|
|
@ -396,9 +457,9 @@ const finalUsersList = computed(() =>
|
||||||
searchQuery.value.length > 0
|
searchQuery.value.length > 0
|
||||||
? usersList.value.slice(
|
? usersList.value.slice(
|
||||||
(page.value - 1) * usersPerPage,
|
(page.value - 1) * usersPerPage,
|
||||||
page.value * usersPerPage
|
page.value * usersPerPage,
|
||||||
)
|
)
|
||||||
: usersList.value
|
: usersList.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Spinner
|
// Spinner
|
||||||
|
|
@ -481,6 +542,69 @@ const copyInviteLink = () => {
|
||||||
// Send Invitation through Email
|
// Send Invitation through Email
|
||||||
const showInviteUserModal = ref(false);
|
const showInviteUserModal = ref(false);
|
||||||
const sendInvitation = useMutation(InviteNewUserDocument);
|
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 sendInvite = async (email: string) => {
|
||||||
const trimmedEmail = email.trim();
|
const trimmedEmail = email.trim();
|
||||||
|
|
@ -538,13 +662,13 @@ const makeUsersToAdmin = async (id: string | null) => {
|
||||||
toast.error(
|
toast.error(
|
||||||
areMultipleUsersSelected.value
|
areMultipleUsersSelected.value
|
||||||
? t('state.users_to_admin_failure')
|
? t('state.users_to_admin_failure')
|
||||||
: t('state.admin_failure')
|
: t('state.admin_failure'),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
toast.success(
|
toast.success(
|
||||||
areMultipleUsersSelected.value
|
areMultipleUsersSelected.value
|
||||||
? t('state.users_to_admin_success')
|
? t('state.users_to_admin_success')
|
||||||
: t('state.admin_success')
|
: t('state.admin_success'),
|
||||||
);
|
);
|
||||||
usersList.value = usersList.value.map((user) => ({
|
usersList.value = usersList.value.map((user) => ({
|
||||||
...user,
|
...user,
|
||||||
|
|
@ -573,7 +697,7 @@ const resetConfirmAdminToUser = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const areMultipleUsersSelectedToAdmin = computed(
|
const areMultipleUsersSelectedToAdmin = computed(
|
||||||
() => selectedRows.value.length > 1
|
() => selectedRows.value.length > 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
const makeAdminsToUsers = async (id: string | null) => {
|
const makeAdminsToUsers = async (id: string | null) => {
|
||||||
|
|
@ -591,13 +715,13 @@ const makeAdminsToUsers = async (id: string | null) => {
|
||||||
toast.error(
|
toast.error(
|
||||||
areMultipleUsersSelected.value
|
areMultipleUsersSelected.value
|
||||||
? t('state.remove_admin_from_users_failure')
|
? t('state.remove_admin_from_users_failure')
|
||||||
: t('state.remove_admin_failure')
|
: t('state.remove_admin_failure'),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
toast.success(
|
toast.success(
|
||||||
areMultipleUsersSelected.value
|
areMultipleUsersSelected.value
|
||||||
? t('state.remove_admin_from_users_success')
|
? t('state.remove_admin_from_users_success')
|
||||||
: t('state.remove_admin_success')
|
: t('state.remove_admin_success'),
|
||||||
);
|
);
|
||||||
usersList.value = usersList.value.map((user) => ({
|
usersList.value = usersList.value.map((user) => ({
|
||||||
...user,
|
...user,
|
||||||
|
|
@ -627,7 +751,7 @@ const resetConfirmUserDeletion = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const areMultipleUsersSelectedForDeletion = computed(
|
const areMultipleUsersSelectedForDeletion = computed(
|
||||||
() => selectedRows.value.length > 1
|
() => selectedRows.value.length > 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
const deleteUsers = async (id: string | null) => {
|
const deleteUsers = async (id: string | null) => {
|
||||||
|
|
@ -649,7 +773,7 @@ const deleteUsers = async (id: string | null) => {
|
||||||
handleUserDeletion(deletedUsers);
|
handleUserDeletion(deletedUsers);
|
||||||
|
|
||||||
usersList.value = usersList.value.filter(
|
usersList.value = usersList.value.filter(
|
||||||
(user) => !deletedUserIDs.includes(user.uid)
|
(user) => !deletedUserIDs.includes(user.uid),
|
||||||
);
|
);
|
||||||
|
|
||||||
selectedRows.value.splice(0, selectedRows.value.length);
|
selectedRows.value.splice(0, selectedRows.value.length);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue