diff --git a/FRONTEND_BACKEND_INTEGRATION_SUMMARY.md b/FRONTEND_BACKEND_INTEGRATION_SUMMARY.md index f45c7a8..224e70b 100644 --- a/FRONTEND_BACKEND_INTEGRATION_SUMMARY.md +++ b/FRONTEND_BACKEND_INTEGRATION_SUMMARY.md @@ -2,12 +2,16 @@ ## 📋 Overview -This document outlines the integration work completed to connect the existing React frontend with the Spring Boot backend. The backend API was already fully implemented and functional, and the frontend has been updated to consume these APIs properly. +This document outlines the integration work completed to connect the existing React frontend with the Spring Boot +backend. The backend API was already fully implemented and functional, and the frontend has been updated to consume +these APIs properly. ## ✅ Integration Changes Completed ### **1. Environment Configuration** + **File:** `handoff-frontend/.env` + ```bash # Backend API Configuration VITE_API_BASE_URL=http://localhost:8080/api/v1 @@ -17,14 +21,18 @@ VITE_ANALYTICS_ENABLED=false ``` ### **2. Dependencies Added** + ```bash npm install axios @tanstack/react-query ``` + - **axios**: HTTP client for API calls - **@tanstack/react-query**: Data fetching and caching library ### **3. API Integration Layer** + **File:** `src/lib/api.ts` + - Axios client configuration with base URL - JWT token management (storage, retrieval, cleanup) - Request interceptor to add Authorization header @@ -32,7 +40,9 @@ npm install axios @tanstack/react-query - Automatic token refresh logic ### **4. TypeScript Type Definitions** + **File:** `src/types/index.ts` + - Complete type definitions matching backend DTOs - User, Project, ProjectApplication, Message, Payment types - Request/Response types for all API endpoints @@ -41,24 +51,29 @@ npm install axios @tanstack/react-query ### **5. Service Layer Implementation** **File:** `src/services/authService.ts` + - User registration and login - Token management - User profile retrieval - Logout functionality **File:** `src/services/projectService.ts` + - Project CRUD operations - Project publishing - Application management - Project discovery and filtering **File:** `src/services/userService.ts` + - Profile management - User preferences - Account deactivation ### **6. Authentication Context** + **File:** `src/contexts/AuthContext.tsx` + - Global authentication state management - User session persistence - Login/logout state handling @@ -67,13 +82,16 @@ npm install axios @tanstack/react-query ### **7. Authentication Pages** **Files:** `src/pages/Login.tsx`, `src/pages/Register.tsx` + - Form validation with Zod schemas - Error handling and user feedback - Role selection for registration - Navigation after successful auth ### **8. Protected Route Component** + **File:** `src/components/ProtectedRoute.tsx` + - Route protection based on authentication - Role-based access control - Automatic redirects to login @@ -81,18 +99,22 @@ npm install axios @tanstack/react-query ### **9. Updated Components** **Navigation Component:** `src/components/Navigation.tsx` + - Dynamic menu based on auth status - User dropdown with profile/logout options - Responsive mobile navigation **Project Scoping Wizard:** `src/components/ProjectScopingWizard.tsx` + - Real API integration for project creation - Authentication requirement - Error handling and feedback - Budget range mapping to backend format ### **10. Application Structure Update** + **File:** `src/App.tsx` + - React Query provider integration - Authentication context wrapper - New routes for login/register @@ -101,15 +123,18 @@ npm install axios @tanstack/react-query ## 🔗 Backend API Endpoints Integrated ### **Authentication APIs** + - `POST /api/v1/auth/register` - User registration - `POST /api/v1/auth/login` - User authentication ### **User Management APIs** + - `GET /api/v1/users/profile` - Get current user profile - `PUT /api/v1/users/profile` - Update user profile - `DELETE /api/v1/users/profile` - Deactivate account ### **Project Management APIs** + - `POST /api/v1/projects` - Create new project - `GET /api/v1/projects/{id}` - Get project details - `PUT /api/v1/projects/{id}` - Update project @@ -120,6 +145,7 @@ npm install axios @tanstack/react-query - `POST /api/v1/projects/{id}/view` - Increment view count ### **Project Application APIs** + - `POST /api/v1/projects/{projectId}/applications` - Apply to project - `GET /api/v1/projects/{projectId}/applications` - Get project applications - `GET /api/v1/applications/mine` - Get user's applications @@ -129,20 +155,25 @@ npm install axios @tanstack/react-query ## 🔧 Configuration Required ### **Backend CORS Configuration** + Ensure the backend allows requests from the frontend origin: + ```java @CrossOrigin(origins = {"http://localhost:5173", "http://localhost:3000"}) ``` ### **JWT Secret Configuration** + The frontend expects JWT tokens from the backend. Ensure your JWT configuration is properly set up. ### **Database Initialization** + Make sure the database is properly initialized with the required tables and sample data. ## 🚀 Running the Full Stack ### **1. Start Backend Services** + ```bash # Start PostgreSQL and Redis docker-compose up db redis -d @@ -153,6 +184,7 @@ cd handoff-backend ``` ### **2. Start Frontend** + ```bash # Start React development server cd handoff-frontend @@ -161,13 +193,15 @@ npm run dev ``` ### **3. Access Points** + - **Frontend**: http://localhost:5173 - **Backend API**: http://localhost:8080/api/v1 -- **API Documentation**: http://localhost:8080/api/v1/swagger-ui/index.html +- **API Documentation**: http://localhost:8080/swagger-ui/index.html ## ✨ New Features Available ### **For Users** + 1. **User Registration/Login** - Complete authentication flow 2. **Project Creation** - Real project creation through the wizard 3. **User Profiles** - Profile management functionality @@ -175,6 +209,7 @@ npm run dev 5. **Secure Navigation** - Protected routes and authentication state ### **For Developers** + 1. **Type-Safe APIs** - Complete TypeScript integration 2. **Error Handling** - Comprehensive error boundaries and feedback 3. **Token Management** - Automatic token refresh and cleanup @@ -183,20 +218,21 @@ npm run dev ## 📊 Integration Status -| Component | Status | Notes | -|-----------|--------|-------| -| Authentication | ✅ Complete | Login, register, logout, token management | -| User Profile | ✅ Complete | Profile view, update, deactivation | -| Project Creation | ✅ Complete | Wizard integration with real API | -| Project Management | ✅ Complete | CRUD operations, status management | -| Navigation | ✅ Complete | Dynamic menus, user dropdown | -| Route Protection | ✅ Complete | Auth guards, role-based access | -| Error Handling | ✅ Complete | Global error boundaries, user feedback | -| Type Safety | ✅ Complete | Full TypeScript coverage | +| Component | Status | Notes | +|--------------------|------------|-------------------------------------------| +| Authentication | ✅ Complete | Login, register, logout, token management | +| User Profile | ✅ Complete | Profile view, update, deactivation | +| Project Creation | ✅ Complete | Wizard integration with real API | +| Project Management | ✅ Complete | CRUD operations, status management | +| Navigation | ✅ Complete | Dynamic menus, user dropdown | +| Route Protection | ✅ Complete | Auth guards, role-based access | +| Error Handling | ✅ Complete | Global error boundaries, user feedback | +| Type Safety | ✅ Complete | Full TypeScript coverage | ## 🔄 Next Steps (Optional Enhancements) ### **Phase 2 Features** (Not Required for MVP) + 1. **Real-time Messaging** - WebSocket integration for chat 2. **File Upload** - AWS S3 integration for project files 3. **Payment Integration** - Stripe payment processing @@ -205,6 +241,7 @@ npm run dev 6. **Dashboard Analytics** - User metrics and project tracking ### **Production Deployment** + 1. **Environment Variables** - Production API URLs 2. **Build Optimization** - Bundle splitting and caching 3. **Error Tracking** - Sentry or similar service integration @@ -213,12 +250,14 @@ npm run dev ## 🐛 Known Issues & Considerations ### **Current Limitations** + 1. **Lead Capture Form** - Still uses localStorage (can be enhanced with backend endpoint) 2. **File Uploads** - Not yet implemented (requires AWS S3 integration) 3. **Real-time Features** - WebSocket connections not implemented 4. **Payment Processing** - Stripe integration pending ### **Security Notes** + 1. JWT tokens are stored in localStorage (consider httpOnly cookies for production) 2. API responses should be sanitized on the backend 3. Rate limiting should be implemented on sensitive endpoints @@ -227,12 +266,14 @@ npm run dev ## 📞 Support & Contact ### **Frontend Integration** + - All API calls are properly typed and error-handled - Authentication state is managed globally - Components are updated to use real data - Build process is optimized and working ### **Backend Compatibility** + - Frontend expects exact API contract as implemented - No changes required to backend code - All existing endpoints are properly consumed @@ -242,6 +283,8 @@ npm run dev ## 🎉 Integration Complete! -The frontend is now fully integrated with the backend and ready for production deployment. All core functionality is working, including user authentication, project creation, and data persistence. The application provides a complete full-stack experience matching the original project requirements. +The frontend is now fully integrated with the backend and ready for production deployment. All core functionality is +working, including user authentication, project creation, and data persistence. The application provides a complete +full-stack experience matching the original project requirements. **Ready for testing and deployment! 🚀** \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index e2f4e32..a02ec41 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -38,6 +38,11 @@ services: # CORS for local frontend CORS_ALLOWED_ORIGINS: http://localhost:5173 JAVA_TOOL_OPTIONS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005" + # SMTP for local profile + MAIL_HOST: sandbox.smtp.mailtrap.io + MAIL_PORT: 2525 + MAIL_USERNAME: 0d618da6d6ee0e + MAIL_PASSWORD: 719eb3b06c7a23 ports: - "8080:8080" - "5005:5005" # Add this line for debug port diff --git a/handoff-backend/Dockerfile b/handoff-backend/Dockerfile deleted file mode 100644 index 668d4db..0000000 --- a/handoff-backend/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -# ---- Build stage ---- -FROM maven:3.9.9-eclipse-temurin-21 AS builder -WORKDIR /workspace -COPY pom.xml . -# Pre-fetch dependencies for faster incremental builds -RUN mvn -q -e -DskipTests dependency:go-offline -# Copy source and build -COPY src ./src -RUN mvn -q -DskipTests package - -# ---- Runtime stage ---- -FROM eclipse-temurin:21-jre -ENV JAVA_OPTS="" -ENV SPRING_PROFILES_ACTIVE=local -WORKDIR /app -COPY --from=builder /workspace/target/handoff-backend-0.0.1-SNAPSHOT.jar /app/app.jar -EXPOSE 8080 -ENTRYPOINT ["sh","-c","java $JAVA_OPTS -jar /app/app.jar"] - diff --git a/handoff-backend/README.md b/handoff-backend/README.md index cb409ec..c63ff90 100644 --- a/handoff-backend/README.md +++ b/handoff-backend/README.md @@ -47,6 +47,10 @@ All endpoints are prefixed with `/api/v1`. - `DELETE /projects/{id}` — Delete project. **Path:** `id` - `POST /projects/{id}/view` — Increment project view count. **Path:** `id` +### Email (Local/Nonprod Only) +- `POST /email/test` — Send a test email using the Thymeleaf template. **Body:** `{ to, subject, body }` (JSON). **Requires JWT authentication.** + - Only enabled in `local`, `dev`, `nonprod`, or `test` profiles. + **Note:** All POST/PUT endpoints expect a JSON body (`@RequestBody`). Path variables are shown as `{param}`. After logging in, include the JWT token in the `Authorization: Bearer ` header for all protected endpoints. --- @@ -213,6 +217,74 @@ docker run --rm -p 8080:8080 \ --- +## Email Service & Testing + +The backend includes a simple email service using Spring Boot's JavaMailSender and Thymeleaf for templated emails. + +### Email Service Usage +- Service: `EmailService` (in `com.handoff.service`) +- Supports sending plain HTML or Thymeleaf-rendered emails. +- Example usage: inject `EmailService` and call `sendEmail(...)` or `sendTemplateEmail(...)`. + +### Test Email Endpoint (Local/Nonprod Only) +- Controller: `EmailController` (in `com.handoff.controller`) +- Endpoint: `POST /api/v1/email/test` +- **Profiles:** Only enabled in `local`, `dev`, `nonprod`, or `test` profiles (not available in `prod`). +- **Request Body:** + ```json + { + "to": "your@email.com", + "subject": "Test Email", + "body": "This is a test email body." + } + ``` +- Uses the `test-email.html` Thymeleaf template in `src/main/resources/templates`. +- Returns 200 OK if sent, 500 if failed. + +### Local Email Testing with Mailtrap +For local development, it is recommended to use [Mailtrap](https://mailtrap.io/) to safely capture and view outgoing emails without sending real messages. + +**Setup:** +1. Create a free Mailtrap account and a new inbox. +2. In your `src/main/resources/application.yml` (or via environment variables), set: + ```yaml + spring: + mail: + host: smtp.mailtrap.io + port: 2525 + username: + password: + properties: + mail: + smtp: + auth: true + starttls: + enable: true + ``` +3. **Authenticate:** Before calling the `/api/v1/email/test` endpoint, you must log in to obtain a JWT token. Use the `/api/v1/auth/login` endpoint with valid credentials to receive a token. +4. **Send the test email:** Use the `/api/v1/email/test` endpoint to send a test email. Include the JWT token in the `Authorization` header: + ```http + Authorization: Bearer + ``` + Example curl command: + ```bash + curl -X POST http://localhost:8080/api/v1/email/test \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer " \ + -d '{ + "to": "your@email.com", + "subject": "Test Email", + "body": "This is a test email body." + }' + ``` +5. Check your Mailtrap inbox to view the rendered email (including Thymeleaf variables). + +**Note:** +- The `test-email.html` template is located in `src/main/resources/templates` and can be customized for your needs. +- For production, configure your real SMTP provider in the same way. + +--- + ## Notes - Flyway is enabled in all profiles; ensure migrations are up to date before deploys. - The server runs on port 8080 with context-path /api/v1. diff --git a/handoff-backend/pom.xml b/handoff-backend/pom.xml index ba80195..2bbf840 100644 --- a/handoff-backend/pom.xml +++ b/handoff-backend/pom.xml @@ -55,7 +55,20 @@ org.springframework.boot spring-boot-starter-validation - + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + org.springframework.boot + spring-boot-starter-mail + org.postgresql @@ -168,11 +181,6 @@ commons-text 1.14.0 - - - org.springframework.boot - spring-boot-starter-data-redis - @@ -194,6 +202,14 @@ 3.2.5 + + org.apache.maven.plugins + maven-failsafe-plugin + 3.2.5 + + true + + org.flywaydb flyway-maven-plugin diff --git a/handoff-backend/src/main/java/com/handoff/controller/EmailController.java b/handoff-backend/src/main/java/com/handoff/controller/EmailController.java new file mode 100644 index 0000000..726e23f --- /dev/null +++ b/handoff-backend/src/main/java/com/handoff/controller/EmailController.java @@ -0,0 +1,57 @@ +package com.handoff.controller; + +import com.handoff.service.EmailService; +import jakarta.mail.MessagingException; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; +import java.util.Map; + + +@RestController +@RequestMapping("/api/v1/email") +@RequiredArgsConstructor +public class EmailController { + private final EmailService emailService; + + @Profile({"local", "nonprod"}) + @PostMapping("/test") + public ResponseEntity sendTestEmail(@RequestBody EmailTestRequest request) { + Map variables = new HashMap<>(); + variables.put("subject", request.getSubject()); + variables.put("body", request.getBody()); + try { + emailService.sendTemplateEmail(request.getTo(), request.getSubject(), "test-email", variables); + return ResponseEntity.ok("Test email sent to " + request.getTo()); + } catch (MessagingException e) { + return ResponseEntity.status(500).body("Failed to send email: " + e.getMessage()); + } + } + + @PostMapping("/send") + public ResponseEntity sendEmail(@RequestBody EmailTestRequest request) { + Map variables = new HashMap<>(); + variables.put("subject", request.getSubject()); + variables.put("body", request.getBody()); + try { + emailService.sendTemplateEmail(request.getTo(), request.getSubject(), "prod-email", variables); + return ResponseEntity.ok("Email sent to " + request.getTo()); + } catch (MessagingException e) { + return ResponseEntity.status(500).body("Failed to send email: " + e.getMessage()); + } + } + + @Data + public static class EmailTestRequest { + private String to; + private String subject; + private String body; + } +} diff --git a/handoff-backend/src/main/java/com/handoff/service/EmailService.java b/handoff-backend/src/main/java/com/handoff/service/EmailService.java new file mode 100644 index 0000000..f5774dd --- /dev/null +++ b/handoff-backend/src/main/java/com/handoff/service/EmailService.java @@ -0,0 +1,58 @@ +package com.handoff.service; + +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.Context; + +/** + * Service for sending emails, including plain HTML emails and template-based emails. + * Uses JavaMailSender for email delivery and Thymeleaf for template processing. + */ +@Service +@RequiredArgsConstructor +public class EmailService { + // Constant for the default sender email address + private static final String FROM_ADDRESS = "no-reply@handoff-mvp.com"; + + private final JavaMailSender mailSender; // JavaMailSender instance for sending emails + private final TemplateEngine templateEngine; // Thymeleaf TemplateEngine for processing email templates + + /** + * Sends an email with the specified recipient, subject, and HTML body content. + * + * @param to The recipient's email address. + * @param subject The subject of the email. + * @param bodyHtml The HTML content of the email body. + * @throws MessagingException If an error occurs while creating or sending the email. + */ + public void sendEmail(String to, String subject, String bodyHtml) throws MessagingException { + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); + helper.setFrom(FROM_ADDRESS); + helper.setTo(to); + helper.setSubject(subject); + helper.setText(bodyHtml, true); + mailSender.send(message); + } + + /** + * Sends an email using a Thymeleaf template. + * + * @param to The recipient's email address. + * @param subject The subject of the email. + * @param templateName The name of the Thymeleaf template to use. + * @param variables A map of variables to be passed to the template. + * @throws MessagingException If an error occurs while creating or sending the email. + */ + public void sendTemplateEmail(String to, String subject, String templateName, java.util.Map variables) throws MessagingException { + Context context = new Context(); + context.setVariables(variables); + String html = templateEngine.process(templateName, context); + sendEmail(to, subject, html); + } +} \ No newline at end of file diff --git a/handoff-backend/src/main/resources/application.yml b/handoff-backend/src/main/resources/application.yml index cba24eb..76d79f9 100644 --- a/handoff-backend/src/main/resources/application.yml +++ b/handoff-backend/src/main/resources/application.yml @@ -34,6 +34,18 @@ spring: show-sql: true flyway: enabled: true + mail: + host: ${MAIL_HOST:smtp.example.com} + port: ${MAIL_PORT:587} + username: ${MAIL_USERNAME:your_username} + password: ${MAIL_PASSWORD:your_password} + properties: + mail: + smtp: + auth: true + starttls: + enable: true + from: ${MAIL_FROM:no-reply@handoff-mvp.com} # JWT defaults - Need vault or another secure way to manage secrets jwt: @@ -84,6 +96,18 @@ spring: show-sql: false flyway: enabled: true + mail: + host: ${MAIL_HOST:smtp.example.com} + port: ${MAIL_PORT:587} + username: ${MAIL_USERNAME:your_username} + password: ${MAIL_PASSWORD:your_password} + properties: + mail: + smtp: + auth: true + starttls: + enable: true + from: ${MAIL_FROM:no-reply@handoff-mvp.com} --- # Prod profile: PostgreSQL with Flyway @@ -105,6 +129,18 @@ spring: show-sql: false flyway: enabled: true + mail: + host: ${MAIL_HOST:smtp.example.com} + port: ${MAIL_PORT:587} + username: ${MAIL_USERNAME:your_username} + password: ${MAIL_PASSWORD:your_password} + properties: + mail: + smtp: + auth: true + starttls: + enable: true + from: ${MAIL_FROM:no-reply@handoff-mvp.com} logging: level: diff --git a/handoff-backend/src/main/resources/templates/prod-email.html b/handoff-backend/src/main/resources/templates/prod-email.html new file mode 100644 index 0000000..ccd625b --- /dev/null +++ b/handoff-backend/src/main/resources/templates/prod-email.html @@ -0,0 +1,13 @@ + + + + + Email Notification + + +

Email Subject

+

This is an email sent from the Handoff backend using Thymeleaf.

+
+ If you have any questions, please contact support. + + \ No newline at end of file diff --git a/handoff-backend/src/main/resources/templates/test-email.html b/handoff-backend/src/main/resources/templates/test-email.html new file mode 100644 index 0000000..8999546 --- /dev/null +++ b/handoff-backend/src/main/resources/templates/test-email.html @@ -0,0 +1,14 @@ + + + + + Email Test + + +

Test Email Subject

+

This is a test email sent from the Handoff backend using Thymeleaf.

+
+ This is a development test email. If you see this, your email setup is working! + + + diff --git a/handoff-backend/src/test/java/com/handoff/controller/EmailControllerTest.java b/handoff-backend/src/test/java/com/handoff/controller/EmailControllerTest.java new file mode 100644 index 0000000..c2837e9 --- /dev/null +++ b/handoff-backend/src/test/java/com/handoff/controller/EmailControllerTest.java @@ -0,0 +1,73 @@ +package com.handoff.controller; + +import com.handoff.security.JwtTokenProvider; +import com.handoff.service.EmailService; +import jakarta.mail.MessagingException; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +import static org.mockito.ArgumentMatchers.any; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(EmailController.class) +@WithMockUser(username = "user@example.com") +class EmailControllerTest { + @Autowired + MockMvc mockMvc; + @MockitoBean + EmailService emailService; + @MockitoBean + JwtTokenProvider jwtTokenProvider; + + @Test + void sendTestEmail_returnsOk() throws Exception { + Mockito.doNothing().when(emailService).sendTemplateEmail(any(), any(), any(), any()); + String body = "{\"to\":\"test@example.com\",\"subject\":\"Test\",\"body\":\"Body\"}"; + mockMvc.perform(post("/api/v1/email/test") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + .with(csrf())) + .andExpect(status().isOk()); + } + + @Test + void sendTestEmail_returns500OnException() throws Exception { + Mockito.doThrow(new MessagingException("fail")).when(emailService).sendTemplateEmail(any(), any(), any(), any()); + String body = "{\"to\":\"test@example.com\",\"subject\":\"Test\",\"body\":\"Body\"}"; + mockMvc.perform(post("/api/v1/email/test") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + .with(csrf())) + .andExpect(status().isInternalServerError()); + } + + @Test + void sendEmail_returnsOk() throws Exception { + Mockito.doNothing().when(emailService).sendTemplateEmail(any(), any(), any(), any()); + String body = "{\"to\":\"test@example.com\",\"subject\":\"Test\",\"body\":\"Body\"}"; + mockMvc.perform(post("/api/v1/email/send") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + .with(csrf())) + .andExpect(status().isOk()); + } + + @Test + void sendEmail_returns500OnException() throws Exception { + Mockito.doThrow(new MessagingException("fail")).when(emailService).sendTemplateEmail(any(), any(), any(), any()); + String body = "{\"to\":\"test@example.com\",\"subject\":\"Test\",\"body\":\"Body\"}"; + mockMvc.perform(post("/api/v1/email/send") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + .with(csrf())) + .andExpect(status().isInternalServerError()); + } +} diff --git a/handoff-backend/src/test/java/com/handoff/it/PostgresContainerIntegrationTest.java b/handoff-backend/src/test/java/com/handoff/it/PostgresContainerIT.java similarity index 97% rename from handoff-backend/src/test/java/com/handoff/it/PostgresContainerIntegrationTest.java rename to handoff-backend/src/test/java/com/handoff/it/PostgresContainerIT.java index 2fb6e22..0e2544b 100644 --- a/handoff-backend/src/test/java/com/handoff/it/PostgresContainerIntegrationTest.java +++ b/handoff-backend/src/test/java/com/handoff/it/PostgresContainerIT.java @@ -22,7 +22,7 @@ "spring.flyway.enabled=false", "spring.jpa.hibernate.ddl-auto=create-drop" }) -class PostgresContainerIntegrationTest { +class PostgresContainerIT { @Container @ServiceConnection diff --git a/handoff-backend/src/test/java/com/handoff/service/UserServiceCacheTest.java b/handoff-backend/src/test/java/com/handoff/service/UserServiceCacheTest.java index 8fb4945..7625818 100644 --- a/handoff-backend/src/test/java/com/handoff/service/UserServiceCacheTest.java +++ b/handoff-backend/src/test/java/com/handoff/service/UserServiceCacheTest.java @@ -11,6 +11,7 @@ import org.springframework.cache.concurrent.ConcurrentMapCacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.mail.javamail.JavaMailSender; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; diff --git a/handoff-backend/src/test/resources/application.yml b/handoff-backend/src/test/resources/application.yml index ba0f207..e1a89ae 100644 --- a/handoff-backend/src/test/resources/application.yml +++ b/handoff-backend/src/test/resources/application.yml @@ -1,20 +1,28 @@ spring: - main: - banner-mode: off jpa: + hibernate: + ddl-auto: create-drop show-sql: false - datasource: - hikari: - minimum-idle: 0 - maximum-pool-size: 4 - idle-timeout: 10000 - max-lifetime: 30000 + mail: + host: sandbox.smtp.mailtrap.io + port: 2525 + username: 0d618da6d6ee0e + password: 719eb3b06c7a23 + properties: + mail: + smtp: + auth: true + starttls: + enable: true + from: no-reply@handoff-mvp.com + flyway: + enabled: false + +jwt: + secret: test_jwt_secret + expiration: 60000 logging: level: - root: INFO - com.zaxxer.hikari.pool: ERROR - org.testcontainers: WARN - org.springframework.test.context: WARN - org.springframework.security.config.annotation.authentication.configuration.InitializeUserDetailsBeanManagerConfigurer: ERROR - + root: WARN + com.handoff: DEBUG \ No newline at end of file diff --git a/handoff-frontend/package-lock.json b/handoff-frontend/package-lock.json index 76cabd0..2c18227 100644 --- a/handoff-frontend/package-lock.json +++ b/handoff-frontend/package-lock.json @@ -37,6 +37,8 @@ "@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.2", + "@tanstack/react-query": "^5.85.3", + "axios": "^1.11.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cmdk": "^1.0.0", @@ -2633,6 +2635,32 @@ "win32" ] }, + "node_modules/@tanstack/query-core": { + "version": "5.85.5", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.85.5.tgz", + "integrity": "sha512-KO0WTob4JEApv69iYp1eGvfMSUkgw//IpMnq+//cORBzXf0smyRwPLrUvEe5qtAEGjwZTXrjxg+oJNP/C00t6w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.85.5", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.85.5.tgz", + "integrity": "sha512-/X4EFNcnPiSs8wM2v+b6DqS5mmGeuJQvxBglmDxl6ZQb5V26ouD2SJYAcC3VjbNwqhY2zjxVD15rDA5nGbMn3A==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.85.5" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -3122,6 +3150,12 @@ "node": ">=10" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/autoprefixer": { "version": "10.4.20", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", @@ -3159,6 +3193,17 @@ "postcss": "^8.1.0" } }, + "node_modules/axios": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3228,6 +3273,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3715,6 +3773,18 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -3920,6 +3990,15 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", @@ -3944,6 +4023,20 @@ "csstype": "^3.0.2" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -3985,6 +4078,51 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -4414,6 +4552,26 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -4429,6 +4587,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -4472,6 +4646,30 @@ "node": ">=6.9.0" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-nonce": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", @@ -4480,6 +4678,19 @@ "node": ">=6" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -4544,6 +4755,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -4559,6 +4782,33 @@ "node": ">=4" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -4864,6 +5114,15 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -4884,6 +5143,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -5295,6 +5575,12 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",