mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 14:43:06 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b4a02e94a |
@@ -8,11 +8,20 @@ import { useTranslation } from "react-i18next";
|
||||
import Paginate from "@/components/common/paginate.tsx";
|
||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||
import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts";
|
||||
import { useAtom } from "jotai";
|
||||
import { userAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import { UserRole } from "@/lib/types.ts";
|
||||
import { useIsEEOnly } from "@/hooks/use-is-cloud-ee.tsx";
|
||||
|
||||
export default function SpaceList() {
|
||||
const { t } = useTranslation();
|
||||
const [page, setPage] = useState(1);
|
||||
const { data, isLoading } = useGetSpacesQuery({ page });
|
||||
const [user] = useAtom(userAtom);
|
||||
const isEEOnly = useIsEEOnly();
|
||||
const { data, isLoading } = useGetSpacesQuery({
|
||||
page,
|
||||
...(isEEOnly && user.role === UserRole.OWNER && { includeAllSpaces: true }),
|
||||
});
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [selectedSpaceId, setSelectedSpaceId] = useState<string>(null);
|
||||
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import { isCloud } from "@/lib/config";
|
||||
import { useLicense } from "@/ee/hooks/use-license";
|
||||
import { useAtom } from "jotai/index";
|
||||
import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import usePlan from "@/ee/hooks/use-plan";
|
||||
|
||||
export const useIsCloudEE = () => {
|
||||
const { hasLicenseKey } = useLicense();
|
||||
return isCloud() || !!hasLicenseKey;
|
||||
};
|
||||
};
|
||||
|
||||
export const useIsEEOnly = () => {
|
||||
const { hasLicenseKey } = useLicense();
|
||||
const { isBusiness } = usePlan();
|
||||
return (isCloud() && isBusiness) || !!hasLicenseKey;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ export interface QueryParams {
|
||||
query?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
includeAllSpaces?: boolean;
|
||||
}
|
||||
|
||||
export enum UserRole {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { UserRepo } from '@docmost/db/repos/user/user.repo';
|
||||
import { PageRepo } from '@docmost/db/repos/page/page.repo';
|
||||
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
|
||||
import { findHighestUserSpaceRole } from '@docmost/db/repos/space/utils';
|
||||
import { SpaceRole } from '../../common/helpers/types/permission';
|
||||
import { SpaceRole, UserRole } from '../../common/helpers/types/permission';
|
||||
import { getPageId } from '../collaboration.util';
|
||||
import { JwtCollabPayload, JwtType } from '../../core/auth/dto/jwt-payload';
|
||||
|
||||
@@ -63,7 +63,10 @@ export class AuthenticationExtension implements Extension {
|
||||
|
||||
const userSpaceRole = findHighestUserSpaceRole(userSpaceRoles);
|
||||
|
||||
if (!userSpaceRole) {
|
||||
// if role not found but user is a workspace owner, grant them readonly permission
|
||||
if (!userSpaceRole && user.role === UserRole.OWNER) {
|
||||
data.connection.readOnly = true;
|
||||
} else if (!userSpaceRole) {
|
||||
this.logger.warn(`User not authorized to access page: ${pageId}`);
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
createMongoAbility,
|
||||
MongoAbility,
|
||||
} from '@casl/ability';
|
||||
import { SpaceRole } from '../../../common/helpers/types/permission';
|
||||
import { SpaceRole, UserRole } from '../../../common/helpers/types/permission';
|
||||
import { User } from '@docmost/db/types/entity.types';
|
||||
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
|
||||
import {
|
||||
@@ -25,13 +25,17 @@ export default class SpaceAbilityFactory {
|
||||
|
||||
const userSpaceRole = findHighestUserSpaceRole(userSpaceRoles);
|
||||
|
||||
if (!userSpaceRole && user.role === UserRole.OWNER) {
|
||||
return buildWorkspaceOwnerAbility();
|
||||
}
|
||||
|
||||
switch (userSpaceRole) {
|
||||
case SpaceRole.ADMIN:
|
||||
return buildSpaceAdminAbility();
|
||||
case SpaceRole.WRITER:
|
||||
return buildSpaceWriterAbility();
|
||||
return buildSpaceWriterAbility(user.role);
|
||||
case SpaceRole.READER:
|
||||
return buildSpaceReaderAbility();
|
||||
return buildSpaceReaderAbility(user.role);
|
||||
default:
|
||||
throw new NotFoundException('Space permissions not found');
|
||||
}
|
||||
@@ -49,23 +53,50 @@ function buildSpaceAdminAbility() {
|
||||
return build();
|
||||
}
|
||||
|
||||
function buildSpaceWriterAbility() {
|
||||
function buildSpaceWriterAbility(workspaceRole?: string) {
|
||||
const { can, build } = new AbilityBuilder<MongoAbility<ISpaceAbility>>(
|
||||
createMongoAbility,
|
||||
);
|
||||
can(SpaceCaslAction.Read, SpaceCaslSubject.Settings);
|
||||
can(SpaceCaslAction.Read, SpaceCaslSubject.Member);
|
||||
|
||||
if (workspaceRole === UserRole.OWNER) {
|
||||
// Workspace owners get manage permissions even with writer space role
|
||||
can(SpaceCaslAction.Manage, SpaceCaslSubject.Settings);
|
||||
can(SpaceCaslAction.Manage, SpaceCaslSubject.Member);
|
||||
} else {
|
||||
can(SpaceCaslAction.Read, SpaceCaslSubject.Settings);
|
||||
can(SpaceCaslAction.Read, SpaceCaslSubject.Member);
|
||||
}
|
||||
|
||||
can(SpaceCaslAction.Manage, SpaceCaslSubject.Page);
|
||||
can(SpaceCaslAction.Manage, SpaceCaslSubject.Share);
|
||||
return build();
|
||||
}
|
||||
|
||||
function buildSpaceReaderAbility() {
|
||||
function buildSpaceReaderAbility(workspaceRole?: string) {
|
||||
const { can, build } = new AbilityBuilder<MongoAbility<ISpaceAbility>>(
|
||||
createMongoAbility,
|
||||
);
|
||||
can(SpaceCaslAction.Read, SpaceCaslSubject.Settings);
|
||||
can(SpaceCaslAction.Read, SpaceCaslSubject.Member);
|
||||
|
||||
if (workspaceRole === UserRole.OWNER) {
|
||||
// Workspace owners get manage permissions even with reader space role
|
||||
can(SpaceCaslAction.Manage, SpaceCaslSubject.Settings);
|
||||
can(SpaceCaslAction.Manage, SpaceCaslSubject.Member);
|
||||
} else {
|
||||
can(SpaceCaslAction.Read, SpaceCaslSubject.Settings);
|
||||
can(SpaceCaslAction.Read, SpaceCaslSubject.Member);
|
||||
}
|
||||
|
||||
can(SpaceCaslAction.Read, SpaceCaslSubject.Page);
|
||||
can(SpaceCaslAction.Read, SpaceCaslSubject.Share);
|
||||
return build();
|
||||
}
|
||||
|
||||
function buildWorkspaceOwnerAbility() {
|
||||
const { can, build } = new AbilityBuilder<MongoAbility<ISpaceAbility>>(
|
||||
createMongoAbility,
|
||||
);
|
||||
can(SpaceCaslAction.Manage, SpaceCaslSubject.Settings);
|
||||
can(SpaceCaslAction.Manage, SpaceCaslSubject.Member);
|
||||
can(SpaceCaslAction.Read, SpaceCaslSubject.Page);
|
||||
can(SpaceCaslAction.Read, SpaceCaslSubject.Share);
|
||||
return build();
|
||||
|
||||
@@ -279,4 +279,14 @@ export class SpaceMemberService {
|
||||
): Promise<PaginationResult<Space>> {
|
||||
return await this.spaceMemberRepo.getUserSpaces(userId, pagination);
|
||||
}
|
||||
|
||||
async getAllWorkspaceSpaces(
|
||||
workspaceId: string,
|
||||
pagination: PaginationOptions,
|
||||
): Promise<PaginationResult<Space>> {
|
||||
return await this.spaceMemberRepo.getAllWorkspaceSpaces(
|
||||
workspaceId,
|
||||
pagination,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
} from '../casl/interfaces/workspace-ability.type';
|
||||
import WorkspaceAbilityFactory from '../casl/abilities/workspace-ability.factory';
|
||||
import { CreateSpaceDto } from './dto/create-space.dto';
|
||||
import { SpaceRole, UserRole } from '../../common/helpers/types/permission';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('spaces')
|
||||
@@ -52,7 +53,17 @@ export class SpaceController {
|
||||
@Body()
|
||||
pagination: PaginationOptions,
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
) {
|
||||
if (pagination.includeAllSpaces) {
|
||||
if (user.role !== UserRole.OWNER) {
|
||||
throw new ForbiddenException('Only workspace owners view all spaces');
|
||||
}
|
||||
return this.spaceMemberService.getAllWorkspaceSpaces(
|
||||
workspace.id,
|
||||
pagination,
|
||||
);
|
||||
}
|
||||
return this.spaceMemberService.getUserSpaces(user.id, pagination);
|
||||
}
|
||||
|
||||
@@ -82,7 +93,10 @@ export class SpaceController {
|
||||
space.id,
|
||||
);
|
||||
|
||||
const userSpaceRole = findHighestUserSpaceRole(userSpaceRoles);
|
||||
let userSpaceRole = findHighestUserSpaceRole(userSpaceRoles);
|
||||
if (!userSpaceRole && user.role === UserRole.OWNER) {
|
||||
userSpaceRole = SpaceRole.READER;
|
||||
}
|
||||
|
||||
const membership = {
|
||||
userId: user.id,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
IsBoolean,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsPositive,
|
||||
@@ -23,4 +24,9 @@ export class PaginationOptions {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
query: string;
|
||||
|
||||
//for space endpoint workspace owners
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
includeAllSpaces?: boolean;
|
||||
}
|
||||
|
||||
@@ -263,4 +263,37 @@ export class SpaceMemberRepo {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async getAllWorkspaceSpaces(
|
||||
workspaceId: string,
|
||||
pagination: PaginationOptions,
|
||||
) {
|
||||
let query = this.db
|
||||
.selectFrom('spaces')
|
||||
.selectAll()
|
||||
.select((eb) => [this.spaceRepo.withMemberCount(eb)])
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.orderBy('createdAt', 'asc');
|
||||
|
||||
if (pagination.query) {
|
||||
query = query.where((eb) =>
|
||||
eb(
|
||||
sql`f_unaccent(name)`,
|
||||
'ilike',
|
||||
sql`f_unaccent(${'%' + pagination.query + '%'})`,
|
||||
).or(
|
||||
sql`f_unaccent(description)`,
|
||||
'ilike',
|
||||
sql`f_unaccent(${'%' + pagination.query + '%'})`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const result = executeWithPagination(query, {
|
||||
page: pagination.page,
|
||||
perPage: pagination.limit,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user