Skip to content

Commit

Permalink
feat: space invitation setting (#679)
Browse files Browse the repository at this point in the history
* feat: add option to allow space invitation for admin setting

* chore: db migration

* chore: publish 1.2.1-beta.0 release

* fix: await missing

* fix: ci

* fix: isAdmin does not exist
  • Loading branch information
Sky-FE authored Jun 29, 2024
1 parent 2bf8027 commit 9e5bbb6
Show file tree
Hide file tree
Showing 38 changed files with 134 additions and 71 deletions.
2 changes: 1 addition & 1 deletion apps/nestjs-backend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@teable/backend",
"version": "1.2.0-beta.0",
"version": "1.2.1-beta.0",
"license": "AGPL-3.0",
"private": true,
"main": "dist/index.js",
Expand Down
2 changes: 1 addition & 1 deletion apps/nestjs-backend/src/features/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export class AuthService {
},
});
}
return await this.userService.createUser({
return await this.userService.createUserWithSettingCheck({
id: generateUserId(),
name: email.split('@')[0],
email,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export class AccessTokenStrategy extends PassportStrategy(PassportAccessTokenStr
this.cls.set('user.id', user.id);
this.cls.set('user.name', user.name);
this.cls.set('user.email', user.email);
this.cls.set('user.isAdmin', user.isAdmin);
this.cls.set('accessTokenId', accessTokenId);
return pickUserMe(user);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export class SessionStrategy extends PassportStrategy(PassportSessionStrategy) {
this.cls.set('user.id', user.id);
this.cls.set('user.name', user.name);
this.cls.set('user.email', user.email);
this.cls.set('user.isAdmin', user.isAdmin);
return pickUserMe(user);
}
}
15 changes: 15 additions & 0 deletions apps/nestjs-backend/src/features/invitation/invitation.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,21 @@ export class InvitationService {

async emailInvitationBySpace(spaceId: string, data: EmailSpaceInvitationRo) {
const user = this.cls.get('user');

if (!user?.isAdmin) {
const setting = await this.prismaService.setting.findFirst({
select: {
disallowSpaceInvitation: true,
},
});

if (setting?.disallowSpaceInvitation) {
throw new ForbiddenException(
'The current instance disallow space invitation by the administrator'
);
}
}

const space = await this.prismaService.space.findFirst({
select: { name: true },
where: { id: spaceId, deletedTime: null },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export class SettingService {
instanceId: true,
disallowSignUp: true,
disallowSpaceCreation: true,
disallowSpaceInvitation: true,
},
})
.catch(() => {
Expand Down
23 changes: 13 additions & 10 deletions apps/nestjs-backend/src/features/space/space.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,17 +98,20 @@ export class SpaceService {

async createSpace(createSpaceRo: ICreateSpaceRo) {
const userId = this.cls.get('user.id');
const setting = await this.prismaService.setting.findFirst({
select: {
disallowSignUp: true,
disallowSpaceCreation: true,
},
});
const isAdmin = this.cls.get('user.isAdmin');

if (setting?.disallowSpaceCreation) {
throw new ForbiddenException(
'The current instance disallow space creation by the administrator'
);
if (!isAdmin) {
const setting = await this.prismaService.setting.findFirst({
select: {
disallowSpaceCreation: true,
},
});

if (setting?.disallowSpaceCreation) {
throw new ForbiddenException(
'The current instance disallow space creation by the administrator'
);
}
}

const spaceList = await this.prismaService.space.findMany({
Expand Down
12 changes: 9 additions & 3 deletions apps/nestjs-backend/src/features/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,21 +78,27 @@ export class UserService {
return space;
}

async createUser(
async createUserWithSettingCheck(
user: Omit<Prisma.UserCreateInput, 'name'> & { name?: string },
account?: Omit<Prisma.AccountUncheckedCreateInput, 'userId'>
) {
const setting = await this.prismaService.setting.findFirst({
select: {
disallowSignUp: true,
disallowSpaceCreation: true,
},
});

if (setting?.disallowSignUp) {
throw new BadRequestException('The current instance disallow sign up by the administrator');
}

return await this.createUser(user, account);
}

async createUser(
user: Omit<Prisma.UserCreateInput, 'name'> & { name?: string },
account?: Omit<Prisma.AccountUncheckedCreateInput, 'userId'>
) {
// defaults
const defaultNotifyMeta: IUserNotifyMeta = {
email: true,
Expand Down Expand Up @@ -292,7 +298,7 @@ export class UserService {
if (avatarUrl) {
avatar = await this.uploadAvatarByUrl(userId, avatarUrl);
}
return await this.createUser(
return await this.createUserWithSettingCheck(
{ id: userId, email, name, avatar },
{ provider, providerId, type }
);
Expand Down
1 change: 1 addition & 0 deletions apps/nestjs-backend/src/types/cls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface IClsStore extends ClsStore {
id: string;
name: string;
email: string;
isAdmin?: boolean | null;
};
accessTokenId?: string;
entry?: {
Expand Down
2 changes: 1 addition & 1 deletion apps/nextjs-app/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@teable/app",
"version": "1.2.0-beta.0",
"version": "1.2.1-beta.0",
"license": "AGPL-3.0",
"private": true,
"main": "main/index.js",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const SettingPage = (props: ISettingPageProps) => {

if (!setting) return null;

const { instanceId, disallowSignUp, disallowSpaceCreation } = setting;
const { instanceId, disallowSignUp, disallowSpaceCreation, disallowSpaceInvitation } = setting;

return (
<div className="flex h-screen w-full flex-col overflow-y-auto overflow-x-hidden px-8 py-6">
Expand All @@ -42,7 +42,7 @@ export const SettingPage = (props: ISettingPageProps) => {
</div>

<div className="flex w-full flex-col space-y-4 py-4">
<div className="flex flex-row items-center justify-between rounded-lg border p-4 shadow-sm">
<div className="flex items-center justify-between space-x-2 rounded-lg border p-4 shadow-sm">
<div className="space-y-1">
<Label htmlFor="allow-sign-up">{t('admin.setting.allowSignUp')}</Label>
<div className="text-[13px] text-gray-500">
Expand All @@ -55,7 +55,20 @@ export const SettingPage = (props: ISettingPageProps) => {
onCheckedChange={(checked) => onCheckedChange('disallowSignUp', !checked)}
/>
</div>
<div className="flex flex-row items-center justify-between rounded-lg border p-4 shadow-sm">
<div className="flex items-center justify-between space-x-2 rounded-lg border p-4 shadow-sm">
<div className="space-y-1">
<Label htmlFor="allow-sign-up">{t('admin.setting.allowSpaceInvitation')}</Label>
<div className="text-[13px] text-gray-500">
{t('admin.setting.allowSpaceInvitationDescription')}
</div>
</div>
<Switch
id="allow-space-invitation"
checked={!disallowSpaceInvitation}
onCheckedChange={(checked) => onCheckedChange('disallowSpaceInvitation', !checked)}
/>
</div>
<div className="flex items-center justify-between space-x-2 rounded-lg border p-4 shadow-sm">
<div className="space-y-1">
<Label htmlFor="allow-space-creation">{t('admin.setting.allowSpaceCreation')}</Label>
<div className="text-[13px] text-gray-500">
Expand Down
4 changes: 3 additions & 1 deletion apps/nextjs-app/src/features/app/blocks/space/SpaceCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ interface ISpaceCard {
space: IGetSpaceVo;
bases?: IGetBaseVo[];
subscription?: ISubscriptionSummaryVo;
disallowSpaceInvitation?: boolean | null;
}
export const SpaceCard: FC<ISpaceCard> = (props) => {
const { space, bases, subscription } = props;
const { space, bases, subscription, disallowSpaceInvitation } = props;
const router = useRouter();
const isCloud = useIsCloud();
const queryClient = useQueryClient();
Expand Down Expand Up @@ -91,6 +92,7 @@ export const SpaceCard: FC<ISpaceCard> = (props) => {
buttonSize="xs"
space={space}
invQueryFilters={ReactQueryKeys.baseAll() as unknown as string[]}
disallowSpaceInvitation={disallowSpaceInvitation}
onDelete={() => deleteSpaceMutator(space.id)}
onRename={() => setRenaming(true)}
onSpaceSetting={onSpaceSetting}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Collaborators } from '../../components/collaborator-manage/space-inner/
import { SpaceActionBar } from '../../components/space/SpaceActionBar';
import { SpaceRenaming } from '../../components/space/SpaceRenaming';
import { useIsCloud } from '../../hooks/useIsCloud';
import { useSetting } from '../../hooks/useSetting';
import { DraggableBaseGrid } from './DraggableBaseGrid';
import { StarButton } from './space-side-bar/StarButton';
import { useBaseList } from './useBaseList';
Expand All @@ -38,6 +39,8 @@ export const SpaceInnerPage: React.FC = () => {

const bases = useBaseList();

const { disallowSpaceInvitation } = useSetting();

const basesInSpace = useMemo(() => {
return bases?.filter((base) => base.spaceId === spaceId);
}, [bases, spaceId]);
Expand Down Expand Up @@ -124,6 +127,7 @@ export const SpaceInnerPage: React.FC = () => {
space={space}
buttonSize={'xs'}
invQueryFilters={ReactQueryKeys.baseAll() as unknown as string[]}
disallowSpaceInvitation={disallowSpaceInvitation}
onDelete={() => deleteSpaceMutator(space.id)}
onRename={() => setRenaming(true)}
onSpaceSetting={onSpaceSetting}
Expand Down
3 changes: 2 additions & 1 deletion apps/nextjs-app/src/features/app/blocks/space/SpacePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const SpacePage: FC = () => {
enabled: isCloud,
});

const { disallowSpaceCreation } = useSetting();
const { disallowSpaceCreation, disallowSpaceInvitation } = useSetting();

const { mutate: createSpaceMutator, isLoading } = useMutation({
mutationFn: createSpace,
Expand Down Expand Up @@ -77,6 +77,7 @@ export const SpacePage: FC = () => {
space={space}
bases={baseList?.filter(({ spaceId }) => spaceId === space.id)}
subscription={subscriptionMap[space.id]}
disallowSpaceInvitation={disallowSpaceInvitation}
/>
))}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@ import { Button } from '@teable/ui-lib/shadcn/ui/button';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { useIsCloud } from '@/features/app/hooks/useIsCloud';
import { spaceConfig } from '@/features/i18n/space.config';
import { PinList } from './PinList';
import { SpaceList } from './SpaceList';

export const SpaceSideBar = (props: { isAdmin?: boolean | null }) => {
const { isAdmin } = props;
const router = useRouter();
const isCloud = useIsCloud();
const { t } = useTranslation(spaceConfig.i18nNamespaces);

const pageRoutes: {
Expand All @@ -30,7 +28,7 @@ export const SpaceSideBar = (props: { isAdmin?: boolean | null }) => {
href: '/admin/setting',
text: t('noun.adminPanel'),
Icon: Admin,
hidden: isCloud || !isAdmin,
hidden: !isAdmin,
},
];
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,22 @@ interface ActionBarProps {
invQueryFilters: string[];
className?: string;
buttonSize?: ButtonProps['size'];
disallowSpaceInvitation?: boolean | null;
onRename?: () => void;
onDelete?: () => void;
onSpaceSetting?: () => void;
}

export const SpaceActionBar: React.FC<ActionBarProps> = (props) => {
const { space, className, buttonSize = 'default', onRename, onDelete, onSpaceSetting } = props;
const {
space,
className,
buttonSize = 'default',
disallowSpaceInvitation,
onRename,
onDelete,
onSpaceSetting,
} = props;
const { t } = useTranslation(spaceConfig.i18nNamespaces);

return (
Expand All @@ -34,11 +43,14 @@ export const SpaceActionBar: React.FC<ActionBarProps> = (props) => {
</Button>
</CreateBaseModalTrigger>
)}
<SpaceCollaboratorModalTrigger space={space}>
<Button variant={'outline'} size={buttonSize}>
<UserPlus className="size-4" /> {t('space:action.invite')}
</Button>
</SpaceCollaboratorModalTrigger>
{!disallowSpaceInvitation && (
<SpaceCollaboratorModalTrigger space={space}>
<Button variant={'outline'} size={buttonSize}>
<UserPlus className="size-4" /> {t('space:action.invite')}
</Button>
</SpaceCollaboratorModalTrigger>
)}

<SpaceActionTrigger
space={space}
showRename={hasPermission(space.role, 'space|update')}
Expand Down
7 changes: 6 additions & 1 deletion apps/nextjs-app/src/features/app/hooks/useSetting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,15 @@ export const useSetting = () => {
queryFn: () => getSetting().then(({ data }) => data),
});

const { disallowSignUp = false, disallowSpaceCreation = false } = setting ?? {};
const {
disallowSignUp = false,
disallowSpaceCreation = false,
disallowSpaceInvitation = false,
} = setting ?? {};

return {
disallowSignUp,
disallowSpaceCreation: !user.isAdmin && (isLoading || disallowSpaceCreation),
disallowSpaceInvitation: !user.isAdmin && (isLoading || disallowSpaceInvitation),
};
};
25 changes: 6 additions & 19 deletions apps/nextjs-app/src/features/auth/pages/LoginPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { useQuery } from '@tanstack/react-query';
import { TeableNew } from '@teable/icons';
import { getSetting } from '@teable/openapi';
import { Tabs, TabsList, TabsTrigger } from '@teable/ui-lib/shadcn';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
Expand All @@ -20,13 +18,6 @@ export const LoginPage: FC = () => {
window.location.href = redirect ? decodeURIComponent(redirect) : '/space';
}, [redirect]);

const { data: setting } = useQuery({
queryKey: ['setting'],
queryFn: () => getSetting().then(({ data }) => data),
});

const { disallowSignUp = false } = setting ?? {};

return (
<>
<NextSeo title={t('auth:page.title')} />
Expand All @@ -36,16 +27,12 @@ export const LoginPage: FC = () => {
<TeableNew className="size-8 text-black" />
{t('common:brand')}
</div>
{disallowSignUp ? (
t('auth:button.signin')
) : (
<Tabs value={signType} onValueChange={(val) => setSignType(val as ISignForm['type'])}>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="signin">{t('auth:button.signin')}</TabsTrigger>
<TabsTrigger value="signup">{t('auth:button.signup')}</TabsTrigger>
</TabsList>
</Tabs>
)}
<Tabs value={signType} onValueChange={(val) => setSignType(val as ISignForm['type'])}>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="signin">{t('auth:button.signin')}</TabsTrigger>
<TabsTrigger value="signup">{t('auth:button.signup')}</TabsTrigger>
</TabsList>
</Tabs>
</div>
<div className="relative top-1/2 mx-auto w-80 -translate-y-1/2 py-[5em] lg:py-24">
<SignForm type={signType} onSuccess={onSuccess} />
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@teable/teable",
"version": "1.2.0-beta.0",
"version": "1.2.1-beta.0",
"license": "AGPL-3.0",
"private": true,
"homepage": "https://github.com/teableio/teable",
Expand Down
2 changes: 1 addition & 1 deletion packages/common-i18n/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@teable/common-i18n",
"version": "1.2.0-beta.0",
"version": "1.2.1-beta.0",
"license": "AGPL-3.0",
"homepage": "https://github.com/teableio/teable",
"private": false,
Expand Down
Loading

0 comments on commit 9e5bbb6

Please sign in to comment.