Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#3981 - Model SDPR API #4018

Merged
merged 10 commits into from
Dec 5, 2024
38 changes: 38 additions & 0 deletions devops/openshift/api-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,38 @@ objects:
to:
kind: Service
name: ${NAME}
- apiVersion: route.openshift.io/v1
kind: Route
metadata:
name: ${EXTERNAL_SWAGGER_NAME}-route
annotations:
haproxy.router.openshift.io/balance: leastconn
haproxy.router.openshift.io/disable_cookies: "true"
haproxy.router.openshift.io/hsts_header: max-age=31536000;includeSubDomains;preload
haproxy.router.openshift.io/timeout: 30s
spec:
host: ${HOST_NAME}
path: ${EXTERNAL_SWAGGER_PATH}
port:
targetPort: http
tls:
insecureEdgeTerminationPolicy: Redirect
termination: edge
certificate: |
-----BEGIN CERTIFICATE-----
${TLS_CERTIFICATE}
-----END CERTIFICATE-----
key: |
-----BEGIN PRIVATE KEY-----
${TLS_KEY}
-----END PRIVATE KEY-----
caCertificate: |
-----BEGIN CERTIFICATE-----
${TLS_CA_CERTIFICATE}
-----END CERTIFICATE-----
to:
kind: Service
name: ${NAME}
- apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
Expand Down Expand Up @@ -345,6 +377,12 @@ parameters:
- name: SWAGGER_PATH
value: /swagger
required: true
- name: EXTERNAL_SWAGGER_NAME
value: external-swagger
required: true
- name: EXTERNAL_SWAGGER_PATH
value: /external/swagger
required: true
- name: PROJECT
value: sims
- name: SERVICE_NAME
Expand Down
11 changes: 11 additions & 0 deletions sources/packages/backend/apps/api/src/app.external.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Module } from "@nestjs/common";
import { StudentExternalController } from "./route-controllers";
import { AuthModule } from "./auth/auth.module";
import { StudentService } from "@sims/integrations/services";

@Module({
imports: [AuthModule],
controllers: [StudentExternalController],
providers: [StudentService],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open for discussion but we can keep those services at the API scope at first.
We can create some specific services for the external module, but I am not sure if we should start creating them in the integrations lib directly.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dheepak-aot can you please collaborate on this?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @andrewsignori-aot, current scope of the services that serve the external APIs can be in API scope.

})
export class AppExternalModule {}
6 changes: 6 additions & 0 deletions sources/packages/backend/apps/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { ConfigModule } from "@sims/utilities/config";
import { DatabaseModule } from "@sims/sims-db";
import { NotificationsModule } from "@sims/services/notifications";
import { QueueModule } from "@sims/services/queue";
import { AppExternalModule } from "./app.external.module";

@Module({
imports: [
Expand All @@ -43,6 +44,7 @@ import { QueueModule } from "@sims/services/queue";
AppAESTModule,
AppInstitutionsModule,
AppStudentsModule,
AppExternalModule,
AppSupportingUsersModule,
QueueModule,
ClamAntivirusModule,
Expand All @@ -63,6 +65,10 @@ import { QueueModule } from "@sims/services/queue";
path: ClientTypeBaseRoute.SupportingUser,
module: AppSupportingUsersModule,
},
{
path: ClientTypeBaseRoute.External,
module: AppExternalModule,
},
]),
],
controllers: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export enum AuthorizedParties {
student = "student",
aest = "aest",
supportingUsers = "supporting-users",
external = "external",
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ export class AuthorizedPartiesGuard implements CanActivate {
authorizedParty: AuthorizedParties,
identityProvider: IdentityProviders,
): boolean {
if (authorizedParty === AuthorizedParties.external) {
return true;
}
switch (authorizedParty) {
case AuthorizedParties.student:
if (
Expand Down
7 changes: 7 additions & 0 deletions sources/packages/backend/apps/api/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Request, Response } from "express";
import { KeycloakConfig } from "@sims/auth/config";
import helmet from "helmet";
import { SystemUsersService } from "@sims/services";
import { AppExternalModule } from "./app.external.module";

async function bootstrap() {
await KeycloakConfig.load();
Expand Down Expand Up @@ -87,6 +88,12 @@ async function bootstrap() {
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup("swagger", app, document);

// External swagger
const externalDocument = SwaggerModule.createDocument(app, options, {
include: [AppExternalModule], // Includes only AppExternalModule.
});
SwaggerModule.setup("external/swagger", app, externalDocument);
}

// Starting application
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,4 @@ export * from "./program-year/program-year.controller.service";
export * from "./cas-supplier/cas-supplier.aest.controller";
export * from "./application-restriction-bypass/application-restriction-bypass.aest.controller";
export * from "./audit/audit.controller";
export * from "./student/student.external.controller";
Original file line number Diff line number Diff line change
Expand Up @@ -300,3 +300,23 @@ export class UpdateStudentDetailsAPIInDTO {
@MaxLength(NOTE_DESCRIPTION_MAX_LENGTH)
noteDescription: string;
}

/**
* Student details.
*/
export class StudentDetailsAPIOutDTO {
firstName: string;
lastName: string;
sin: string;
dateOfBirth: string;
address: AddressAPIOutDTO;
applicationNumbers: string[];
}

/**
* Student external search parameters.
*/
export class ExternalSearchStudentAPIInDTO {
@IsValidSIN()
sin: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {
Body,
Controller,
HttpCode,
HttpStatus,
NotFoundException,
Post,
} from "@nestjs/common";
import { ApiNotFoundResponse, ApiTags } from "@nestjs/swagger";
import { ClientTypeBaseRoute } from "../../types";
import { AuthorizedParties } from "../../auth/authorized-parties.enum";
import {
AllowAuthorizedParty,
RequiresUserAccount,
} from "../../auth/decorators";
import BaseController from "../BaseController";
import { StudentService } from "@sims/integrations/services";
import {
ExternalSearchStudentAPIInDTO,
StudentDetailsAPIOutDTO,
} from "./models/student.dto";

/**
* Student controller for external client.
*/
@RequiresUserAccount(false)
@AllowAuthorizedParty(AuthorizedParties.external)
@Controller("student")
@ApiTags(`${ClientTypeBaseRoute.External}-student`)
export class StudentExternalController extends BaseController {
constructor(private readonly studentService: StudentService) {
super();
}

/**
* Searches for student details.
* This request method is POST to avoid passing sensitive data in the URL.
* @param payload payload with SIN to retrieve the student details.
* @returns student details.
*/
@Post()
@ApiNotFoundResponse({ description: "Student not found." })
@HttpCode(HttpStatus.OK)
async searchStudentDetails(
@Body() payload: ExternalSearchStudentAPIInDTO,
): Promise<StudentDetailsAPIOutDTO> {
const student = await this.studentService.getStudentBySIN(payload.sin);
if (!student) {
throw new NotFoundException("Student not found.");
}
return {
firstName: student.user.firstName,
lastName: student.user.lastName,
sin: student.sinValidation.sin,
dateOfBirth: student.birthDate,
address: {
addressLine1: student.contactInfo.address.addressLine1,
addressLine2: student.contactInfo.address.addressLine2,
city: student.contactInfo.address.city,
provinceState: student.contactInfo.address.provinceState,
country: student.contactInfo.address.country,
postalCode: student.contactInfo.address.postalCode,
},
applicationNumbers: student.applications.map((a) => a.applicationNumber),
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export enum ClientTypeBaseRoute {
Institution = "institutions",
AEST = "aest",
SupportingUser = "supporting-users",
External = "external",
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,30 @@ export class StudentService {
});
}

/**
* Gets student by SIN.
* @param sin student's SIN.
* @returns student.
*/
async getStudentBySIN(sin: string): Promise<Student> {
return this.studentRepo.findOne({
select: {
id: true,
sinValidation: { id: true, sin: true },
user: { id: true, firstName: true, lastName: true, email: true },
birthDate: true,
contactInfo: true as unknown,
applications: { applicationNumber: true },
},
relations: {
sinValidation: true,
user: true,
applications: true,
},
where: { sinValidation: { sin } },
});
}

/**
* Gets all the students that have the SIN validation pending.
* @returns Students pending SIN validation.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
StudentRestriction,
CASSupplier,
DisbursementOveraward,
Application,
} from ".";
import { SINValidation } from "./sin-validation.model";

Expand Down Expand Up @@ -140,4 +141,13 @@ export class Student extends RecordDataModel {
cascade: false,
})
overawards?: DisbursementOveraward[];

/**
* Student applications.
*/
@OneToMany(() => Application, (application) => application.student, {
eager: false,
cascade: false,
})
applications?: Application[];
}
Loading