Skip to content

Commit

Permalink
feat(face-validation): integrate face validation service and update t…
Browse files Browse the repository at this point in the history
…utor profile picture handling
  • Loading branch information
elskow committed Nov 26, 2024
1 parent 85a86de commit a05348a
Show file tree
Hide file tree
Showing 11 changed files with 199 additions and 3 deletions.
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ FIREBASE_DATABASE_URL=
GCS_BUCKET_NAME=
GROQ_KEY=
DATABASE_URL=
JWT_SECRET=
JWT_SECRET=
FACE_VALIDATION_URL=
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
},
"dependencies": {
"@google-cloud/storage": "^7.14.0",
"axios": "^1.7.8",
"bcryptjs": "^2.4.3",
"compression": "^1.7.5",
"cors": "^2.8.5",
Expand Down
25 changes: 25 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,6 @@ export const bucket =
process.env.NODE_ENV === "test"
? admin.storage().bucket(GCS_BUCKET_NAME) // use the firebase storage emulator
: new Storage().bucket(GCS_BUCKET_NAME);

export const FACE_VALIDATION_URL = process.env.FACE_VALIDATION_URL;
export const FACE_VALIDATION_ENABLED = !!FACE_VALIDATION_URL;
4 changes: 4 additions & 0 deletions src/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { FCMRepository } from "./common/fcm.repository";
import { TutoriesRepository } from "./module/tutories/tutories.repository";
import { OrderRepository } from "./module/order/order.repository";
import { ReviewRepository } from "./module/review/review.repository";
import { FaceValidationService } from "./module/face-validation/face-validation.interface";
import { createFaceValidationService } from "./module/face-validation/face-validation.factory";

interface Container {
authRepository: AuthRepository;
Expand All @@ -19,6 +21,7 @@ interface Container {
chatRepository: ChatRepository;
fcmRepository: FCMRepository;
reviewRepository: ReviewRepository;
faceValidationService: FaceValidationService;
}

let containerInstance: Container | null = null;
Expand All @@ -35,6 +38,7 @@ export const setupContainer = (): Container => {
chatRepository: new ChatRepository(db),
fcmRepository: new FCMRepository(db),
reviewRepository: new ReviewRepository(db),
faceValidationService: createFaceValidationService(),
};
}

Expand Down
34 changes: 34 additions & 0 deletions src/module/face-validation/face-validation.factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { FACE_VALIDATION_ENABLED, FACE_VALIDATION_URL } from "@/config";
import {
RemoteFaceValidationService,
NoOpFaceValidationService,
} from "./face-validation.service";
import { logger } from "@middleware/logging.middleware";
import axios from "axios";

export const createFaceValidationService = () => {
if (!FACE_VALIDATION_ENABLED) {
logger.info("Face validation is disabled, using NoOp service");
return new NoOpFaceValidationService();
}

const service = new RemoteFaceValidationService(FACE_VALIDATION_URL!);

try {
axios
.get(`${FACE_VALIDATION_URL}/health`, {
timeout: 5000,
})
.catch(() => {
throw new Error("Health check failed");
});

logger.info("Face validation service is healthy and ready");
return service;
} catch (error) {
logger.warn(
`Face validation service is unavailable: ${error}, falling back to NoOp service`,
);
return new NoOpFaceValidationService();
}
};
10 changes: 10 additions & 0 deletions src/module/face-validation/face-validation.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface FaceValidationResponse {
is_valid: boolean;
face_count: number;
message: string;
}

export interface FaceValidationService {
validateFace(imageBuffer: Buffer): Promise<FaceValidationResponse>;
checkHealth(): Promise<boolean>;
}
80 changes: 80 additions & 0 deletions src/module/face-validation/face-validation.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import axios from "axios";
import {
FaceValidationService,
FaceValidationResponse,
} from "./face-validation.interface";
import { logger } from "@middleware/logging.middleware";

export class RemoteFaceValidationService implements FaceValidationService {
constructor(private readonly baseUrl: string) {}

async validateFace(imageBuffer: Buffer): Promise<FaceValidationResponse> {
try {
const imageBase64 = imageBuffer.toString("base64");
const response = await axios.post<{
is_valid: boolean;
face_count: number;
message: string;
}>(
`${this.baseUrl}/validate-face`,
{ image: imageBase64 },
{
headers: { "Content-Type": "application/json" },
timeout: 5000,
},
);

return {
is_valid: response.data.is_valid,
face_count: response.data.face_count,
message: response.data.message,
};
} catch (error) {
logger.error("Face validation service error:", error);
throw new Error("Face validation service unavailable");
}
}

checkHealthSync(): boolean {
try {
const xhr = new XMLHttpRequest();
xhr.open("GET", `${this.baseUrl}/health`, false); // Make the request synchronous
xhr.timeout = 5000;
xhr.send(null);

return xhr.status === 200;
} catch (error) {
logger.warn("Health check failed:", error);
return false;
}
}

async checkHealth(): Promise<boolean> {
try {
const response = await axios.get(`${this.baseUrl}/health`, {
timeout: 5000,
});
return response.data.status === "healthy";
} catch (error) {
return false;
}
}
}

export class NoOpFaceValidationService implements FaceValidationService {
async validateFace(_imageBuffer: Buffer): Promise<FaceValidationResponse> {
return {
is_valid: true,
face_count: 0,
message: "Face validation is not enabled",
};
}

checkHealthSync(): boolean {
return true;
}

async checkHealth(): Promise<boolean> {
return true;
}
}
11 changes: 10 additions & 1 deletion src/module/tutor/tutor.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import { TutorService } from "@/module/tutor/tutor.service";
import { RequestHandler } from "express";
import { z } from "zod";
import { container } from "@/container";
import { ValidationError } from "./tutor.error";

const tutorService = new TutorService({
tutorRepository: container.tutorRepository,
downscaleImage,
bucket,
faceValidation: container.faceValidationService,
});

type UpdateTutorProfileSchema = z.infer<typeof updateProfileSchema>;
Expand Down Expand Up @@ -50,8 +52,15 @@ export const updateProfilePicture: RequestHandler = async (req, res) => {
data: { url },
});
} catch (error) {
logger.error(`Failed to upload profile picture: ${error}`);
if (error instanceof ValidationError) {
res.status(400).json({
status: "fail",
message: error.message,
});
return;
}

logger.error(`Failed to upload profile picture: ${error}`);
res.status(500).json({
status: "error",
message: "Failed to upload profile picture",
Expand Down
10 changes: 10 additions & 0 deletions src/module/tutor/tutor.error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export class ValidationError extends Error {
constructor(message: string) {
super(message);
this.name = "ValidationError";
}

static isFaceValidationError(error: unknown): error is ValidationError {
return error instanceof ValidationError;
}
}
21 changes: 20 additions & 1 deletion src/module/tutor/tutor.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,32 @@ import { z } from "zod";
import { TutorRepository } from "./tutor.repository";
import { AuthRepository } from "../auth/auth.repository";
import { hash } from "bcryptjs";
import { FaceValidationService } from "@/module/face-validation/face-validation.interface";
import { ValidationError } from "./tutor.error";

export interface TutorServiceDependencies {
tutorRepository: TutorRepository;
bucket: Bucket;
downscaleImage: (imageBuffer: Buffer) => Promise<Buffer>;
faceValidation: FaceValidationService;
}

export class TutorService {
private tutorRepository: TutorRepository;
private bucket: Bucket;
private downscaleImage: (imageBuffer: Buffer) => Promise<Buffer>;
private faceValidation: FaceValidationService;

constructor({
tutorRepository,
bucket,
downscaleImage,
faceValidation,
}: TutorServiceDependencies) {
this.tutorRepository = tutorRepository;
this.bucket = bucket;
this.downscaleImage = downscaleImage;
this.faceValidation = faceValidation;
}

async updateProfile(
Expand All @@ -33,16 +39,29 @@ export class TutorService {
await this.tutorRepository.updateTutorProfile(userId, data);
}

async updateProfilePicture(file: Express.Multer.File, userId: string) {
async updateProfilePicture(
file: Express.Multer.File,
userId: string,
): Promise<string> {
const name = `profile-pictures/${userId}.jpg`;

try {
const image = await this.downscaleImage(file.buffer);

const valResult = await this.faceValidation.validateFace(file.buffer);
if (!valResult.is_valid) {
throw new ValidationError(
valResult.message || "Face validation failed",
);
}
const bucketFile = this.bucket.file(name);
await bucketFile.save(image, { public: true });

return bucketFile.publicUrl();
} catch (error) {
if (error instanceof ValidationError) {
throw error;
}
throw new Error(`Failed to update profile picture: ${error}`);
}
}
Expand Down

0 comments on commit a05348a

Please sign in to comment.