Skip to content

Commit

Permalink
Make userService optional (#1684)
Browse files Browse the repository at this point in the history
Allows easier migration as the userService is only needed for
the UserPermissions administration panel
  • Loading branch information
fraxachun authored Feb 19, 2024
1 parent a4fac91 commit 7ea43eb
Show file tree
Hide file tree
Showing 8 changed files with 75 additions and 46 deletions.
7 changes: 7 additions & 0 deletions .changeset/afraid-horses-hide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@comet/cms-api": minor
---

Make the `UserService`-option of the `UserPermissionsModule` optional.

The service is still necessary though for the Administration-Panel.
2 changes: 1 addition & 1 deletion demo/api/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { UserService } from "./user.service";
@Module({
providers: [
createStaticAuthedUserStrategy({
staticAuthedUser: staticUsers[0].id,
staticAuthedUser: staticUsers[0],
}),
createAuthResolver(),
{
Expand Down
56 changes: 38 additions & 18 deletions docs/docs/migration/migration-from-v5-to-v6.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,20 +86,7 @@ It automatically installs the new versions of all `@comet` libraries, runs an ES
export {};
```

4. Create necessary services for the `UserPermissionsModule` (either in a new module or where it fits best)

```ts
// Attention: might already being provided by the library which syncs the users
@Injectable()
export class UserService implements UserPermissionsUserServiceInterface {
getUser(id: string): User {
...
}
findUsers(args: FindUsersArgs): Users {
...
}
}
```
4. Create the `AccessControlService` for the `UserPermissionsModule` (either in a new module or where it fits best)

```ts
@Injectable()
Expand Down Expand Up @@ -127,13 +114,12 @@ It automatically installs the new versions of all `@comet` libraries, runs an ES

```ts
UserPermissionsModule.forRootAsync({
useFactory: (userService: UserService, accessControlService: AccessControlService) => ({
useFactory: (accessControlService: AccessControlService) => ({
availablePermissions: [/* Array of strings defined in interface Permission */],
availableContentScopes: [/* Array of content Scopes */],
userService,
accessControlService,
}),
inject: [UserService, AccessControlService],
inject: [AccessControlService],
imports: [/* Modules which provide the services injected in useFactory */],
}),
```
Expand All @@ -153,6 +139,40 @@ It automatically installs the new versions of all `@comet` libraries, runs an ES
- @AllowForRole(...)
```

7. Optional: Add the `UserService` (required for Administration Panel, see Admin)

Create a `UserService`:

```ts
// Attention: might already being provided by the library which syncs the users
@Injectable()
export class UserService implements UserPermissionsUserServiceInterface {
getUser(id: string): User {
...
}
findUsers(args: FindUsersArgs): Users {
...
}
}
```

Add it to the `UserPermissionsModule`:

```diff
UserPermissionsModule.forRootAsync({
+ useFactory: (accessControlService: AccessControlService, userService: UserService) => ({
- useFactory: (accessControlService: AccessControlService) => ({
availablePermissions: [/* Array of strings defined in interface Permission */],
availableContentScopes: [/* Array of content Scopes */],
+ userService,
accessControlService,
}),
+ inject: [AccessControlService, UserService],
- inject: [AccessControlService],
imports: [/* Modules which provide the services injected in useFactory */],
}),
```

## Admin

### User Permissions
Expand Down Expand Up @@ -184,7 +204,7 @@ It automatically installs the new versions of all `@comet` libraries, runs an ES
+ const allowedUserDomains = user.allowedContentScopes.map((contentScope) => contentScope.domain);
```

3. Add the `UserPermissionsPage`
3. Optional: Add the Adminstration Panel

```tsx
<MenuItemRouterLink
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export class UserContentScopesResolver {
@Args("skipManual", { type: () => Boolean, nullable: true }) skipManual = false,
): Promise<ContentScope[]> {
return this.userService.normalizeContentScopes(
await this.userService.getContentScopes(userId, !skipManual),
await this.userService.getContentScopes(await this.userService.getUser(userId), !skipManual),
await this.userService.getAvailableContentScopes(),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class UserPermissionResolver {

@Query(() => [UserPermission])
async userPermissionsPermissionList(@Args() args: UserPermissionListArgs): Promise<UserPermission[]> {
return this.service.getPermissions(args.userId);
return this.service.getPermissions(await this.service.getUser(args.userId));
}

@Query(() => UserPermission)
Expand Down Expand Up @@ -96,7 +96,7 @@ export class UserPermissionResolver {
if (!userId) {
throw new Error(`Permission not found: ${id}`);
}
for (const p of await this.service.getPermissions(userId)) {
for (const p of await this.service.getPermissions(await this.service.getUser(userId))) {
if (p.id === id) return p;
}
throw new Error("Permission not found");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,18 @@ export class UserPermissionsModule {
provide: USER_PERMISSIONS_OPTIONS,
useValue: options,
},
{
provide: USER_PERMISSIONS_USER_SERVICE,
useClass: options.UserService,
},
{
provide: ACCESS_CONTROL_SERVICE,
useClass: options.AccessControlService,
},
...(options.UserService
? [
{
provide: USER_PERMISSIONS_USER_SERVICE,
useClass: options.UserService,
},
]
: []),
],
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { EntityRepository } from "@mikro-orm/core";
import { InjectRepository } from "@mikro-orm/nestjs";
import { Inject, Injectable } from "@nestjs/common";
import { Inject, Injectable, Optional } from "@nestjs/common";
import { isFuture, isPast } from "date-fns";
import isEqual from "lodash.isequal";
import getUuid from "uuid-by-string";
Expand All @@ -24,7 +24,7 @@ import {
export class UserPermissionsService {
constructor(
@Inject(USER_PERMISSIONS_OPTIONS) private readonly options: UserPermissionsOptions,
@Inject(USER_PERMISSIONS_USER_SERVICE) private readonly userService: UserPermissionsUserServiceInterface,
@Inject(USER_PERMISSIONS_USER_SERVICE) @Optional() private readonly userService: UserPermissionsUserServiceInterface | undefined,
@Inject(ACCESS_CONTROL_SERVICE) private readonly accessControlService: AccessControlServiceInterface,
@InjectRepository(UserPermission) private readonly permissionRepository: EntityRepository<UserPermission>,
@InjectRepository(UserContentScopes) private readonly contentScopeRepository: EntityRepository<UserContentScopes>,
Expand All @@ -41,10 +41,12 @@ export class UserPermissionsService {
}

async getUser(id: string): Promise<User> {
if (!this.userService) throw new Error("For this functionality you need to define the userService in the UserPermissionsModule.");
return this.userService.getUser(id);
}

async findUsers(args: FindUsersArgs): Promise<[User[], number]> {
if (!this.userService) throw new Error("For this functionality you need to define the userService in the UserPermissionsModule.");
return this.userService.findUsers(args);
}

Expand All @@ -57,18 +59,17 @@ export class UserPermissionsService {
});
}

async getPermissions(userId: string): Promise<UserPermission[]> {
async getPermissions(user: User): Promise<UserPermission[]> {
const availablePermissions = await this.getAvailablePermissions();
const permissions = (
await this.permissionRepository.find({
$and: [{ userId }, { permission: { $in: availablePermissions } }],
$and: [{ userId: user.id }, { permission: { $in: availablePermissions } }],
})
).map((p) => {
p.source = UserPermissionSource.MANUAL;
return p;
});
if (this.accessControlService.getPermissionsForUser) {
const user = await this.getUser(userId);
if (user) {
let permissionsByRule = await this.accessControlService.getPermissionsForUser(user);
if (permissionsByRule === UserPermissions.allPermissions) {
Expand All @@ -78,7 +79,7 @@ export class UserPermissionsService {
const permission = new UserPermission();
permission.id = getUuid(JSON.stringify(p));
permission.source = UserPermissionSource.BY_RULE;
permission.userId = userId;
permission.userId = user.id;
permission.overrideContentScopes = !!p.contentScopes;
permission.assign(p);
permissions.push(permission);
Expand All @@ -94,24 +95,21 @@ export class UserPermissionsService {
);
}

async getContentScopes(userId: string, includeContentScopesManual = true): Promise<ContentScope[]> {
async getContentScopes(user: User, includeContentScopesManual = true): Promise<ContentScope[]> {
const contentScopes: ContentScope[] = [];
const availableContentScopes = await this.getAvailableContentScopes();

if (this.accessControlService.getContentScopesForUser) {
const user = await this.getUser(userId);
if (user) {
const userContentScopes = await this.accessControlService.getContentScopesForUser(user);
if (userContentScopes === UserPermissions.allContentScopes) {
contentScopes.push(...availableContentScopes);
} else {
contentScopes.push(...userContentScopes);
}
const userContentScopes = await this.accessControlService.getContentScopesForUser(user);
if (userContentScopes === UserPermissions.allContentScopes) {
contentScopes.push(...availableContentScopes);
} else {
contentScopes.push(...userContentScopes);
}
}

if (includeContentScopesManual) {
const entity = await this.contentScopeRepository.findOne({ userId });
const entity = await this.contentScopeRepository.findOne({ userId: user.id });
if (entity) {
contentScopes.push(...entity.contentScopes.filter((value) => availableContentScopes.some((cs) => isEqual(cs, value))));
}
Expand All @@ -128,8 +126,8 @@ export class UserPermissionsService {

async createCurrentUser(user: User): Promise<CurrentUser> {
const availableContentScopes = await this.getAvailableContentScopes();
const userContentScopes = await this.getContentScopes(user.id);
const permissions = (await this.getPermissions(user.id))
const userContentScopes = await this.getContentScopes(user);
const permissions = (await this.getPermissions(user))
.filter((p) => (!p.validFrom || isPast(p.validFrom)) && (!p.validTo || isFuture(p.validTo)))
.reduce((acc: CurrentUser["permissions"], userPermission) => {
const contentScopes = userPermission.overrideContentScopes ? userPermission.contentScopes : userContentScopes;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ export interface UserPermissionsOptions {
availableContentScopes?: ContentScope[];
}
export interface UserPermissionsModuleSyncOptions extends UserPermissionsOptions {
UserService: Type<UserPermissionsUserServiceInterface>;
UserService?: Type<UserPermissionsUserServiceInterface>;
AccessControlService: Type<AccessControlServiceInterface>;
}

export interface UserPermissionsAsyncOptions extends UserPermissionsOptions {
userService: UserPermissionsUserServiceInterface;
userService?: UserPermissionsUserServiceInterface;
accessControlService: AccessControlServiceInterface;
}

Expand Down

0 comments on commit 7ea43eb

Please sign in to comment.