Skip to content

Commit c69ad0e

Browse files
aakashg00llam36
andauthored
Aakash - Add Accept account request + support JWT for existing request methods (#245)
Co-authored-by: Long Lam <llam36@gatech.edu>
1 parent bd79e3e commit c69ad0e

9 files changed

Lines changed: 509 additions & 22 deletions

File tree

‎packages/api-gateway/src/models/registration.dto.ts‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,67 @@ export class NewAccountRequestsResponse {
8787
}
8888
}
8989

90+
export class AcceptAccountRequestResponseUser {
91+
@ApiProperty({ description: 'User ID' })
92+
id: number;
93+
94+
@ApiProperty({ description: 'Email' })
95+
email: string;
96+
97+
@ApiProperty({ description: 'Name' })
98+
name: string;
99+
100+
@ApiProperty({
101+
description: 'User type',
102+
enum: ['SUPERADMIN', 'ADMIN', 'USER'],
103+
})
104+
type: string;
105+
106+
@ApiPropertyOptional({
107+
description: 'Project IDs the user is linked to',
108+
type: [Number],
109+
})
110+
projectIds?: number[];
111+
}
112+
113+
export class AcceptAccountRequestResponseProject {
114+
@ApiProperty({ description: 'Project ID' })
115+
id: number;
116+
117+
@ApiProperty({ description: 'Project name' })
118+
name: string;
119+
}
120+
121+
export class AcceptAccountRequestResponseModel {
122+
@ApiProperty({
123+
description: 'The created user',
124+
type: AcceptAccountRequestResponseUser,
125+
})
126+
user: AcceptAccountRequestResponseUser;
127+
128+
@ApiPropertyOptional({
129+
description: 'The created or linked project (if applicable)',
130+
type: AcceptAccountRequestResponseProject,
131+
})
132+
project?: AcceptAccountRequestResponseProject;
133+
134+
constructor(res: UserProto.AcceptAccountRequestResponse) {
135+
this.user = {
136+
id: Number(res.user.id),
137+
email: res.user.email,
138+
name: res.user.name,
139+
type: fromUserType(res.user.type),
140+
projectIds: res.user.projectIds?.map(Number),
141+
};
142+
if (res.project) {
143+
this.project = {
144+
id: Number(res.project.id),
145+
name: res.project.name,
146+
};
147+
}
148+
}
149+
}
150+
90151
export function userTypeStringToProto(value: string): CommonProto.UserType {
91152
switch (value) {
92153
case 'SUPERADMIN':

‎packages/api-gateway/src/modules/auth/auth.controller.ts‎

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import {
4949
RequestNewAccountModel,
5050
NewAccountRequestResponse,
5151
NewAccountRequestsResponse,
52+
AcceptAccountRequestResponseModel,
5253
userTypeStringToProto,
5354
} from 'src/models/registration.dto';
5455
import { userLinkedToProject } from 'src/user_project_validator';
@@ -511,13 +512,13 @@ export class AuthController implements OnModuleInit {
511512
@ApiHeader({
512513
name: 'X-User-Email',
513514
description: 'Email of an admin or superadmin user',
514-
required: true,
515+
required: false,
515516
schema: { type: 'string' },
516517
})
517518
@ApiHeader({
518519
name: 'X-User-Password',
519520
description: 'Password of the admin or superadmin user',
520-
required: true,
521+
required: false,
521522
schema: { type: 'string' },
522523
})
523524
@ApiBearerAuth('API_Key')
@@ -561,13 +562,13 @@ export class AuthController implements OnModuleInit {
561562
@ApiHeader({
562563
name: 'X-User-Email',
563564
description: 'Email of an admin or superadmin user',
564-
required: true,
565+
required: false,
565566
schema: { type: 'string' },
566567
})
567568
@ApiHeader({
568569
name: 'X-User-Password',
569570
description: 'Password of the admin or superadmin user',
570-
required: true,
571+
required: false,
571572
schema: { type: 'string' },
572573
})
573574
@ApiBearerAuth('API_Key')
@@ -587,4 +588,59 @@ export class AuthController implements OnModuleInit {
587588
);
588589
return new NewAccountRequestResponse(removed);
589590
}
591+
592+
@Post('/account-request/:id/accept')
593+
@ApiOperation({
594+
summary: 'Accept an account request',
595+
description:
596+
'Accepts a pending account request: creates the user, optionally creates/links a project (for ADMIN requests with a projectName), and deletes the pending request. Requires admin or superadmin credentials.',
597+
})
598+
@ApiParam({
599+
name: 'id',
600+
required: true,
601+
description: 'ID of the account request to accept',
602+
type: String,
603+
})
604+
@ApiResponse({
605+
status: HttpStatus.CREATED,
606+
description: 'Account request accepted and user created',
607+
type: AcceptAccountRequestResponseModel,
608+
})
609+
@ApiResponse({
610+
status: HttpStatus.NOT_FOUND,
611+
description: 'Account request not found',
612+
})
613+
@ApiResponse({
614+
status: HttpStatus.UNAUTHORIZED,
615+
description: 'Unauthorized',
616+
})
617+
@ApiHeader({
618+
name: 'X-User-Email',
619+
description: 'Email of an admin or superadmin user',
620+
required: false,
621+
schema: { type: 'string' },
622+
})
623+
@ApiHeader({
624+
name: 'X-User-Password',
625+
description: 'Password of the admin or superadmin user',
626+
required: false,
627+
schema: { type: 'string' },
628+
})
629+
@ApiBearerAuth('API_Key')
630+
async acceptAccountRequest(
631+
@User() user: CommonProto.User,
632+
@Param('id') idStr: string,
633+
): Promise<AcceptAccountRequestResponseModel> {
634+
if (!user || user.type === CommonProto.UserType.USER) {
635+
throw new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED);
636+
}
637+
const id = parseInt(idStr);
638+
if (Number.isNaN(id)) {
639+
throw new HttpException('id must be a number', HttpStatus.BAD_REQUEST);
640+
}
641+
const result = await lastValueFrom(
642+
this.accountRequestService.acceptAccountRequest({ id }),
643+
);
644+
return new AcceptAccountRequestResponseModel(result);
645+
}
590646
}

‎packages/api-gateway/src/modules/auth/auth.module.ts‎

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -86,16 +86,18 @@ const { PROJECT_SERVICE_NAME, JUNO_PROJECT_PACKAGE_NAME } = ProjectProto;
8686
})
8787
export class AuthModule implements NestModule {
8888
configure(consumer: MiddlewareConsumer) {
89-
consumer
90-
.apply(CredentialsMiddleware)
91-
.forRoutes(
92-
{ path: 'auth/user/jwt', method: RequestMethod.POST },
93-
{ path: 'auth/test-auth', method: RequestMethod.GET },
94-
{ path: 'auth/account-request', method: RequestMethod.GET },
95-
{ path: 'auth/account-request/:id', method: RequestMethod.DELETE },
96-
{ path: 'auth/key', method: RequestMethod.POST },
97-
{ path: 'auth/key/all', method: RequestMethod.GET },
98-
{ path: 'auth/key/:id', method: RequestMethod.DELETE },
99-
);
89+
consumer.apply(CredentialsMiddleware).forRoutes(
90+
{ path: 'auth/user/jwt', method: RequestMethod.POST },
91+
{ path: 'auth/test-auth', method: RequestMethod.GET },
92+
{ path: 'auth/account-request', method: RequestMethod.GET },
93+
{ path: 'auth/account-request/:id', method: RequestMethod.DELETE },
94+
{ path: 'auth/key', method: RequestMethod.POST },
95+
{ path: 'auth/key/all', method: RequestMethod.GET },
96+
{ path: 'auth/key/:id', method: RequestMethod.DELETE },
97+
{
98+
path: 'auth/account-request/:id/accept',
99+
method: RequestMethod.POST,
100+
},
101+
);
100102
}
101103
}

‎packages/api-gateway/test/auth.e2e-spec.ts‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,3 +643,71 @@ describe('Account Request - DELETE /auth/account-request/:id', () => {
643643
.expect(404);
644644
});
645645
});
646+
647+
describe('Account Request - POST /auth/account-request/:id/accept', () => {
648+
it('should accept a USER request and return the created user', async () => {
649+
const createResp = await request(app.getHttpServer())
650+
.post('/auth/account-request')
651+
.send({
652+
email: 'accept-gw-user@example.com',
653+
name: 'Accept GW User',
654+
password: 'securepassword',
655+
userType: 'USER',
656+
})
657+
.expect(201);
658+
659+
const id = createResp.body.id;
660+
661+
const acceptResp = await request(app.getHttpServer())
662+
.post(`/auth/account-request/${id}/accept`)
663+
.set('X-User-Email', ADMIN_EMAIL)
664+
.set('X-User-Password', ADMIN_PASSWORD)
665+
.expect(201);
666+
667+
expect(acceptResp.body.user).toBeDefined();
668+
expect(acceptResp.body.user.email).toBe('accept-gw-user@example.com');
669+
expect(acceptResp.body.user.name).toBe('Accept GW User');
670+
expect(acceptResp.body.user.type).toBe('USER');
671+
});
672+
673+
it('should accept an ADMIN request with projectName and return user + project', async () => {
674+
const createResp = await request(app.getHttpServer())
675+
.post('/auth/account-request')
676+
.send({
677+
email: 'accept-gw-admin@example.com',
678+
name: 'Accept GW Admin',
679+
password: 'securepassword',
680+
userType: 'ADMIN',
681+
projectName: 'gw-accept-project',
682+
})
683+
.expect(201);
684+
685+
const id = createResp.body.id;
686+
687+
const acceptResp = await request(app.getHttpServer())
688+
.post(`/auth/account-request/${id}/accept`)
689+
.set('X-User-Email', ADMIN_EMAIL)
690+
.set('X-User-Password', ADMIN_PASSWORD)
691+
.expect(201);
692+
693+
expect(acceptResp.body.user).toBeDefined();
694+
expect(acceptResp.body.user.email).toBe('accept-gw-admin@example.com');
695+
expect(acceptResp.body.user.type).toBe('ADMIN');
696+
expect(acceptResp.body.project).toBeDefined();
697+
expect(acceptResp.body.project.name).toBe('gw-accept-project');
698+
});
699+
700+
it('should reject unauthenticated accept requests', () => {
701+
return request(app.getHttpServer())
702+
.post('/auth/account-request/1/accept')
703+
.expect(401);
704+
});
705+
706+
it('should return 400 for non-numeric id', () => {
707+
return request(app.getHttpServer())
708+
.post('/auth/account-request/abc/accept')
709+
.set('X-User-Email', ADMIN_EMAIL)
710+
.set('X-User-Password', ADMIN_PASSWORD)
711+
.expect(400);
712+
});
713+
});

‎packages/db-service/src/modules/auth/auth.controller.ts‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,22 @@ export class ApiKeyDbController
124124
createdAt: removed.createdAt.toISOString(),
125125
};
126126
}
127+
128+
async acceptAccountRequest(
129+
request: UserProto.AcceptAccountRequestMessage,
130+
): Promise<UserProto.AcceptAccountRequestResponse> {
131+
const result = await this.apiKeyService.acceptAccountRequest(request.id);
132+
return {
133+
user: {
134+
id: result.user.id,
135+
email: result.user.email,
136+
name: result.user.name,
137+
type: mapPrismaRoleToRPC(result.user.type),
138+
projectIds: result.user.projectIds,
139+
},
140+
project: result.project
141+
? { id: result.project.id, name: result.project.name }
142+
: undefined,
143+
};
144+
}
127145
}

0 commit comments

Comments
 (0)