diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index 6a2b296..fc1dbb4 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -6,44 +6,7 @@ on: - main jobs: - # run-tests: - # runs-on: ubuntu-latest - - # steps: - # - name: Checkout code - # uses: actions/checkout@v4 - - # - name: Set up Node.js - # uses: actions/setup-node@v4 - # with: - # node-version: '21' - - # - name: Test appointment service - # run: | - # cd services/appointment - # yarn install --frozen-lockfile - # yarn test - - # - name: Test user service - # run: | - # cd services/user - # yarn install --frozen-lockfile - # yarn test - - # - name: Test auth service - # run: | - # cd services/auth - # yarn install --frozen-lockfile - # yarn test - - # - name: Test email service - # run: | - # cd services/email - # yarn install --frozen-lockfile - # yarn test - build-and-push: - # needs: run-tests runs-on: ubuntu-latest steps: - name: Checkout code diff --git a/README.md b/README.md index 069137e..f281963 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ ## [Basic System Desin](https://ali-akkas.notion.site/Healthcare-Appointment-Scheduling-System-cf67ead3bb1947f58f505c18fb886280?pvs=4) - ## Overview + - The Healthcare Appointment Scheduling System employs a microservices architecture with Docker containers for modularization and scalability. - CI/CD pipelines ensure automated and reliable deployment of updates and features. - RabbitMQ enables asynchronous communication between services, enhancing system resilience and responsiveness. @@ -22,31 +22,100 @@ - [Docker](https://www.docker.com/) - Docker is a platform designed to help developers build, share, and run container applications. - [Redis](https://redis.io/) - Redis is a source-available, in-memory storage, used as a distributed, in-memory key–value database, cache and message broker, with optional durability. - [RabbitMQ](https://www.rabbitmq.com/) - RabbitMQ is another widely used open source message broker, employed by several companies worldwide. +- [GitHub Actions](https://github.com/features/actions) - GitHub Actions is a continuous integration and continuous delivery (CI/CD) platform that allows to automate build, test, and deployment pipeline. +- [Postman](https://www.postman.com/) - Postman is an application that allows the testing of web APIs. ## File Structure ``` Healthcare-Appointment-Scheduling-System/ ├── .github + └── workflows + └── cd.yaml ├── api-gateway └── services/ - ├── auth - ├── user + ├── auth/ + ├── prisma + ├── src/ + ├── controllers + ├── lib + ├── routes + ├── app.ts + ├── index.ts + ├── config.ts + ├── tests + ├── package.json + ├── Dockerfile + ├── user + ├── prisma + ├── src/ + ├── controllers + ├── lib + ├── routes + ├── app.ts + ├── index.ts + ├── config.ts + ├── tests + ├── package.json + ├── Dockerfile ├── email + ├── prisma + ├── src/ + ├── controllers + ├── lib + ├── routes + ├── app.ts + ├── index.ts + ├── config.ts + ├── tests + ├── package.json + ├── Dockerfile ├── appointment + ├── prisma + ├── src/ + ├── controllers + ├── lib + ├── routes + ├── app.ts + ├── index.ts + ├── config.ts + ├── tests + ├── package.json + ├── Dockerfile ├── notification + ├── prisma + ├── src/ + ├── controllers + ├── lib + ├── routes + ├── app.ts + ├── index.ts + ├── config.ts + ├── tests + ├── package.json + ├── Dockerfile +├── docker-compose.yaml +├── README.md ``` ## Setup + follow .env.example file for setup environment variables ### Run the `Microservices Dependency` + ```bash docker-compose up ``` ### Run the `Tests` -``` + +```bash yarn run test .\tests\**\**\* ``` +### Automatic `CI/CD` + +``` +When push the code in the GitHub automatic run the actions +``` diff --git a/services/appointment/src/routes/index.ts b/services/appointment/src/routes/index.ts index eee47db..afffa54 100644 --- a/services/appointment/src/routes/index.ts +++ b/services/appointment/src/routes/index.ts @@ -17,12 +17,12 @@ router .post('/appointments', createAppointment) .get('/appointments', getAppointments); -router.get('/appointments/:patientId', getAppointmentsByPatientId); - router .get('/appointments/:id', getAppointmentById) .delete('/appointments/:id', deleteAppointmentById); +router.get('/appointments/:patientId', getAppointmentsByPatientId); + router.post('/patients', createPatient).get('/patients', getPatients); router.post('/providers', createProvider).get('/providers', getProviders); diff --git a/services/auth/.env.example b/services/auth/.env.example index 6fd5514..8e43eb4 100644 --- a/services/auth/.env.example +++ b/services/auth/.env.example @@ -1,2 +1,4 @@ -DATABASE_URL="postgresql://admin:password@localhost:5432/auth_db?schema=public" -JWT_SECRET=Secret_Key \ No newline at end of file +DATABASE_URL="postgresql://admin:password@postgres:5432/auth_db?schema=public" +JWT_SECRET=Secret_Key +USER_SERVICE_URL='http://user:4000' +EMAIL_SERVICE_URL='http://email:4006' \ No newline at end of file diff --git a/services/auth/src/controllers/login.ts b/services/auth/src/controllers/login.ts index 51d151f..60b5e62 100644 --- a/services/auth/src/controllers/login.ts +++ b/services/auth/src/controllers/login.ts @@ -4,7 +4,8 @@ import loginService from '@/lib/LoginService'; const login = async (req: Request, res: Response, next: NextFunction) => { try { - const ipAddress = (req.headers['x-forwarded-for'] as string) || req.ip || ''; + const ipAddress = + (req.headers['x-forwarded-for'] as string) || req.ip || ''; const userAgent = req.headers['user-agent'] || ''; // Validate the request body diff --git a/services/auth/src/controllers/register.ts b/services/auth/src/controllers/register.ts index 3ef813e..7e5e119 100644 --- a/services/auth/src/controllers/register.ts +++ b/services/auth/src/controllers/register.ts @@ -1,13 +1,14 @@ -import { Request, Response, NextFunction } from 'express'; +import { Response, Request, NextFunction } from 'express'; import { UserCreateSchema } from '@/schemas'; import registrationService from '@/lib/RegistrationService'; +import emailService from '@/lib/EmailService'; const register = async (req: Request, res: Response, next: NextFunction) => { try { - // Validate request body + // Validate the request body const parsedBody = UserCreateSchema.safeParse(req.body); if (!parsedBody.success) { - return res.status(400).json({ errors: parsedBody.error }); + return res.status(400).json({ errors: parsedBody.error.errors }); } const { name, email } = parsedBody.data; @@ -15,28 +16,26 @@ const register = async (req: Request, res: Response, next: NextFunction) => { // Check if the user already exists const existingUser = await registrationService.checkExistingUser(email); if (existingUser) { - return res.status(400).json({ errors: 'User already exists!' }); + return res.status(400).json({ message: 'User already exists' }); } - // Create the auth user + // Create the auth user and user profile const user = await registrationService.createUser(parsedBody.data); - - // Create the user profile by calling the user service await registrationService.createUserProfile(user.id, name, email); // Generate verification code - await registrationService.createVerificationCode(user.id); + const code = emailService.generateVerificationCode(); + await emailService.createVerificationCode(user.id, code); - // Send the verification email - await registrationService.sendVerificationEmail(email); + // Send verification email + await emailService.sendVerificationEmail(email, code); - // Return the response return res.status(201).json({ message: 'User created. Check your email for verification code', user, }); - } catch (err) { - next(err); + } catch (error) { + next(error); } }; diff --git a/services/auth/src/controllers/verifyToken.ts b/services/auth/src/controllers/verifyToken.ts index 83446f3..dda6dc6 100644 --- a/services/auth/src/controllers/verifyToken.ts +++ b/services/auth/src/controllers/verifyToken.ts @@ -2,29 +2,25 @@ import { Request, Response, NextFunction } from 'express'; import { AccessTokenSchema } from '@/schemas'; import tokenService from '@/lib/TokenService'; -const verifyToken = (req: Request, res: Response, next: NextFunction) => { +const verifyToken = async (req: Request, res: Response, next: NextFunction) => { try { // Validate the request body const parsedBody = AccessTokenSchema.safeParse(req.body); - if (!parsedBody.success) { return res.status(400).json({ errors: parsedBody.error.errors }); } const { accessToken } = parsedBody.data; - - // Verify the access token const decoded = tokenService.validateAccessToken(accessToken); - const user = tokenService.getUserFromToken(decoded); + const user = await tokenService.getUserFromToken(decoded); if (!user) { return res.status(401).json({ message: 'Unauthorized' }); } - // Return the user return res.status(200).json({ message: 'Authorized', user }); - } catch (err) { - next(err); + } catch (error) { + next(error); } }; diff --git a/services/auth/src/lib/EmailService.ts b/services/auth/src/lib/EmailService.ts index ba8ac8a..921d595 100644 --- a/services/auth/src/lib/EmailService.ts +++ b/services/auth/src/lib/EmailService.ts @@ -3,6 +3,41 @@ import prisma from '@/prisma'; import axios from 'axios'; class EmailService { + /** + * Generates a random 5-digit verification code. + */ + public generateVerificationCode() { + const randomNum = Math.floor(10000 + Math.random() * 90000); + return randomNum.toString(); + } + + /** + * Create verification code and save it to the database + */ + public async createVerificationCode(userId: string, code: string) { + await prisma.verificationCode.create({ + data: { + userId, + code, + expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24), // 24 hours + }, + }); + } + + /** + * Send the verification email. + */ + public async sendVerificationEmail(recipient: string, code: string) { + console.log('Sending verification email to:', recipient); + + await axios.post(`${EMAIL_SERVICE}/emails/send`, { + recipient, + subject: 'Email Verification', + body: `Your verification code is ${code}`, + source: 'user-registration', + }); + } + /** * Verify user email via verification code */ @@ -22,8 +57,6 @@ class EmailService { }, }); - console.log('verificationCode: ', verificationCode); - if (!verificationCode) { throw new Error('Invalid verification code'); } @@ -35,7 +68,6 @@ class EmailService { return { user, verificationCode }; } - /** * Update user account status */ diff --git a/services/auth/src/lib/RegistrationService.ts b/services/auth/src/lib/RegistrationService.ts index 873d8ce..2ef3b74 100644 --- a/services/auth/src/lib/RegistrationService.ts +++ b/services/auth/src/lib/RegistrationService.ts @@ -4,14 +4,6 @@ import axios from 'axios'; import bcrypt from 'bcryptjs'; class RegistrationService { - /** - * Generates a random 5-digit verification code. - */ - private generateVerificationCode() { - const randomNum = Math.floor(10000 + Math.random() * 90000); - return randomNum.toString(); - } - /** * Check if a user with the given email already exists. */ @@ -34,7 +26,7 @@ class RegistrationService { } /** - * Create a auth user. + * Create an auth user. */ public async createUser(userData) { const user = await prisma.user.create({ @@ -67,32 +59,6 @@ class RegistrationService { console.log('USER:', user.data); return user; } - - /** - * Create a verification code. - */ - public async createVerificationCode(userId) { - const verificationCode = await prisma.verificationCode.create({ - data: { - userId, - code: this.generateVerificationCode(), - expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24), // 24 hours - }, - }); - return verificationCode; - } - - /** - * Send the verification email. - */ - public async sendVerificationEmail(recipient: string) { - await axios.post(`${EMAIL_SERVICE}/emails/send`, { - recipient, - subject: 'Email Verification', - body: `Your verification code is ${this.generateVerificationCode()}`, - source: 'user-registration', - }); - } } const registrationService = new RegistrationService(); diff --git a/services/auth/src/lib/TokenService.ts b/services/auth/src/lib/TokenService.ts index 79a7376..432e5c2 100644 --- a/services/auth/src/lib/TokenService.ts +++ b/services/auth/src/lib/TokenService.ts @@ -12,9 +12,9 @@ class TokenService { /** * Get user from access token */ - public async getUserFromToken(decodedToken: any) { + public async getUserFromToken(decoded: any) { const user = await prisma.user.findUnique({ - where: { id: decodedToken.id }, + where: { id: (decoded as any).userId }, select: { id: true, email: true, @@ -22,6 +22,7 @@ class TokenService { role: true, }, }); + return user; } } diff --git a/services/email/prisma/schema.prisma b/services/email/prisma/schema.prisma index df9f0d7..bd695d9 100644 --- a/services/email/prisma/schema.prisma +++ b/services/email/prisma/schema.prisma @@ -2,6 +2,7 @@ generator client { provider = "prisma-client-js" + binaryTargets = ["native", "windows"] } datasource db { diff --git a/services/user/src/routes/index.ts b/services/user/src/routes/index.ts index 83aefe6..9d0206a 100644 --- a/services/user/src/routes/index.ts +++ b/services/user/src/routes/index.ts @@ -13,6 +13,7 @@ router .get('/users/:id', getUserById) .put('/users/:id', updateUserById) .delete('/users/:id', deleteUserById); + router.post('/users', createUser).get('/users', getUsers); export default router; diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index 4c378ba..0000000 --- a/yarn.lock +++ /dev/null @@ -1,13 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -express-rate-limit@^7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-7.2.0.tgz#06ce387dd5388f429cab8263c514fc07bf90a445" - integrity sha512-T7nul1t4TNyfZMJ7pKRKkdeVJWa2CqB8NA1P8BwYaoDI5QSBZARv5oMS43J7b7I5P+4asjVXjb7ONuwDKucahg== - -helmet@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/helmet/-/helmet-7.1.0.tgz#287279e00f8a3763d5dccbaf1e5ee39b8c3784ca" - integrity sha512-g+HZqgfbpXdCkme/Cd/mZkV0aV3BZZZSugecH03kl38m/Kmdx8jKjBikpDj2cr+Iynv4KpYEviojNdTJActJAg==