Skip to content

Commit 9c3ea4d

Browse files
committed
feat(api): users controllers
1 parent 7a1c918 commit 9c3ea4d

File tree

7 files changed

+92
-42
lines changed

7 files changed

+92
-42
lines changed

modules/libs/protocol/users.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,3 @@ export type UpdateUserRequest =
4343

4444
export type UpdateUserResponse =
4545
crud.UpdateItemResponse<UserDetails>;
46-
47-
/* -------------------------------------------------------------------------- */
48-
/* Delete */
49-
/* -------------------------------------------------------------------------- */
50-
51-
export type DeleteUserResponse =
52-
crud.DeleteItemResponse;

modules/services/api/src/edu/controllers/users/specs/context.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,14 @@ export const createContext = async (
5555
name: 'Org Admin',
5656
description: 'Organization Admin',
5757
schoolId: one.id,
58-
permissions: ['users:read'],
58+
permissions: ['users:read', 'users:update', 'users:delete'],
5959
});
6060

6161
const twoAdmin = await rolesService.create({
6262
name: 'Org Two Admin',
6363
description: 'Organization Two Admin',
6464
schoolId: two.id,
65-
permissions: ['users:read'],
65+
permissions: ['users:read', 'users:update', 'users:delete'],
6666
});
6767

6868
/* -------------------------------------------------------------------------- */

modules/services/api/src/edu/controllers/users/specs/users.controller.test.get.spec.ts

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { INestApplication } from '@nestjs/common';
44
import { UserPermissions } from '@vidya/api/auth/utils';
55
import { UsersController } from '@vidya/api/edu/controllers';
66
import * as dto from '@vidya/api/edu/dto';
7+
import { UsersService } from '@vidya/api/edu/services';
78
import { createTestingApp } from '@vidya/api/edu/shared';
89
import { Role } from '@vidya/entities';
910
import * as entities from '@vidya/entities';
@@ -42,33 +43,94 @@ describe('UsersController', () => {
4243
);
4344
}
4445

46+
describe('getOne', () => {
47+
it('returns user by Id', async () => {
48+
const res = await ctr.getOne(
49+
ctx.one.users.oneAdmin.id,
50+
getPermissions([ctx.one.roles.oneAdmin]),
51+
);
52+
expect(res).toEqual(
53+
mapper.map(ctx.one.users.oneAdmin, entities.User, dto.GetUserResponse),
54+
);
55+
});
56+
57+
it('returns 404 if access is not permitted', async () => {
58+
await expect(async () => {
59+
await ctr.getOne(
60+
ctx.one.users.oneAdmin.id,
61+
getPermissions([ctx.two.roles.twoAdmin]),
62+
);
63+
}).rejects.toThrow(`User with id ${ctx.one.users.oneAdmin.id} not found`);
64+
});
65+
66+
it('returns 403 for user without any permissions', async () => {
67+
await expect(async () => {
68+
await ctr.getOne(ctx.one.users.oneAdmin.id, new UserPermissions([]));
69+
}).rejects.toThrow(`User does not have permission`);
70+
});
71+
});
72+
4573
/* -------------------------------------------------------------------------- */
4674
/* Get Many */
4775
/* -------------------------------------------------------------------------- */
4876

4977
describe('getMany', () => {
50-
it('user can see all users in school', async () => {
78+
it('returns all users in permitted school', async () => {
5179
const res = await ctr.getMany(
5280
new dto.GetUsersQuery(),
5381
getPermissions([ctx.one.roles.oneAdmin]),
5482
);
5583
expectUsers(res, [ctx.one.users.oneAdmin]);
5684
});
5785

58-
it('user can see all users in multiple schools', async () => {
86+
it('returns all users in multiple permitted schools', async () => {
5987
const res = await ctr.getMany(
6088
new dto.GetUsersQuery(),
6189
getPermissions([ctx.one.roles.oneAdmin, ctx.two.roles.twoAdmin]),
6290
);
6391
expectUsers(res, [ctx.one.users.oneAdmin, ctx.two.users.twoAdmin]);
6492
});
6593

66-
it('should filter by schoolId', async () => {
94+
it('filters by schoolId', async () => {
6795
const res = await ctr.getMany(
6896
new dto.GetUsersQuery({ schoolId: ctx.one.school.id }),
6997
getPermissions([ctx.one.roles.oneAdmin, ctx.two.roles.twoAdmin]),
7098
);
7199
expectUsers(res, [ctx.one.users.oneAdmin]);
72100
});
73101
});
102+
103+
/* -------------------------------------------------------------------------- */
104+
/* Update One */
105+
/* -------------------------------------------------------------------------- */
106+
107+
describe('updateOne', () => {
108+
it('updates user by Id', async () => {
109+
const res = await ctr.updateOne(
110+
new dto.UpdateUserRequest({ name: 'Updated Name' }),
111+
ctx.one.users.oneAdmin.id,
112+
getPermissions([ctx.one.roles.oneAdmin]),
113+
);
114+
115+
expect(res).toEqual(
116+
mapper.map(
117+
await app
118+
.get(UsersService)
119+
.findOneBy({ id: ctx.one.users.oneAdmin.id }),
120+
entities.User,
121+
dto.UpdateUserResponse,
122+
),
123+
);
124+
});
125+
126+
it('throws if user do not have permission', async () => {
127+
await expect(async () => {
128+
await ctr.updateOne(
129+
new dto.UpdateUserRequest({ name: 'Updated Name' }),
130+
ctx.one.users.oneAdmin.id,
131+
getPermissions([ctx.two.roles.twoAdmin]),
132+
);
133+
}).rejects.toThrow(`User does not have permission`);
134+
});
135+
});
74136
});

modules/services/api/src/edu/controllers/users/users.controller.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ const Crud = CrudDecorators({
2525
getOneResponseDto: dto.GetUserResponse,
2626
getManyResponseDto: dto.GetUsersResponse,
2727
updateOneResponseDto: dto.UpdateUserResponse,
28-
deleteOneResponseDto: dto.DeleteUserResponse,
2928
});
3029

3130
@Controller()
@@ -104,18 +103,4 @@ export class UsersController {
104103
const user = await this.usersService.updateOneBy({ id }, request);
105104
return this.mapper.map(user, entities.User, dto.UpdateUserResponse);
106105
}
107-
108-
/* -------------------------------------------------------------------------- */
109-
/* DELETE /edu/users/:id */
110-
/* -------------------------------------------------------------------------- */
111-
112-
@Crud.DeleteOne(Routes().edu.user(':id').delete())
113-
async deleteOne(
114-
@Param('id', new ParseUUIDPipe(), UserExistsPipe) id: string,
115-
@UserWithPermissions() userPermissions: UserPermissions,
116-
): Promise<dto.DeleteUserResponse> {
117-
// userPermissions.check(['users:delete'], { userId: id });
118-
await this.usersService.deleteOneBy({ id });
119-
return new dto.DeleteUserResponse({ success: true });
120-
}
121106
}

modules/services/api/src/edu/dto/users.dto.ts

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ export class GetUsersResponse implements protocol.GetUsersResponse {
8282
/* -------------------------------------------------------------------------- */
8383

8484
export class UpdateUserRequest implements protocol.UpdateUserRequest {
85+
constructor(options?: { name?: string; email?: string; phone?: string }) {
86+
this.name = options?.name;
87+
this.email = options?.email;
88+
this.phone = options?.phone;
89+
}
90+
8591
@ApiPropertyOptional({ example: 'User' })
8692
@IsString()
8793
@IsOptional()
@@ -106,15 +112,3 @@ export class UpdateUserRequest implements protocol.UpdateUserRequest {
106112
export class UpdateUserResponse
107113
extends UserDetails
108114
implements protocol.UpdateUserResponse {}
109-
110-
/* -------------------------------------------------------------------------- */
111-
/* Delete */
112-
/* -------------------------------------------------------------------------- */
113-
114-
export class DeleteUserResponse {
115-
constructor(options?: { success: boolean }) {
116-
this.success = options.success;
117-
}
118-
@ApiProperty({ example: 'success' })
119-
success: boolean;
120-
}

modules/services/api/src/edu/mappers/users.mapper.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,21 @@ export class UsersMappingProfile extends AutomapperProfile {
2222
forMember((d) => d.name, mapFrom((s) => s.name)),
2323
);
2424

25+
createMap(
26+
mapper, entities.User, dto.GetUserResponse,
27+
forMember((d) => d.id, mapFrom((s) => s.id)),
28+
forMember((d) => d.name, mapFrom((s) => s.name)),
29+
forMember((d) => d.email, mapFrom((s) => s.email)),
30+
forMember((d) => d.phone, mapFrom((s) => s.phone))
31+
);
32+
33+
createMap(
34+
mapper, entities.User, dto.UpdateUserResponse,
35+
forMember((d) => d.id, mapFrom((s) => s.id)),
36+
forMember((d) => d.name, mapFrom((s) => s.name)),
37+
forMember((d) => d.email, mapFrom((s) => s.email)),
38+
forMember((d) => d.phone, mapFrom((s) => s.phone))
39+
);
2540
};
2641
}
2742
}

modules/services/api/src/edu/services/users.service.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ export class UsersService extends ScopedEntitiesService<User, Scope> {
1212
constructor(@InjectRepository(User) repository: Repository<User>) {
1313
super(repository, (query, scope) => {
1414
// TODO add support for query.where as array
15-
const {
16-
roles: { schoolId },
17-
} = query?.where ?? ({} as any);
15+
const q = query?.where as any;
16+
const userId = q?.id;
17+
const schoolId = q?.roles?.schoolId;
1818

1919
// Get all user scopes and filter whem by query params if provided
2020
const userScopes = scope.permissions
@@ -25,6 +25,7 @@ export class UsersService extends ScopedEntitiesService<User, Scope> {
2525
const scopedQuery = {
2626
where: [
2727
...userScopes.map((s) => ({
28+
id: userId,
2829
roles: {
2930
schoolId: s.schoolId,
3031
},

0 commit comments

Comments
 (0)