Skip to content

Commit

Permalink
Merge pull request #121 from DashHub-ai/feature/add-users-groups
Browse files Browse the repository at this point in the history
Add users groups
  • Loading branch information
Mati365 authored Dec 29, 2024
2 parents a2b33b2 + e8b780a commit d6ffa45
Show file tree
Hide file tree
Showing 172 changed files with 4,039 additions and 344 deletions.
1 change: 0 additions & 1 deletion apps/admin/src/helpers/index.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import {

import type { SdkCreateUserAuthMethodsT } from '@llm/sdk';

import { genRandomPassword } from '@llm/commons';
import { Checkbox, FormField, Input } from '@llm/ui';
import { genRandomPassword } from '~/helpers';
import { useI18n } from '~/i18n';

type Props = ValidationErrorsListProps<SdkCreateUserAuthMethodsT>;
Expand Down
2 changes: 1 addition & 1 deletion apps/admin/src/modules/users/form/update/fields/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from './user-organization-info-form-field';
export * from './user-organization-settings-form-field';
export * from './user-update-auth-methods-form-field';

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {
controlled,
useFormValidatorMessages,
type ValidationErrorsListProps,
} from '@under-control/forms';

import type { SdkTableRowWithIdNameT, SdkUpdateUserOrganizationInputT } from '@llm/sdk';

import { FormField } from '@llm/ui';
import { useI18n } from '~/i18n';
import { OrganizationsSearchSelect, UserOrganizationRoleSelect } from '~/modules/organizations';

type Props = ValidationErrorsListProps<SdkUpdateUserOrganizationInputT> & {
organization: SdkTableRowWithIdNameT;
};

export const UserOrganizationSettingsFormField = controlled<SdkUpdateUserOrganizationInputT, Props>((
{
organization,
errors,
control: { bind },
},
) => {
const t = useI18n().pack.modules.users.form.fields.organization;
const validation = useFormValidatorMessages({ errors });

return (
<>
<FormField
className="uk-margin"
label={t.choose.label}
>
<OrganizationsSearchSelect
defaultValue={organization}
disabled
/>
</FormField>

<FormField
className="uk-margin"
label={t.role.label}
{...validation.extract('role')}
>
<UserOrganizationRoleSelect {...bind.path('role')} required />
</FormField>

<hr />
</>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { useMemo } from 'react';

import type { SdkUpdateUserAuthMethodsT } from '@llm/sdk';

import { genRandomPassword } from '@llm/commons';
import { Checkbox, FormField, Input } from '@llm/ui';
import { genRandomPassword } from '~/helpers';
import { useI18n } from '~/i18n';

type Props = ValidationErrorsListProps<SdkUpdateUserAuthMethodsT>;
Expand Down
8 changes: 8 additions & 0 deletions apps/admin/src/modules/users/form/update/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type {
SdkTableRowWithIdT,
SdkUpdateUserInputT,
} from '@llm/sdk';

export type UpdateUserFormValue =
SdkTableRowWithIdT &
SdkUpdateUserInputT;
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ import { type FormHookAttrs, useForm } from '@under-control/forms';
import { flow } from 'fp-ts/lib/function';

import { runTask, tapTaskEither } from '@llm/commons';
import { type SdkTableRowWithIdT, type SdkUpdateUserInputT, useSdkForLoggedIn } from '@llm/sdk';
import { useSdkForLoggedIn } from '@llm/sdk';
import { usePredefinedFormValidators, useSaveTaskEitherNotification } from '@llm/ui';

import type { UpdateUserFormValue } from './types';

import { useUseAuthFormValidator } from '../shared';

type UpdateUserFormHookAttrs =
& Omit<
FormHookAttrs<SdkUpdateUserInputT & SdkTableRowWithIdT>,
FormHookAttrs<UpdateUserFormValue>,
'validation' | 'onSubmit'
>
& {
Expand All @@ -23,9 +25,9 @@ export function useUserUpdateForm(
}: UpdateUserFormHookAttrs,
) {
const { sdks } = useSdkForLoggedIn();
const { emailFormatValidator } = usePredefinedFormValidators<SdkUpdateUserInputT & SdkTableRowWithIdT>();
const { emailFormatValidator } = usePredefinedFormValidators<UpdateUserFormValue>();
const saveNotifications = useSaveTaskEitherNotification();
const authValidator = useUseAuthFormValidator<SdkUpdateUserInputT & SdkTableRowWithIdT>();
const authValidator = useUseAuthFormValidator<UpdateUserFormValue>();

return useForm({
resetAfterSubmit: false,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useMemo } from 'react';

import type { SdkUserT } from '@llm/sdk';

import {
Expand All @@ -10,8 +12,10 @@ import {
} from '@llm/ui';
import { useI18n } from '~/i18n';

import type { UpdateUserFormValue } from './types';

import { UserSharedFormFields } from '../shared';
import { UserOrganizationInfoField, UserUpdateAuthMethodsFormField } from './fields';
import { UserOrganizationSettingsFormField, UserUpdateAuthMethodsFormField } from './fields';
import { useUserUpdateForm } from './use-user-update-form';

export type UserUpdateFormModalProps =
Expand All @@ -30,14 +34,32 @@ export function UserUpdateFormModal(
}: UserUpdateFormModalProps,
) {
const t = useI18n().pack.modules.users.form;
const { handleSubmitEvent, validator, submitState, bind } = useUserUpdateForm({
defaultValue: {

const defaultValue = useMemo<UpdateUserFormValue>(() => {
const attrs = {
id: user.id,
active: user.active,
archiveProtection: user.archiveProtection,
auth: user.auth,
email: user.email,
},
};

return (
user.role === 'user'
? {
...attrs,
role: 'user',
organization: user.organization,
}
: {
...attrs,
role: 'root',
}
);
}, [user]);

const { handleSubmitEvent, validator, submitState, bind } = useUserUpdateForm({
defaultValue,
onAfterSubmit,
});

Expand All @@ -61,9 +83,12 @@ export function UserUpdateFormModal(
)}
>
{user.role === 'user' && (
<UserOrganizationInfoField user={user} />
<UserOrganizationSettingsFormField
organization={user.organization}
{...validator.errors.extract('organization', { nested: true })}
{...bind.path('organization')}
/>
)}

<UserSharedFormFields
errors={validator.errors.all as unknown as any}
{...bind.merged()}
Expand Down
7 changes: 3 additions & 4 deletions apps/admin/src/modules/users/table/users-table-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import type { z } from 'zod';

import { pipe } from 'fp-ts/lib/function';

import { tapTaskOption } from '@llm/commons';
import { genRandomPassword, tapTaskOption } from '@llm/commons';
import { useAsyncCallback } from '@llm/commons-front';
import { SdkIdNameUrlEntryV, SdKSearchUsersInputV, serializeSdkIdNameUrlEntry, useSdkForLoggedIn } from '@llm/sdk';
import { SdkIdNameUrlEntryV, SdkSearchUsersInputV, serializeSdkIdNameUrlEntry, useSdkForLoggedIn } from '@llm/sdk';
import {
ArchiveFilterTabs,
CreateButton,
Expand All @@ -14,14 +14,13 @@ import {
ResetFiltersButton,
useDebouncedPaginatedSearch,
} from '@llm/ui';
import { genRandomPassword } from '~/helpers';
import { useI18n } from '~/i18n';
import { OrganizationsSearchSelect } from '~/modules/organizations';

import { useUserCreateModal } from '../form';
import { UsersTableRow } from './users-table-row';

const SearchUsersUrlFiltersV = SdKSearchUsersInputV
const SearchUsersUrlFiltersV = SdkSearchUsersInputV
.omit({
organizationIds: true,
})
Expand Down
60 changes: 60 additions & 0 deletions apps/backend/src/migrations/0026-add-users-groups-table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { type Kysely, sql } from 'kysely';

import { addArchivedAtColumns, addIdColumn, addTimestampColumns } from './utils';

export async function up(db: Kysely<any>): Promise<void> {
await db.schema
.createTable('users_groups')
.$call(addIdColumn)
.$call(addTimestampColumns)
.$call(addArchivedAtColumns)
.addColumn('organization_id', 'integer', col => col.notNull().references('organizations.id').onDelete('restrict'))
.addColumn('creator_user_id', 'integer', col => col.notNull().references('users.id').onDelete('restrict'))
.addColumn('name', 'text', col => col.notNull())
.execute();

await db.schema
.createTable('users_groups_users')
.addColumn('user_id', 'integer', col => col.notNull().references('users.id').onDelete('cascade'))
.addColumn('group_id', 'integer', col => col.notNull().references('users_groups.id').onDelete('cascade'))
.addUniqueConstraint('user_id_group_id_unique', ['user_id', 'group_id'])
.execute();

await db.schema
.createIndex('users_groups_organization_id_index')
.on('users_groups')
.column('organization_id')
.execute();

await sql`
CREATE OR REPLACE FUNCTION users_groups_users_check_organization() RETURNS TRIGGER AS $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM organizations_users ou
JOIN users_groups ug ON ug.organization_id = ou.organization_id
WHERE ou.user_id = NEW.user_id
AND ug.id = NEW.group_id
) THEN
RAISE EXCEPTION 'User must belong to the same organization as the group';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER users_groups_users_organization_check
BEFORE INSERT OR UPDATE ON users_groups_users
FOR EACH ROW
EXECUTE FUNCTION users_groups_users_check_organization();
`.execute(db);
}

export async function down(db: Kysely<any>): Promise<void> {
await sql`
DROP TRIGGER IF EXISTS users_groups_users_organization_check ON users_groups_users;
DROP FUNCTION IF EXISTS users_groups_users_check_organization();
`.execute(db);

await db.schema.dropTable('users_groups_users').execute();
await db.schema.dropTable('users_groups').execute();
}
Loading

0 comments on commit d6ffa45

Please sign in to comment.