Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 📢 NotifierAPI Backend

A robust, enterprise-grade Node.js backend API for managing user authentication, announcements, and notifications. Built with **Express.js** and **Prisma ORM**, this system is engineered for high availability, security, and scalability.
A robust, enterprise-grade Node.js backend API for managing user authentication, announcements, and notifications. Built with **Express.js** and **Prisma ORM**, this system is engineered for high availability, security, and scalability. It is fully **deployed and hosted on Microsoft Azure**, leveraging cloud-native features for optimal performance.

---

Expand All @@ -23,6 +23,17 @@ A robust, enterprise-grade Node.js backend API for managing user authentication,

---

## ☁️ Azure Cloud Deployment

NotifierAPI is designed for and actively **deployed on Microsoft Azure**. The deployment architecture takes advantage of Azure's robust ecosystem to ensure enterprise-grade reliability:

- **Azure Virtual Machine:** The backend Node.js API is containerized via Docker and orchestrated on Azure for seamless scaling and zero-downtime deployments.
- **Neon Database for PostgreSQL:** A fully managed, highly available PostgreSQL instance ensures secure and resilient data storage.
- **Azure Cache for Redis:** Provides a distributed, low-latency rate-limiting and caching layer, protecting the API from abuse while keeping response times minimal.
- **CI/CD Integration:** Automated deployment pipelines ensure seamless updates directly to the Azure environment.

---

## 🏗️ Backend Architecture

NotifierAPI utilizes a modern, scalable Request Flow Architecture:
Expand Down Expand Up @@ -144,4 +155,4 @@ Interactive API documentation is generated dynamically from Zod schemas. Once th
├── docker-compose.yml # Docker Compose configuration
├── Dockerfile # Container build instructions
└── package.json # Dependencies and scripts
```
```
2 changes: 1 addition & 1 deletion src/controllers/announcements/getAdmin.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const getAdminAnnouncements = async (req, res) => {
page,
limit,
totalPages: Math.ceil(total / limit),
next: limit * page < total,
hasNext: limit * page < total,
},
},
});
Expand Down
2 changes: 1 addition & 1 deletion src/controllers/users/profilePic.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const uploadProfilePicture = async (req, res) => {
throw new AppError("Please upload a file", 400);
}

if (!file.mimetype.startsWith("profilePicture/")) {
if (!file.mimetype.startsWith("image/")) {
throw new AppError("Profile picture must be an image", 400);
}

Expand Down
47 changes: 47 additions & 0 deletions src/swagger/docs/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,34 @@ export const userSwaggerDocs = {
},
},

uploadProfilePicture: {
method: "post",
path: "/user/me/profile-picture",
tags: ["Users"],
summary: "Upload profile picture",
security: [{ bearerAuth: [] }],
request: {
body: {
content: {
"multipart/form-data": {
schema: z.object({
profilePicture: z.string().openapi({
format: "binary",
description: "Profile picture image file",
}),
}),
},
},
},
},
responses: {
200: {
description: "Profile picture uploaded successfully",
content: { "application/json": { schema: getMyselfResponseSchema } },
},
},
},

getUsers: {
method: "get",
path: "/user",
Expand All @@ -40,6 +68,25 @@ export const userSwaggerDocs = {
},
},

getUser: {
method: "get",
path: "/user/{id}",
tags: ["Users"],
summary: "Get a user by ID (Admin)",
security: [{ bearerAuth: [] }],
request: {
params: z.object({
id: z.string().openapi({ description: "User ID" }),
}),
},
responses: {
200: {
description: "User details",
content: { "application/json": { schema: getMyselfResponseSchema } },
},
},
},

deleteUser: {
method: "delete",
path: "/user/{id}",
Expand Down
17 changes: 14 additions & 3 deletions src/validators/responses/announcementResponses.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@ import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi";

extendZodWithOpenApi(z);

export const attachmentSchema = z.object({
filename: z.string().openapi({ example: "document.pdf" }),
fileUrl: z
.string()
.openapi({ example: "/announcements/cm0z.../document.pdf" }),
fileType: z.string().openapi({ example: "application/pdf" }),
signedUrl: z.string().url().openapi({
example: "https://supabase.../document.pdf?token=...",
description: "Signed URL valid for 1 hour",
}),
});

export const announcementBaseSchema = z.object({
id: z.string().openapi({ example: "cm0z..." }),
title: z.string().openapi({ example: "System Maintenance" }),
Expand Down Expand Up @@ -80,9 +92,8 @@ export const getAnnouncementByIdResponseSchema = z
.omit({ isRead: true, userId: true, updatedAt: true })
.extend({
isRead: z.boolean().optional(),
readAt: z.string().optional(),
attachments: z.array(z.any()).optional(),
submission: z.any().optional(),
readAt: z.string().nullable().optional(),
attachments: z.array(attachmentSchema).optional(),
}),
}),
})
Expand Down
10 changes: 9 additions & 1 deletion src/validators/responses/userResponses.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,15 @@ export const userProfileSchema = z.object({
id: z.string().openapi({ example: "cm0z..." }),
name: z.string().openapi({ example: "John Doe" }),
email: z.string().email().openapi({ example: "user@example.com" }),
role: z.string().openapi({ example: "USER" }),
isEmailVerified: z.boolean().openapi({ example: false }),
profilePictureUrl: z.string().nullable().openapi({
example: "https://...",
description: "Profile picture URL with cache-busting timestamp appended",
}),
updatedAt: z
.string()
.datetime()
.openapi({ example: "2026-04-10T10:00:00.000Z" }),
});

export const getMyselfResponseSchema = z
Expand Down
Loading