diff --git a/.gitignore b/.gitignore index 13f0b035..f329398a 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,11 @@ package/ lambda_package/ *.zip +# Java / Maven +target/ +*.class +.mvn/wrapper/maven-wrapper.jar + # Node / frontend node_modules/ .next/ diff --git a/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/API_CONTRACT.md b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/API_CONTRACT.md new file mode 100644 index 00000000..8ef8a148 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/API_CONTRACT.md @@ -0,0 +1,380 @@ +# Group 1 Notice Board / Trainee Tracker — API Contract + +## Purpose + +This file defines the shared contract between the React frontend, Java/Spring Boot backend, MongoDB layer, deployment configuration, and CI/CD. + +Do not rename shared fields, endpoints, class names, or environment variables without updating this file and notifying the team. + +**Status:** this replaces an earlier draft of this contract that assumed a single `Notice` resource. The backend actually implemented (Aaron) is a multi-resource trainee-onboarding tracker, matching the class layout already committed to `feature/backend`. This version documents what's real as of 2026-08-28 so Alena/Danielle/Mark can build against it. + +--- + +## 1. Backend Stack + +```text +Language: Java 17 +Framework: Spring Boot 3.3.4 +Build tool: Maven (pom.xml) — not Gradle +Package root: com.edtech.noticeboard +Database: MongoDB Atlas +``` + +Build command (from `backend/`): + +```bash +mvn clean package +``` + +Produces: + +```text +backend/target/noticeboard-backend-0.0.1-SNAPSHOT.jar +``` + +Dev startup: + +```bash +mvn spring-boot:run +``` + +Production startup: + +```bash +java -jar target/noticeboard-backend-0.0.1-SNAPSHOT.jar +``` + +Backend port: + +```text +8080 +``` + +--- + +## 2. Resources + +Four resources exist as MongoDB `@Document` models. **Only `TrainingPlan` has working CRUD right now** — the other three have models + repositories but empty controllers/services (stubs, not yet implemented). See section 4 for exact implementation status per resource. + +| Resource | Owner | Status | +|---|---|---| +| `TrainingPlan` | Aaron | **Implemented** — full CRUD, tested against Atlas | +| `User` | Mark | Model + repository only | +| `Cohort` | Mark | Model + repository only | +| `ProgressLog` | Aaron / Danielle | Model + repository only — **field shape not yet finalized, see section 9** | + +--- + +## 3. Resource Fields + +### TrainingPlan (implemented) + +```text +id String — generated by MongoDB +title String — required, must not be blank +modules List +milestones List +``` + +Note: there is no `createdAt` field on `TrainingPlan`. If the frontend needs one, tell Aaron before assuming it exists. + +### User (stub — not yet exposed via any endpoint) + +```text +id String +name String +email String +passwordHash String +role String — TRAINEE, HR, or MANAGER +``` + +### Cohort (stub — not yet exposed via any endpoint) + +```text +id String +name String +studentIds List +``` + +### ProgressLog (stub — not yet exposed via any endpoint) + +```text +id String +traineeId String +moduleId String +status String — NOT_STARTED, IN_PROGRESS, BLOCKED, or COMPLETED +submissionNote String +updatedAt Instant +``` + +**This field list is a guess Aaron made early on, not something confirmed with Danielle.** Aaron and Danielle must agree on the real shape before `ProgressController`/`ProgressService` get implemented — do not assume these fields are final. + +--- + +## 4. REST Endpoints + +### Curriculum API — `TrainingPlan` (implemented) + +Backend class names: + +```text +TrainingPlan +CurriculumController +CurriculumService +TrainingPlanRepository +``` + +| Method | Path | Purpose | Status | +|---|---|---|---| +| POST | `/api/curriculum` | Create a TrainingPlan | 201 Created | +| GET | `/api/curriculum` | List all TrainingPlans | 200 OK | +| GET | `/api/curriculum/{id}` | Read one TrainingPlan | 200 OK / 404 | +| PUT | `/api/curriculum/{id}` | Update a TrainingPlan | 200 OK / 404 | +| DELETE | `/api/curriculum/{id}` | Delete a TrainingPlan | 204 No Content / 404 | + +#### Create + +```http +POST /api/curriculum +Content-Type: application/json + +{ + "title": "Onboarding Week 1", + "modules": ["Git", "Java Basics"], + "milestones": ["Setup complete"] +} +``` + +Do not send `id` — it's generated by MongoDB. + +Response — `201 Created`: + +```json +{ + "id": "6a91c37d76e8582777776f72", + "title": "Onboarding Week 1", + "modules": ["Git", "Java Basics"], + "milestones": ["Setup complete"] +} +``` + +#### List + +```http +GET /api/curriculum +``` + +Response — `200 OK`, empty array if none exist: + +```json +[ + { + "id": "6a91c37d76e8582777776f72", + "title": "Onboarding Week 1", + "modules": ["Git", "Java Basics"], + "milestones": ["Setup complete"] + } +] +``` + +#### Read one + +```http +GET /api/curriculum/{id} +``` + +`200 OK` with the object, or `404 Not Found`: + +```json +{ "message": "Training plan not found: " } +``` + +#### Update + +```http +PUT /api/curriculum/{id} +Content-Type: application/json + +{ + "title": "Onboarding Week 1 (updated)", + "modules": ["Git", "Java Basics", "Spring Boot"], + "milestones": ["Setup complete"] +} +``` + +`200 OK` with the updated object (same `id`), or `404 Not Found` in the same shape as above. + +#### Delete + +```http +DELETE /api/curriculum/{id} +``` + +`204 No Content` on success, or `404 Not Found` in the same shape as above. + +### Other resources (not yet implemented) + +`AuthController` (`/api/auth`), `CohortController` (`/api/cohorts`), `ProgressController` (`/api/progress`), and `DashboardController` (`/api/dashboard`) exist as empty `@RestController` classes with the base `@RequestMapping` path set, but no methods yet. Calling any of them currently returns `404` (no matching route), not a real response. + +--- + +## 5. Validation & Error Format + +All error responses (validation failures, not-found, bad input) come back as: + +```json +{ "message": "" } +``` + +- `400 Bad Request` — bean validation failure, e.g. `{ "message": "title must not be blank" }` (verified live) +- `400 Bad Request` — malformed input MongoDB can't parse (e.g. a non-ObjectId `{id}`), e.g. `{ "message": "Invalid request: ..." }` (defensive — added but not yet verified against a live Atlas call in this session; Aaron should confirm with a real `GET /api/curriculum/not-a-real-id` against a running instance) +- `404 Not Found` — missing resource, e.g. `{ "message": "Training plan not found: " }` (verified live) + +This is enforced globally by a `@RestControllerAdvice` (`GlobalExceptionHandler`), so any new controller gets the same error shape for free — no need to hand-roll error handling per controller. + +--- + +## 6. Environment Variables + +### Backend + +```text +MONGODB_URI +``` + +Full Atlas connection string **including the database name in the path**, e.g.: + +```text +mongodb+srv://:@bankcluster.xxxxx.mongodb.net/noticeboard?retryWrites=true&w=majority +``` + +There is **no separate `MONGODB_DATABASE` variable** — the db name (`noticeboard`) lives inside `MONGODB_URI`. Don't introduce a second variable for it. + +Mail (for Aaron's scheduled notification jobs, optional — defaults exist for local dev): + +```text +MAIL_HOST +MAIL_PORT +MAIL_USERNAME +MAIL_PASSWORD +``` + +### Frontend + +```text +VITE_API_URL +``` + +Local value: + +```text +http://localhost:8080 +``` + +Curriculum calls should be built as `${VITE_API_URL}/api/curriculum`. + +--- + +## 7. CORS + +Configured in `CorsConfig` (`com.edtech.noticeboard.config`). Currently allows only: + +```text +http://localhost:5173 +``` + +for `GET`, `POST`, `PUT`, `DELETE`, `OPTIONS` on `/api/**`. **Mark must get the production frontend URL added here before deployment** — it is not automatic. + +--- + +## 8. Auth Status + +`SecurityConfig` currently allows every request unauthenticated (`permitAll()`), with CSRF disabled. This is intentional and temporary — it exists so controllers are testable before real auth is wired up. **Mark owns replacing this with real JWT/session auth and role-based access** (`TRAINEE`/`HR`/`MANAGER`, per the `User.role` field). No endpoint currently requires a token. + +--- + +## 9. Open Items + +- **`ProgressLog` field shape** — not finalized. Aaron and Danielle must agree on real fields before `ProgressController`/`ProgressService` are implemented. +- **Danielle's USP** — not yet selected. Once chosen, if it changes any resource's fields or adds endpoints, this file must be updated *before* implementation starts, and Aaron/Alena notified. +- **Auth/JWT** — not started (Mark). +- **Production CORS origin** — pending frontend deployment URL (Mark). +- **Deployment architecture** — the backend is Java/Spring Boot (a persistent server), which is a deliberate deviation from this workshop's default Python/FastAPI/Lambda stack. The assignment's Terraform/Lambda scripts assume a Python zip handler and won't apply to this backend as-is. Mark needs either a Lambda-compatible Java handler wrapper or a non-Lambda compute target (EC2/ECS/Elastic Beanstalk) — not yet decided. + +--- + +## 10. How to Test This + +Two layers of testing exist, covering different purposes: + +### Automated (JUnit) + +`backend/src/test/java/com/edtech/noticeboard/service/CurriculumServiceTest.java` — 5 Mockito-based unit tests covering `CurriculumService`: listing, 404-on-missing-id (read and delete), id-stripping on create, and field updates on PUT. No real MongoDB needed — the repository is mocked, so these run fast and don't touch Atlas. + +Run them: + +```bash +cd backend +mvn test +``` + +These are what a grader/CI checks for TDD coverage. New backend logic should get tests here, not just manual Postman checks. + +### Manual (Postman) + +`backend/postman/NoticeBoardTracker-Curriculum.postman_collection.json` — import this into Postman. Two folders: + +- **Happy Path** — Create → List → Get By Id → Update → Delete, run top-to-bottom (or via Collection Runner). Each request has `pm.test()` assertions on status code and response shape, and the Create request auto-captures the generated `id` into a collection variable so the rest of the chain uses it automatically. The Delete step cleans up after itself, so running this doesn't leave junk data in the shared Atlas cluster. +- **Error Cases** — blank-title create (400), get/delete on a non-existent id (404). Demonstrates the `{"message": "..."}` error format from section 5. + +Collection variable `baseUrl` defaults to `http://localhost:8080` — change it if testing against a deployed instance instead of local. + +Before running either folder, start the backend locally with a real `MONGODB_URI` set (see section 6), or point `baseUrl` at wherever it's already running. + +--- + +## 11. Team Responsibilities + +### Aaron — Backend + MongoDB CRUD + +Owns `CurriculumController`/`CurriculumService` (done), `ProgressController`/`ProgressService` (pending field sync with Danielle), and `NotificationService` (scheduled email jobs, not started). + +### Alena — React CRUD Frontend + +Consumes `/api/curriculum` per section 4. Needs: React/Vite setup details, Node version, package manager, frontend build command, build output directory — not yet documented here, Alena to fill in. + +### Danielle — USP + UX + +Owns `DashboardController`/`DashboardService`, and the USP feature. Must notify Aaron/Alena before changing any resource's fields, per section 9. + +### Mark — Deployment + CI/CD + Integration + +Owns `AuthController`/`AuthService`, `CohortController`/`CohortService`, `SecurityConfig`, `CorsConfig`, and all deployment/CI-CD. See sections 7, 8, and 9 for what's blocking him. + +--- + +## 12. Change Rules + +When a contract change is required: + +1. Agree on the change with affected teammates. +2. Update this file. +3. Notify all affected team members. +4. Update backend code. +5. Update frontend code. +6. Update tests/Postman. +7. Update deployment configuration if necessary. + +### Change Log + +```text +2026-08-28 — Replaced single-Notice draft with the actual multi-resource + (User/Cohort/TrainingPlan/ProgressLog) contract matching + what's implemented on feature/backend, after the team + decided to keep the existing Java/Spring Boot backend + rather than rewrite it to match the original draft. + +2026-08-28 — Added section 10 (How to Test This): CurriculumServiceTest + (5 JUnit/Mockito unit tests) and a Postman collection at + backend/postman/NoticeBoardTracker-Curriculum.postman_collection.json. +``` diff --git a/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/README.md b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/README.md new file mode 100644 index 00000000..115dd7c0 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/README.md @@ -0,0 +1,53 @@ +Aaron (fraze-dev) — Backend + MongoDB CRUD: Build the Notice API, service logic, MongoDB Atlas integration, and Create/Read/Update/Delete operations. + +Alena (futurecoder123456) — React CRUD Frontend: Build the notice list, create form, edit form, delete controls, and connect them to the agreed API. + +Danielle (danielle-jack) — Group USP + UX: Build the team's unique feature such as urgent notices, threads, dark mode, or another chosen USP, plus the related frontend/backend changes. + +Mark (MarkPaulRosenthal) — Deployment + CI/CD + Integration: Package/deploy Lambda, API Gateway, S3 frontend, environment variables, and GitHub Actions, while also handling integration fixes needed for deployment. + + +```text +Group1_MAAD_Mark_Alena_Aaron_Danielle/ +├── backend/ +│ ├── src/ +│ │ ├── main/ +│ │ │ ├── java/com/edtech/noticeboard/ +│ │ │ │ ├── config/ # Security, MongoConfig & CORS +│ │ │ │ ├── controller/ # REST Endpoints +│ │ │ │ │ ├── AuthController.java # Mark +│ │ │ │ │ ├── CohortController.java # Mark +│ │ │ │ │ ├── CurriculumController.java # Aaron +│ │ │ │ │ ├── ProgressController.java # Aaron/Danielle +│ │ │ │ │ └── DashboardController.java # Danielle +│ │ │ │ ├── model/ # MongoDB Documents (@Document) +│ │ │ │ │ ├── User.java # Trainees, HR, Managers +│ │ │ │ │ ├── Cohort.java # Group metadata & student IDs +│ │ │ │ │ ├── TrainingPlan.java # Modules & Milestones +│ │ │ │ │ └── ProgressLog.java # Trainee submissions & status +│ │ │ │ ├── repository/ # MongoRepository interfaces +│ │ │ │ │ ├── UserRepository.java +│ │ │ │ │ ├── CohortRepository.java +│ │ │ │ │ ├── TrainingPlanRepository.java +│ │ │ │ │ └── ProgressLogRepository.java +│ │ │ │ └── service/ # Business Logic & Scheduled Email Jobs (Aaron) +│ │ │ └── resources/ +│ │ │ └── application.properties # MongoDB URI & App Settings +│ │ └── test/ +│ └── pom.xml (or build.gradle) +│ +├── frontend/ +│ ├── src/ +│ │ ├── components/ # Reusable UI components +│ │ ├── pages/ +│ │ │ ├── Onboarding.jsx # HR user & cohort onboarding UI (Alena) +│ │ │ ├── TraineeView.jsx # Student progress & blocker UI (Alena) +│ │ │ ├── PlanBuilder.jsx # Curriculum management UI (Aaron) +│ │ │ └── ManagerDash.jsx # Manager holistic view & charts (Danielle) +│ │ └── services/ # Axios / Fetch API integrations +│ └── index.html +│ +├── .gitignore +├── README.md +└── docker-compose.yml +``` diff --git a/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/pom.xml b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/pom.xml new file mode 100644 index 00000000..66552100 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/pom.xml @@ -0,0 +1,81 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.4 + + + + com.edtech + noticeboard-backend + 0.0.1-SNAPSHOT + noticeboard-backend + Notice Board backend API (Spring Boot + MongoDB) + + + 17 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-mongodb + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-mail + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/postman/NoticeBoardTracker-Curriculum.postman_collection.json b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/postman/NoticeBoardTracker-Curriculum.postman_collection.json new file mode 100644 index 00000000..2afa444c --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/postman/NoticeBoardTracker-Curriculum.postman_collection.json @@ -0,0 +1,268 @@ +{ + "info": { + "_postman_id": "b3f1a2e0-4c1a-4e8a-9d3a-1f2b3c4d5e6f", + "name": "NoticeBoardTracker - Curriculum API", + "description": "Manual/regression collection for the Curriculum API (TrainingPlan CRUD). See API_CONTRACT.md for the full contract. Run the 'Happy Path' folder top-to-bottom (or use Collection Runner) - it creates a TrainingPlan, exercises it, then deletes it, so it doesn't leave data behind in the shared Atlas cluster.", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "variable": [ + { "key": "baseUrl", "value": "http://localhost:8080", "type": "string" }, + { "key": "trainingPlanId", "value": "", "type": "string" } + ], + "item": [ + { + "name": "Happy Path", + "item": [ + { + "name": "Create Training Plan", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\"Status code is 201\", function () {", + " pm.response.to.have.status(201);", + "});", + "pm.test(\"Response has a generated id\", function () {", + " var json = pm.response.json();", + " pm.expect(json.id).to.exist;", + " pm.collectionVariables.set(\"trainingPlanId\", json.id);", + "});", + "pm.test(\"Title matches request\", function () {", + " pm.expect(pm.response.json().title).to.eql(\"Onboarding Week 1\");", + "});" + ] + } + } + ], + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json" }], + "body": { + "mode": "raw", + "raw": "{\n \"title\": \"Onboarding Week 1\",\n \"modules\": [\"Git\", \"Java Basics\"],\n \"milestones\": [\"Setup complete\"]\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/curriculum", + "host": ["{{baseUrl}}"], + "path": ["api", "curriculum"] + } + }, + "response": [] + }, + { + "name": "List Training Plans", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "pm.test(\"Response is an array\", function () {", + " pm.expect(pm.response.json()).to.be.an(\"array\");", + "});" + ] + } + } + ], + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/api/curriculum", + "host": ["{{baseUrl}}"], + "path": ["api", "curriculum"] + } + }, + "response": [] + }, + { + "name": "Get Training Plan By Id", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "pm.test(\"Returned id matches requested id\", function () {", + " pm.expect(pm.response.json().id).to.eql(pm.collectionVariables.get(\"trainingPlanId\"));", + "});" + ] + } + } + ], + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/api/curriculum/{{trainingPlanId}}", + "host": ["{{baseUrl}}"], + "path": ["api", "curriculum", "{{trainingPlanId}}"] + } + }, + "response": [] + }, + { + "name": "Update Training Plan", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "pm.test(\"Title was updated\", function () {", + " pm.expect(pm.response.json().title).to.eql(\"Onboarding Week 1 (Updated)\");", + "});", + "pm.test(\"Id is unchanged\", function () {", + " pm.expect(pm.response.json().id).to.eql(pm.collectionVariables.get(\"trainingPlanId\"));", + "});" + ] + } + } + ], + "request": { + "method": "PUT", + "header": [{ "key": "Content-Type", "value": "application/json" }], + "body": { + "mode": "raw", + "raw": "{\n \"title\": \"Onboarding Week 1 (Updated)\",\n \"modules\": [\"Git\", \"Java Basics\", \"Spring Boot\"],\n \"milestones\": [\"Setup complete\"]\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/curriculum/{{trainingPlanId}}", + "host": ["{{baseUrl}}"], + "path": ["api", "curriculum", "{{trainingPlanId}}"] + } + }, + "response": [] + }, + { + "name": "Delete Training Plan", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\"Status code is 204\", function () {", + " pm.response.to.have.status(204);", + "});" + ] + } + } + ], + "request": { + "method": "DELETE", + "url": { + "raw": "{{baseUrl}}/api/curriculum/{{trainingPlanId}}", + "host": ["{{baseUrl}}"], + "path": ["api", "curriculum", "{{trainingPlanId}}"] + } + }, + "response": [] + } + ] + }, + { + "name": "Error Cases", + "item": [ + { + "name": "Create Training Plan - Blank Title (400)", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\"Status code is 400\", function () {", + " pm.response.to.have.status(400);", + "});", + "pm.test(\"Error body has a message field\", function () {", + " pm.expect(pm.response.json().message).to.exist;", + "});" + ] + } + } + ], + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json" }], + "body": { + "mode": "raw", + "raw": "{\n \"title\": \"\",\n \"modules\": [],\n \"milestones\": []\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/curriculum", + "host": ["{{baseUrl}}"], + "path": ["api", "curriculum"] + } + }, + "response": [] + }, + { + "name": "Get Training Plan - Not Found (404)", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\"Status code is 404\", function () {", + " pm.response.to.have.status(404);", + "});", + "pm.test(\"Error body has a message field\", function () {", + " pm.expect(pm.response.json().message).to.exist;", + "});" + ] + } + } + ], + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/api/curriculum/000000000000000000000000", + "host": ["{{baseUrl}}"], + "path": ["api", "curriculum", "000000000000000000000000"] + } + }, + "response": [] + }, + { + "name": "Delete Training Plan - Not Found (404)", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test(\"Status code is 404\", function () {", + " pm.response.to.have.status(404);", + "});", + "pm.test(\"Error body has a message field\", function () {", + " pm.expect(pm.response.json().message).to.exist;", + "});" + ] + } + } + ], + "request": { + "method": "DELETE", + "url": { + "raw": "{{baseUrl}}/api/curriculum/000000000000000000000000", + "host": ["{{baseUrl}}"], + "path": ["api", "curriculum", "000000000000000000000000"] + } + }, + "response": [] + } + ] + } + ] +} diff --git a/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/NoticeboardApplication.java b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/NoticeboardApplication.java new file mode 100644 index 00000000..603dc278 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/NoticeboardApplication.java @@ -0,0 +1,14 @@ +package com.edtech.noticeboard; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; + +@SpringBootApplication +@EnableScheduling +public class NoticeboardApplication { + + public static void main(String[] args) { + SpringApplication.run(NoticeboardApplication.class, args); + } +} diff --git a/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/config/CorsConfig.java b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/config/CorsConfig.java new file mode 100644 index 00000000..119fac4c --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/config/CorsConfig.java @@ -0,0 +1,18 @@ +package com.edtech.noticeboard.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class CorsConfig implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry) { + // TODO(Mark): restrict allowedOrigins to the deployed frontend URL before shipping + registry.addMapping("/api/**") + .allowedOrigins("http://localhost:5173") + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS"); + } +} diff --git a/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/config/GlobalExceptionHandler.java b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/config/GlobalExceptionHandler.java new file mode 100644 index 00000000..55320ee5 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/config/GlobalExceptionHandler.java @@ -0,0 +1,34 @@ +package com.edtech.noticeboard.config; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.server.ResponseStatusException; + +import java.util.Map; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(ResponseStatusException.class) + public ResponseEntity> handleResponseStatus(ResponseStatusException ex) { + return ResponseEntity.status(ex.getStatusCode()) + .body(Map.of("message", ex.getReason() != null ? ex.getReason() : ex.getStatusCode().toString())); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidation(MethodArgumentNotValidException ex) { + String message = ex.getBindingResult().getFieldErrors().stream() + .findFirst() + .map(error -> error.getField() + " " + error.getDefaultMessage()) + .orElse("Validation failed"); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Map.of("message", message)); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity> handleBadArgument(IllegalArgumentException ex) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Map.of("message", "Invalid request: " + ex.getMessage())); + } +} diff --git a/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/config/MongoConfig.java b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/config/MongoConfig.java new file mode 100644 index 00000000..9023e75c --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/config/MongoConfig.java @@ -0,0 +1,10 @@ +package com.edtech.noticeboard.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; + +@Configuration +@EnableMongoRepositories(basePackages = "com.edtech.noticeboard.repository") +public class MongoConfig { + // Connection details come from spring.data.mongodb.uri in application.properties +} diff --git a/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/config/SecurityConfig.java b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/config/SecurityConfig.java new file mode 100644 index 00000000..0fd4bea0 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/config/SecurityConfig.java @@ -0,0 +1,20 @@ +package com.edtech.noticeboard.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // TODO(Mark): replace with real JWT/session auth + role-based access. + // Left open for now so the other controllers are testable before login exists. + http + .csrf(csrf -> csrf.disable()) + .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()); + return http.build(); + } +} diff --git a/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/controller/AuthController.java b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/controller/AuthController.java new file mode 100644 index 00000000..6d662a8f --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/controller/AuthController.java @@ -0,0 +1,10 @@ +package com.edtech.noticeboard.controller; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/auth") +public class AuthController { + // TODO(Mark): login, registration, session/JWT endpoints +} diff --git a/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/controller/CohortController.java b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/controller/CohortController.java new file mode 100644 index 00000000..4d761671 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/controller/CohortController.java @@ -0,0 +1,10 @@ +package com.edtech.noticeboard.controller; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/cohorts") +public class CohortController { + // TODO(Mark): cohort CRUD endpoints +} diff --git a/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/controller/CurriculumController.java b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/controller/CurriculumController.java new file mode 100644 index 00000000..d69e883c --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/controller/CurriculumController.java @@ -0,0 +1,55 @@ +package com.edtech.noticeboard.controller; + +import com.edtech.noticeboard.model.TrainingPlan; +import com.edtech.noticeboard.service.CurriculumService; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/api/curriculum") +public class CurriculumController { + + private final CurriculumService curriculumService; + + public CurriculumController(CurriculumService curriculumService) { + this.curriculumService = curriculumService; + } + + @GetMapping + public List getAll() { + return curriculumService.getAllTrainingPlans(); + } + + @GetMapping("/{id}") + public TrainingPlan getById(@PathVariable String id) { + return curriculumService.getTrainingPlanById(id); + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public TrainingPlan create(@Valid @RequestBody TrainingPlan trainingPlan) { + return curriculumService.createTrainingPlan(trainingPlan); + } + + @PutMapping("/{id}") + public TrainingPlan update(@PathVariable String id, @Valid @RequestBody TrainingPlan trainingPlan) { + return curriculumService.updateTrainingPlan(id, trainingPlan); + } + + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete(@PathVariable String id) { + curriculumService.deleteTrainingPlan(id); + } +} diff --git a/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/controller/DashboardController.java b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/controller/DashboardController.java new file mode 100644 index 00000000..b05abc09 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/controller/DashboardController.java @@ -0,0 +1,10 @@ +package com.edtech.noticeboard.controller; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/dashboard") +public class DashboardController { + // TODO(Danielle): manager dashboard aggregation endpoints +} diff --git a/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/controller/ProgressController.java b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/controller/ProgressController.java new file mode 100644 index 00000000..b09b2c76 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/controller/ProgressController.java @@ -0,0 +1,10 @@ +package com.edtech.noticeboard.controller; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/progress") +public class ProgressController { + // TODO(Aaron/Danielle): progress log endpoints +} diff --git a/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/model/Cohort.java b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/model/Cohort.java new file mode 100644 index 00000000..1b3caff9 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/model/Cohort.java @@ -0,0 +1,18 @@ +package com.edtech.noticeboard.model; + +import lombok.Data; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.util.List; + +@Data +@Document(collection = "cohorts") +public class Cohort { + + @Id + private String id; + + private String name; + private List studentIds; +} diff --git a/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/model/ProgressLog.java b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/model/ProgressLog.java new file mode 100644 index 00000000..529defe0 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/model/ProgressLog.java @@ -0,0 +1,21 @@ +package com.edtech.noticeboard.model; + +import lombok.Data; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.time.Instant; + +@Data +@Document(collection = "progress_logs") +public class ProgressLog { + + @Id + private String id; + + private String traineeId; + private String moduleId; + private String status; // NOT_STARTED, IN_PROGRESS, BLOCKED, COMPLETED + private String submissionNote; + private Instant updatedAt; +} diff --git a/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/model/TrainingPlan.java b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/model/TrainingPlan.java new file mode 100644 index 00000000..4bf8a188 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/model/TrainingPlan.java @@ -0,0 +1,22 @@ +package com.edtech.noticeboard.model; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.util.List; + +@Data +@Document(collection = "training_plans") +public class TrainingPlan { + + @Id + private String id; + + @NotBlank + private String title; + + private List modules; + private List milestones; +} diff --git a/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/model/User.java b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/model/User.java new file mode 100644 index 00000000..c5ce64c7 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/model/User.java @@ -0,0 +1,18 @@ +package com.edtech.noticeboard.model; + +import lombok.Data; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +@Data +@Document(collection = "users") +public class User { + + @Id + private String id; + + private String name; + private String email; + private String passwordHash; + private String role; // TRAINEE, HR, MANAGER +} diff --git a/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/repository/CohortRepository.java b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/repository/CohortRepository.java new file mode 100644 index 00000000..73d9af57 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/repository/CohortRepository.java @@ -0,0 +1,7 @@ +package com.edtech.noticeboard.repository; + +import com.edtech.noticeboard.model.Cohort; +import org.springframework.data.mongodb.repository.MongoRepository; + +public interface CohortRepository extends MongoRepository { +} diff --git a/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/repository/ProgressLogRepository.java b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/repository/ProgressLogRepository.java new file mode 100644 index 00000000..23780b31 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/repository/ProgressLogRepository.java @@ -0,0 +1,10 @@ +package com.edtech.noticeboard.repository; + +import com.edtech.noticeboard.model.ProgressLog; +import org.springframework.data.mongodb.repository.MongoRepository; + +import java.util.List; + +public interface ProgressLogRepository extends MongoRepository { + List findByStatus(String status); +} diff --git a/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/repository/TrainingPlanRepository.java b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/repository/TrainingPlanRepository.java new file mode 100644 index 00000000..96ba1287 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/repository/TrainingPlanRepository.java @@ -0,0 +1,7 @@ +package com.edtech.noticeboard.repository; + +import com.edtech.noticeboard.model.TrainingPlan; +import org.springframework.data.mongodb.repository.MongoRepository; + +public interface TrainingPlanRepository extends MongoRepository { +} diff --git a/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/repository/UserRepository.java b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/repository/UserRepository.java new file mode 100644 index 00000000..2e4fe5b5 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/repository/UserRepository.java @@ -0,0 +1,7 @@ +package com.edtech.noticeboard.repository; + +import com.edtech.noticeboard.model.User; +import org.springframework.data.mongodb.repository.MongoRepository; + +public interface UserRepository extends MongoRepository { +} diff --git a/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/service/AuthService.java b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/service/AuthService.java new file mode 100644 index 00000000..57c644c3 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/service/AuthService.java @@ -0,0 +1,8 @@ +package com.edtech.noticeboard.service; + +import org.springframework.stereotype.Service; + +@Service +public class AuthService { + // TODO(Mark): authentication business logic +} diff --git a/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/service/CohortService.java b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/service/CohortService.java new file mode 100644 index 00000000..be1c529c --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/service/CohortService.java @@ -0,0 +1,8 @@ +package com.edtech.noticeboard.service; + +import org.springframework.stereotype.Service; + +@Service +public class CohortService { + // TODO(Mark): cohort business logic +} diff --git a/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/service/CurriculumService.java b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/service/CurriculumService.java new file mode 100644 index 00000000..6f23f204 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/service/CurriculumService.java @@ -0,0 +1,48 @@ +package com.edtech.noticeboard.service; + +import com.edtech.noticeboard.model.TrainingPlan; +import com.edtech.noticeboard.repository.TrainingPlanRepository; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +import java.util.List; + +@Service +public class CurriculumService { + + private final TrainingPlanRepository trainingPlanRepository; + + public CurriculumService(TrainingPlanRepository trainingPlanRepository) { + this.trainingPlanRepository = trainingPlanRepository; + } + + public List getAllTrainingPlans() { + return trainingPlanRepository.findAll(); + } + + public TrainingPlan getTrainingPlanById(String id) { + return trainingPlanRepository.findById(id) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Training plan not found: " + id)); + } + + public TrainingPlan createTrainingPlan(TrainingPlan trainingPlan) { + trainingPlan.setId(null); + return trainingPlanRepository.save(trainingPlan); + } + + public TrainingPlan updateTrainingPlan(String id, TrainingPlan updated) { + TrainingPlan existing = getTrainingPlanById(id); + existing.setTitle(updated.getTitle()); + existing.setModules(updated.getModules()); + existing.setMilestones(updated.getMilestones()); + return trainingPlanRepository.save(existing); + } + + public void deleteTrainingPlan(String id) { + if (!trainingPlanRepository.existsById(id)) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Training plan not found: " + id); + } + trainingPlanRepository.deleteById(id); + } +} diff --git a/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/service/DashboardService.java b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/service/DashboardService.java new file mode 100644 index 00000000..fe1bf8db --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/service/DashboardService.java @@ -0,0 +1,8 @@ +package com.edtech.noticeboard.service; + +import org.springframework.stereotype.Service; + +@Service +public class DashboardService { + // TODO(Danielle): dashboard aggregation logic +} diff --git a/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/service/NotificationService.java b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/service/NotificationService.java new file mode 100644 index 00000000..1c82491e --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/service/NotificationService.java @@ -0,0 +1,54 @@ +package com.edtech.noticeboard.service; + +import com.edtech.noticeboard.model.ProgressLog; +import com.edtech.noticeboard.repository.ProgressLogRepository; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class NotificationService { + + private final ProgressLogRepository progressLogRepository; + private final JavaMailSender mailSender; + + @Value("${spring.mail.username}") + private String fromAddress; + + @Value("${notification.recipient}") + private String recipientAddress; + + public NotificationService(ProgressLogRepository progressLogRepository, JavaMailSender mailSender) { + this.progressLogRepository = progressLogRepository; + this.mailSender = mailSender; + } + + @Scheduled(cron = "0 0 8 * * *") + public void sendBlockedTraineeDigest() { + List blocked = progressLogRepository.findByStatus("BLOCKED"); + if (blocked.isEmpty()) { + return; + } + + String body = blocked.stream() + .map(this::describe) + .collect(Collectors.joining("\n")); + + SimpleMailMessage message = new SimpleMailMessage(); + message.setFrom(fromAddress); + message.setTo(recipientAddress); + message.setSubject("Blocked trainees digest (" + blocked.size() + ")"); + message.setText(body); + mailSender.send(message); + } + + private String describe(ProgressLog log) { + String note = log.getSubmissionNote() != null ? ": " + log.getSubmissionNote() : ""; + return "Trainee " + log.getTraineeId() + " is blocked on module " + log.getModuleId() + note; + } +} diff --git a/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/service/ProgressService.java b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/service/ProgressService.java new file mode 100644 index 00000000..e5a98e1f --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/java/com/edtech/noticeboard/service/ProgressService.java @@ -0,0 +1,8 @@ +package com.edtech.noticeboard.service; + +import org.springframework.stereotype.Service; + +@Service +public class ProgressService { + // TODO(Aaron/Danielle): progress log business logic +} diff --git a/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/resources/application.properties b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/resources/application.properties new file mode 100644 index 00000000..d5285a57 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/main/resources/application.properties @@ -0,0 +1,13 @@ +spring.application.name=noticeboard-backend +server.port=${PORT:8080} + +spring.data.mongodb.uri=${MONGODB_URI:mongodb://localhost:27017/noticeboard} + +spring.mail.host=${MAIL_HOST:smtp.gmail.com} +spring.mail.port=${MAIL_PORT:587} +spring.mail.username=${MAIL_USERNAME:} +spring.mail.password=${MAIL_PASSWORD:} +spring.mail.properties.mail.smtp.auth=true +spring.mail.properties.mail.smtp.starttls.enable=true + +notification.recipient=${NOTIFICATION_RECIPIENT_EMAIL:hr@example.com} diff --git a/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/test/java/com/edtech/noticeboard/NoticeboardApplicationTests.java b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/test/java/com/edtech/noticeboard/NoticeboardApplicationTests.java new file mode 100644 index 00000000..936fa442 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/test/java/com/edtech/noticeboard/NoticeboardApplicationTests.java @@ -0,0 +1,12 @@ +package com.edtech.noticeboard; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class NoticeboardApplicationTests { + + @Test + void contextLoads() { + } +} diff --git a/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/test/java/com/edtech/noticeboard/service/CurriculumServiceTest.java b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/test/java/com/edtech/noticeboard/service/CurriculumServiceTest.java new file mode 100644 index 00000000..f51167e8 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/backend/src/test/java/com/edtech/noticeboard/service/CurriculumServiceTest.java @@ -0,0 +1,112 @@ +package com.edtech.noticeboard.service; + +import com.edtech.noticeboard.model.TrainingPlan; +import com.edtech.noticeboard.repository.TrainingPlanRepository; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CurriculumServiceTest { + + @Mock + private TrainingPlanRepository trainingPlanRepository; + + @InjectMocks + private CurriculumService curriculumService; + + @Test + void getAllTrainingPlans_returnsAllFromRepository() { + TrainingPlan plan1 = new TrainingPlan(); + plan1.setId("1"); + plan1.setTitle("Onboarding Week 1"); + + TrainingPlan plan2 = new TrainingPlan(); + plan2.setId("2"); + plan2.setTitle("Onboarding Week 2"); + + when(trainingPlanRepository.findAll()).thenReturn(List.of(plan1, plan2)); + + List result = curriculumService.getAllTrainingPlans(); + + assertThat(result).containsExactly(plan1, plan2); + } + + @Test + void getTrainingPlanById_notFound_throws404() { + when(trainingPlanRepository.findById("missing-id")).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> curriculumService.getTrainingPlanById("missing-id")) + .isInstanceOf(ResponseStatusException.class) + .satisfies(ex -> assertThat(((ResponseStatusException) ex).getStatusCode()) + .isEqualTo(HttpStatus.NOT_FOUND)); + } + + @Test + void createTrainingPlan_ignoresClientSuppliedId() { + TrainingPlan input = new TrainingPlan(); + input.setId("client-supplied-id"); + input.setTitle("Onboarding Week 1"); + + when(trainingPlanRepository.save(any(TrainingPlan.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + curriculumService.createTrainingPlan(input); + + ArgumentCaptor captor = ArgumentCaptor.forClass(TrainingPlan.class); + verify(trainingPlanRepository).save(captor.capture()); + assertThat(captor.getValue().getId()).isNull(); + } + + @Test + void updateTrainingPlan_found_updatesFieldsAndSaves() { + TrainingPlan existing = new TrainingPlan(); + existing.setId("123"); + existing.setTitle("Old Title"); + existing.setModules(List.of("Old Module")); + existing.setMilestones(List.of("Old Milestone")); + + TrainingPlan updates = new TrainingPlan(); + updates.setTitle("New Title"); + updates.setModules(List.of("New Module")); + updates.setMilestones(List.of("New Milestone")); + + when(trainingPlanRepository.findById("123")).thenReturn(Optional.of(existing)); + when(trainingPlanRepository.save(any(TrainingPlan.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + TrainingPlan result = curriculumService.updateTrainingPlan("123", updates); + + assertThat(result.getId()).isEqualTo("123"); + assertThat(result.getTitle()).isEqualTo("New Title"); + assertThat(result.getModules()).containsExactly("New Module"); + assertThat(result.getMilestones()).containsExactly("New Milestone"); + } + + @Test + void deleteTrainingPlan_notFound_throws404() { + when(trainingPlanRepository.existsById("missing-id")).thenReturn(false); + + assertThatThrownBy(() -> curriculumService.deleteTrainingPlan("missing-id")) + .isInstanceOf(ResponseStatusException.class) + .satisfies(ex -> assertThat(((ResponseStatusException) ex).getStatusCode()) + .isEqualTo(HttpStatus.NOT_FOUND)); + + verify(trainingPlanRepository, never()).deleteById(any()); + } +} diff --git a/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/frontend/index.html b/workshops/fullstack-aws/projects/submission/Group1_MAAD_Mark_Alena_Aaron_Danielle/frontend/index.html new file mode 100644 index 00000000..e69de29b