Skip to content

Commit aaeb9dd

Browse files
committed
added new endpoint and testing
1 parent b4ede4d commit aaeb9dd

10 files changed

Lines changed: 736 additions & 5 deletions

File tree

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
2+
import {
3+
IsEmail,
4+
IsNotEmpty,
5+
IsOptional,
6+
IsString,
7+
MinLength,
8+
IsIn,
9+
} from 'class-validator';
10+
import { CommonProto, UserProto } from 'juno-proto';
11+
12+
export class RequestNewAccountModel {
13+
@IsNotEmpty()
14+
@IsEmail()
15+
@ApiProperty({ description: 'Email for the new account' })
16+
email: string;
17+
18+
@IsNotEmpty()
19+
@IsString()
20+
@ApiProperty({ description: 'Name of the requester' })
21+
name: string;
22+
23+
@IsNotEmpty()
24+
@IsString()
25+
@MinLength(6, { message: 'Password must be at least 6 characters long' })
26+
@ApiProperty({ description: 'Password for the new account' })
27+
password: string;
28+
29+
@IsNotEmpty()
30+
@IsIn(['SUPERADMIN', 'ADMIN', 'USER'], {
31+
message: 'userType must be one of: SUPERADMIN, ADMIN, USER',
32+
})
33+
@ApiProperty({
34+
description: 'Requested user type / role',
35+
enum: ['SUPERADMIN', 'ADMIN', 'USER'],
36+
})
37+
userType: string;
38+
39+
@IsOptional()
40+
@IsString()
41+
@ApiPropertyOptional({ description: 'Optional project name to associate' })
42+
projectName?: string;
43+
}
44+
45+
export class NewAccountRequestResponse {
46+
@ApiProperty({ description: 'Request ID' })
47+
id: number;
48+
49+
@ApiProperty({ description: 'Email' })
50+
email: string;
51+
52+
@ApiProperty({ description: 'Name' })
53+
name: string;
54+
55+
@ApiProperty({
56+
description: 'Requested user type',
57+
enum: ['SUPERADMIN', 'ADMIN', 'USER'],
58+
})
59+
userType: string;
60+
61+
@ApiPropertyOptional({ description: 'Project name' })
62+
projectName?: string;
63+
64+
@ApiProperty({ description: 'Timestamp of request creation' })
65+
createdAt: string;
66+
67+
constructor(req: UserProto.NewAccountRequest) {
68+
this.id = req.id;
69+
this.email = req.email;
70+
this.name = req.name;
71+
this.userType = fromUserType(req.userType);
72+
this.projectName = req.projectName;
73+
this.createdAt = req.createdAt;
74+
}
75+
}
76+
77+
export class NewAccountRequestsResponse {
78+
@ApiProperty({
79+
description: 'List of new account requests',
80+
type: [NewAccountRequestResponse],
81+
})
82+
requests: NewAccountRequestResponse[];
83+
84+
constructor(res: UserProto.NewAccountRequests) {
85+
this.requests =
86+
res?.requests?.map((r) => new NewAccountRequestResponse(r)) || [];
87+
}
88+
}
89+
90+
export function userTypeStringToProto(value: string): CommonProto.UserType {
91+
switch (value) {
92+
case 'SUPERADMIN':
93+
return CommonProto.UserType.SUPERADMIN;
94+
case 'ADMIN':
95+
return CommonProto.UserType.ADMIN;
96+
case 'USER':
97+
return CommonProto.UserType.USER;
98+
default:
99+
return CommonProto.UserType.USER;
100+
}
101+
}
102+
103+
function fromUserType(type: CommonProto.UserType): string {
104+
switch (type) {
105+
case CommonProto.UserType.SUPERADMIN:
106+
return 'SUPERADMIN';
107+
case CommonProto.UserType.ADMIN:
108+
return 'ADMIN';
109+
default:
110+
return 'USER';
111+
}
112+
}

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

Lines changed: 148 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
HttpStatus,
88
Inject,
99
OnModuleInit,
10+
Param,
1011
Post,
1112
Get,
1213
UnauthorizedException,
@@ -20,32 +21,49 @@ import {
2021
ApiHeader,
2122
ApiResponse,
2223
ApiBody,
24+
ApiParam,
2325
} from '@nestjs/swagger';
24-
import { ApiKeyProto, CommonProto, JwtProto, ProjectProto } from 'juno-proto';
26+
import {
27+
ApiKeyProto,
28+
CommonProto,
29+
JwtProto,
30+
ProjectProto,
31+
UserProto,
32+
} from 'juno-proto';
2533
import { lastValueFrom } from 'rxjs';
2634
import { User } from 'src/decorators/user.decorator';
2735
import {
2836
IssueApiKeyRequest,
2937
IssueApiKeyResponse,
3038
IssueJWTResponse,
3139
} from 'src/models/auth.dto';
40+
import {
41+
RequestNewAccountModel,
42+
NewAccountRequestResponse,
43+
NewAccountRequestsResponse,
44+
userTypeStringToProto,
45+
} from 'src/models/registration.dto';
3246
import { userLinkedToProject } from 'src/user_project_validator';
3347

3448
const { JWT_SERVICE_NAME } = JwtProto;
3549
const { API_KEY_SERVICE_NAME } = ApiKeyProto;
3650
const { PROJECT_SERVICE_NAME } = ProjectProto;
51+
const { ACCOUNT_REQUEST_SERVICE_NAME } = UserProto;
3752

3853
@ApiTags('auth')
3954
@Controller('auth')
4055
export class AuthController implements OnModuleInit {
4156
private jwtService: JwtProto.JwtServiceClient;
4257
private apiKeyService: ApiKeyProto.ApiKeyServiceClient;
4358
private projectService: ProjectProto.ProjectServiceClient;
59+
private accountRequestService: UserProto.AccountRequestServiceClient;
4460

4561
constructor(
4662
@Inject(JWT_SERVICE_NAME) private jwtClient: ClientGrpc,
4763
@Inject(API_KEY_SERVICE_NAME) private apiClient: ClientGrpc,
4864
@Inject(PROJECT_SERVICE_NAME) private projectClient: ClientGrpc,
65+
@Inject(ACCOUNT_REQUEST_SERVICE_NAME)
66+
private accountRequestClient: ClientGrpc,
4967
) {}
5068

5169
onModuleInit() {
@@ -59,6 +77,10 @@ export class AuthController implements OnModuleInit {
5977
this.projectClient.getService<ProjectProto.ProjectServiceClient>(
6078
PROJECT_SERVICE_NAME,
6179
);
80+
this.accountRequestService =
81+
this.accountRequestClient.getService<UserProto.AccountRequestServiceClient>(
82+
ACCOUNT_REQUEST_SERVICE_NAME,
83+
);
6284
}
6385

6486
@Post('/api_key/jwt')
@@ -255,4 +277,129 @@ export class AuthController implements OnModuleInit {
255277
},
256278
};
257279
}
280+
281+
@Post('/account-request')
282+
@ApiOperation({
283+
summary: 'Submit a new account request',
284+
description:
285+
'Allows a prospective user to submit a request for a new account. The request is stored and can be reviewed by an admin.',
286+
})
287+
@ApiCreatedResponse({
288+
description: 'Account request successfully created',
289+
type: NewAccountRequestResponse,
290+
})
291+
@ApiResponse({
292+
status: HttpStatus.BAD_REQUEST,
293+
description: 'Bad request',
294+
})
295+
@ApiBody({ type: RequestNewAccountModel })
296+
async createAccountRequest(
297+
@Body() body: RequestNewAccountModel,
298+
): Promise<NewAccountRequestResponse> {
299+
const created = await lastValueFrom(
300+
this.accountRequestService.createAccountRequest({
301+
email: body.email,
302+
name: body.name,
303+
password: body.password,
304+
userType: userTypeStringToProto(body.userType),
305+
projectName: body.projectName,
306+
}),
307+
);
308+
return new NewAccountRequestResponse(created);
309+
}
310+
311+
@Get('/account-request')
312+
@ApiOperation({
313+
summary: 'Retrieve all account requests',
314+
description:
315+
'Returns all pending account requests. Requires admin or superadmin credentials.',
316+
})
317+
@ApiResponse({
318+
status: HttpStatus.OK,
319+
description: 'All account requests returned',
320+
type: NewAccountRequestsResponse,
321+
})
322+
@ApiResponse({
323+
status: HttpStatus.UNAUTHORIZED,
324+
description: 'Unauthorized',
325+
})
326+
@ApiHeader({
327+
name: 'X-User-Email',
328+
description: 'Email of an admin or superadmin user',
329+
required: true,
330+
schema: { type: 'string' },
331+
})
332+
@ApiHeader({
333+
name: 'X-User-Password',
334+
description: 'Password of the admin or superadmin user',
335+
required: true,
336+
schema: { type: 'string' },
337+
})
338+
@ApiBearerAuth('API_Key')
339+
async getAllAccountRequests(
340+
@User() user: CommonProto.User,
341+
): Promise<NewAccountRequestsResponse> {
342+
if (!user || user.type === CommonProto.UserType.USER) {
343+
throw new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED);
344+
}
345+
const result = await lastValueFrom(
346+
this.accountRequestService.getAllAccountRequests({}),
347+
);
348+
return new NewAccountRequestsResponse(result);
349+
}
350+
351+
@Delete('/account-request/:id')
352+
@ApiOperation({
353+
summary: 'Delete an account request by ID',
354+
description:
355+
'Deletes an account request by its ID. Requires admin or superadmin credentials.',
356+
})
357+
@ApiParam({
358+
name: 'id',
359+
required: true,
360+
description: 'ID of the account request to delete',
361+
type: String,
362+
})
363+
@ApiResponse({
364+
status: HttpStatus.OK,
365+
description: 'Account request successfully deleted',
366+
type: NewAccountRequestResponse,
367+
})
368+
@ApiResponse({
369+
status: HttpStatus.NOT_FOUND,
370+
description: 'Account request not found',
371+
})
372+
@ApiResponse({
373+
status: HttpStatus.UNAUTHORIZED,
374+
description: 'Unauthorized',
375+
})
376+
@ApiHeader({
377+
name: 'X-User-Email',
378+
description: 'Email of an admin or superadmin user',
379+
required: true,
380+
schema: { type: 'string' },
381+
})
382+
@ApiHeader({
383+
name: 'X-User-Password',
384+
description: 'Password of the admin or superadmin user',
385+
required: true,
386+
schema: { type: 'string' },
387+
})
388+
@ApiBearerAuth('API_Key')
389+
async deleteAccountRequest(
390+
@User() user: CommonProto.User,
391+
@Param('id') idStr: string,
392+
): Promise<NewAccountRequestResponse> {
393+
if (!user || user.type === CommonProto.UserType.USER) {
394+
throw new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED);
395+
}
396+
const id = parseInt(idStr);
397+
if (Number.isNaN(id)) {
398+
throw new HttpException('id must be a number', HttpStatus.BAD_REQUEST);
399+
}
400+
const removed = await lastValueFrom(
401+
this.accountRequestService.deleteAccountRequest({ id }),
402+
);
403+
return new NewAccountRequestResponse(removed);
404+
}
258405
}

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ import { CredentialsMiddleware } from 'src/middleware/credentials.middleware';
2222

2323
const { JWT_SERVICE_NAME, JUNO_JWT_PACKAGE_NAME } = JwtProto;
2424
const { API_KEY_SERVICE_NAME, JUNO_API_KEY_PACKAGE_NAME } = ApiKeyProto;
25-
const { USER_AUTH_SERVICE_NAME, JUNO_USER_PACKAGE_NAME } = UserProto;
25+
const {
26+
USER_AUTH_SERVICE_NAME,
27+
ACCOUNT_REQUEST_SERVICE_NAME,
28+
JUNO_USER_PACKAGE_NAME,
29+
} = UserProto;
2630
const { PROJECT_SERVICE_NAME, JUNO_PROJECT_PACKAGE_NAME } = ProjectProto;
2731

2832
@Module({
@@ -67,6 +71,15 @@ const { PROJECT_SERVICE_NAME, JUNO_PROJECT_PACKAGE_NAME } = ProjectProto;
6771
protoPath: ProjectProtoFile,
6872
},
6973
},
74+
{
75+
name: ACCOUNT_REQUEST_SERVICE_NAME,
76+
transport: Transport.GRPC,
77+
options: {
78+
url: process.env.DB_SERVICE_ADDR,
79+
package: JUNO_USER_PACKAGE_NAME,
80+
protoPath: UserProtoFile,
81+
},
82+
},
7083
]),
7184
],
7285
controllers: [AuthController],
@@ -79,6 +92,8 @@ export class AuthModule implements NestModule {
7992
{ path: 'auth/key', method: RequestMethod.POST },
8093
{ path: 'auth/user/jwt', method: RequestMethod.POST },
8194
{ path: 'auth/test-auth', method: RequestMethod.GET },
95+
{ path: 'auth/account-request', method: RequestMethod.GET },
96+
{ path: 'auth/account-request/:id', method: RequestMethod.DELETE },
8297
);
8398
}
8499
}

0 commit comments

Comments
 (0)