Compare commits

67 Commits

Author SHA1 Message Date
0a60f016e9 Ssecurity update 2026-02-23 09:58:16 +05:30
b97e57e070 Ssecurity update 2026-02-23 09:43:24 +05:30
152ea94034 mapper updte 2026-02-17 19:07:02 +05:30
9348e456a7 mapper updte 2026-02-17 18:42:10 +05:30
96f56fd1ca past image issue resolve 2025-11-21 09:39:27 +05:30
fdab880de2 Service tabs updated 2025-11-21 00:31:10 +05:30
4286934d9d Service tabs updated 2025-11-20 16:21:48 +05:30
1aa92b7501 Changes on 19-11-2025 2025-11-19 14:52:11 +05:30
d1173ed400 Team and service update 2025-11-12 10:13:03 +05:30
8eeed12d0b CSS update 2025-11-07 17:48:04 +05:30
a9cc0a9122 publication added newly 2025-11-06 14:52:45 +05:30
311ca61dea Hero and Service tile added newly 2025-11-06 10:26:42 +05:30
c228fee79c Event update 2025-11-05 16:23:28 +05:30
71af6e4268 Professor update 2025-11-05 15:27:49 +05:30
0df3fc6ae3 Professor update 2025-11-05 12:49:26 +05:30
6d46457973 Update favicon and professor styles 2025-11-03 14:32:47 +05:30
46234722f8 Update favicon 2025-11-03 14:32:27 +05:30
931e49e58b Further updates on 03-11-2025 2025-11-03 14:15:45 +05:30
434d95eeaf Further updates on 03-11-2025 2025-11-03 13:46:43 +05:30
91cb073c78 Domain updadte on cros 2025-10-22 14:02:30 +05:30
53ba9d1beb Domain updadte on cros 2025-10-22 13:37:37 +05:30
953c9dea16 Domain updadte on cros 2025-10-22 13:23:36 +05:30
3d1fab5c72 Domain updadte on cros 2025-10-22 13:01:12 +05:30
ed17ec6c7e Domain updadte on cros 2025-10-22 11:49:48 +05:30
74ea5a62bf Domain updadte on cros 2025-10-22 10:31:04 +05:30
42ed53db2c Domain updadte on cros 2025-10-22 10:26:53 +05:30
41824cfc38 Image issue solve 2025-10-13 20:08:14 +05:30
9643c762d5 Image issue solve 2025-10-13 17:47:20 +05:30
1386459e03 Image error solve update 2025-10-10 19:18:00 +05:30
f5210206f6 Image error solve update 2025-10-10 19:15:32 +05:30
0ad84e030a Image error solve update 2025-10-10 19:15:18 +05:30
5a88ba6993 Image error solve update 2025-10-10 19:11:20 +05:30
b5df742a80 Image error solve update 2025-10-10 19:04:43 +05:30
e6b9a44f9b Image error solve update 2025-10-10 18:38:27 +05:30
677462879e Image error solve update 2025-10-10 18:31:57 +05:30
c7bf66814f Image error solve update 2025-10-10 18:28:19 +05:30
90e4c719fb Image error solve update 2025-10-10 18:21:31 +05:30
3804f39632 Image error solve update 2025-10-10 13:48:37 +05:30
0c3054df1b Image error solve update 2025-10-10 13:29:37 +05:30
279f025432 Image error solve update 2025-10-10 11:33:53 +05:30
94c4f03455 Image error solve update 2025-10-10 11:13:50 +05:30
8e20b100eb Image error solve update 2025-10-10 11:08:41 +05:30
10ee87fce4 Image error solve update 2025-10-10 10:43:03 +05:30
10b80e12ba Image error solve update 2025-10-10 10:32:07 +05:30
43ed620a0a Image error solve update 2025-10-10 10:04:53 +05:30
7763940b2a Image error solve update 2025-10-10 09:48:04 +05:30
2f6c0a08b4 docker update 2025-10-09 22:00:23 +05:30
55bacb9d2d docker update 2025-10-09 21:51:44 +05:30
46343199d8 font change on index 2025-10-09 21:40:20 +05:30
45640454bc docker update 2025-10-09 21:28:22 +05:30
e1abf8631f docker update 2025-10-09 21:25:17 +05:30
93917b63e1 Pom.xml update 2025-10-09 18:36:58 +05:30
b81b4b49ae Profile update resolved 2025-10-09 18:18:21 +05:30
df444a5db2 Dockerfile update 2025-10-09 18:09:08 +05:30
d2dc7eb83b Profile update resolved 2025-10-09 17:38:00 +05:30
c0e6685005 Docker Update 2025-10-09 13:50:24 +05:30
189d4cec77 docker update 2025-09-23 22:40:32 +05:30
464531aad6 docker update 2025-09-23 22:32:39 +05:30
158f97c657 docker update 2025-09-23 22:29:57 +05:30
1728167833 docker update 2025-09-23 22:25:39 +05:30
6525c50679 docker update 2025-09-23 22:22:31 +05:30
badde1665d docker update 2025-09-23 22:11:12 +05:30
0e45929b5e docker update 2025-09-23 22:02:03 +05:30
e72a3ea6ba docker update 2025-09-23 21:47:56 +05:30
106d42860c docker port update 2025-09-23 21:00:04 +05:30
b8b004bf31 docker port update 2025-09-23 20:36:09 +05:30
bd2f5b95ce Update with new components 2025-09-23 19:41:25 +05:30
242 changed files with 32360 additions and 5078 deletions

View File

@ -1,26 +1,70 @@
version: '3.1'
version: '3.8'
services:
cmc-new-backend:
backend:
build:
context: ./support-portal-backend
context: support-portal-backend
dockerfile: Dockerfile
restart: always
ports:
- "8070:8080"
- "8080:8080"
environment:
- DATABASE_HOST=mysql-common-mysql-1
- SPRING_DATASOURCE_URL=jdbc:mysql://mysql-common-mysql-1:3306/demo
- SPRING_DATASOURCE_USERNAME=youruser
- SPRING_DATASOURCE_PASSWORD=youruserpassword
MYSQL_HOST: db
MYSQL_USER: support_portal_user
MYSQL_PASSWORD: support_portal_password
MYSQL_DATABASE: support-portal
# ✅ Activate production profile and enforce HTTPS base URL
SPRING_PROFILES_ACTIVE: production
APP_BASE_URL: https://cmcbackend.rootxwire.com
# ✅ Optional: ensures Spring detects HTTPS correctly behind reverse proxy
SERVER_FORWARD_HEADERS_STRATEGY: native
volumes:
# Persist uploaded images and files
- blog-uploads:/app/uploads
networks:
- cmc-forntend
- mysql-common_mynetwork
- angular-spring
- spring-mysql
depends_on:
db:
condition: service_healthy
db:
image: mysql:8.0.19
restart: always
environment:
MYSQL_USER: support_portal_user
MYSQL_PASSWORD: support_portal_password
MYSQL_DATABASE: support-portal
MYSQL_ROOT_PASSWORD: root_password
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "--silent"]
interval: 3s
retries: 5
start_period: 30s
volumes:
- db-data:/var/lib/mysql
networks:
- spring-mysql
frontend:
build:
context: support-portal-frontend
dockerfile: Dockerfile
restart: always
ports:
- "8072:80"
networks:
- angular-spring
depends_on:
- backend
volumes:
db-data: {}
blog-uploads: {}
networks:
cmc-forntend: {}
mysql-common_mynetwork:
external: true
angular-spring: {}
spring-mysql: {}

View File

@ -17,7 +17,7 @@ services:
image: adminer
restart: always
ports:
- 8081:8080
- 8082:8080
#https://medium.com/dandelion-tutorials/using-s3-localstack-with-spring-boot-and-r2dbc-5ea201a18aea

View File

@ -1,15 +1,46 @@
FROM --platform=$BUILDPLATFORM maven:3.8.5-eclipse-temurin-17 AS builder
WORKDIR /workdir/server
COPY pom.xml /workdir/server/pom.xml
RUN mvn dependency:go-offline
# =======================
# Builder stage
# =======================
FROM maven:3.8.5-eclipse-temurin-17 AS builder
COPY src /workdir/server/src
RUN mvn package -Dmaven.test.skip=true
WORKDIR /workdir/server
# Copy only pom.xml first for better layer caching
COPY pom.xml .
# Download dependencies with optimization flags
RUN mvn dependency:resolve dependency:resolve-plugins -B -T 1C
# Copy source code
COPY src ./src
# Build the application
RUN mvn package -B -T 1C -DskipTests -Dmaven.javadoc.skip=true
# Verify the JAR was created
RUN ls -la target/
# =======================
# Runtime stage
# =======================
FROM eclipse-temurin:17-jre-focal
# Expose port
EXPOSE 8080
VOLUME /tmp
COPY --from=builder /workdir/server/target/*.jar /app/app.jar
ENTRYPOINT ["java","-jar","/app/app.jar"]
# Create app and uploads directories
RUN mkdir -p /app && \
mkdir -p /app/uploads && \
chmod 755 /app/uploads
# Set working directory
WORKDIR /app
# Copy the JAR from builder stage
COPY --from=builder /workdir/server/target/*.jar /app/
# Create volume for uploads (optional but recommended)
VOLUME ["/app/uploads"]
# Entry point: dynamically run the first JAR in /app
ENTRYPOINT ["sh", "-c", "java -jar /app/$(ls /app | grep .jar | head -n1)"]

View File

@ -166,16 +166,20 @@
</dependencyManagement>
<build>
<finalName>support-portal-backend</finalName>
<plugins>
<!-- Spring Boot plugin (repackage to make it executable) -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.5.4</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
<configuration>
<excludes>
<exclude>
@ -183,11 +187,10 @@
<artifactId>lombok</artifactId>
</exclude>
</excludes>
<!-- <executable>true</executable>-->
<finalName>support-portal</finalName>
</configuration>
</plugin>
<!-- Compiler plugin for MapStruct and Lombok -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
@ -206,61 +209,13 @@
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<version>1.18.28</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>0.33.0</version>
<configuration>
<dockerHost>http://dockerapp.shyshkin.net:2375</dockerHost>
<verbose>true</verbose>
<containerNamePattern>%a</containerNamePattern>
<images>
<image>
<name>${docker.image.prefix}/${docker.image.name}</name>
<alias>${docker.image.name}</alias>
<build>
<assembly>
<descriptorRef>artifact</descriptorRef>
</assembly>
<dockerFile>Dockerfile</dockerFile>
<tags>
<tag>latest</tag>
<tag>${project.version}</tag>
</tags>
</build>
<run>
<ports>
<port>443:8080</port>
</ports>
<env>
<SPRING_PROFILES_ACTIVE>aws-rds,image-s3</SPRING_PROFILES_ACTIVE>
</env>
<restartPolicy>
<name>always</name>
</restartPolicy>
<volumes>
<bind>
<volume>/home/ec2-user/supportportal:/root/supportportal</volume>
</bind>
</volumes>
</run>
</image>
</images>
</configuration>
</plugin>
</plugins>
</build>
</build>
</project>

View File

@ -7,6 +7,7 @@ import net.shyshkin.study.fullstack.supportportal.backend.filter.JwtAuthenticati
import net.shyshkin.study.fullstack.supportportal.backend.filter.JwtAuthorizationFilter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
@ -17,16 +18,18 @@ import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.List;
import java.util.Arrays;
import static org.springframework.http.HttpMethod.*;
import static org.springframework.http.HttpMethod.POST;
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtAuthorizationFilter jwtAuthorizationFilter;
@ -38,17 +41,19 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${app.public-urls}")
private String[] publicUrls;
@Value("${app.cors.allowed-origins}")
private String[] allowedOrigins;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.cors();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests()
.antMatchers(publicUrls).permitAll()
.antMatchers(POST, "/api/files/upload").permitAll() // Allow file uploads
.anyRequest().authenticated();
http.exceptionHandling()
@ -60,9 +65,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder);
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
}
@Bean
@ -72,24 +75,17 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
@Bean
public WebMvcConfigurer corsConfigurer(@Value("${app.cors.allowed-origins}") String[] allowedOrigins) {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/user/login")
.allowedOrigins(allowedOrigins)
.exposedHeaders(SecurityConstants.JWT_TOKEN_HEADER);
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Arrays.asList(allowedOrigins));
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(Arrays.asList("*"));
config.setExposedHeaders(Arrays.asList(SecurityConstants.JWT_TOKEN_HEADER));
config.setAllowCredentials(true);
config.setMaxAge(3600L); // 1 hour
String[] allowedMethods = List.of(GET, POST, PUT, DELETE)
.stream()
.map(Enum::name)
.toArray(String[]::new);
registry.addMapping("/**")
.allowedMethods(allowedMethods)
.allowedOrigins(allowedOrigins);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
};
}
}

View File

@ -0,0 +1,29 @@
package net.shyshkin.study.fullstack.supportportal.backend.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.io.File;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Value("${file.upload.directory}")
private String uploadDirectory;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// Ensure the path ends with a separator
String uploadPath = uploadDirectory;
if (!uploadPath.endsWith(File.separator)) {
uploadPath += File.separator;
}
// Serve uploaded files under /uploads/**
registry.addResourceHandler("/uploads/**")
.addResourceLocations("file:" + uploadPath)
.setCachePeriod(3600); // Cache for 1 hour
}
}

View File

@ -4,7 +4,7 @@ public class FileConstant {
public static final String USER_IMAGE_PATH = "/user/image/";
public static final String JPG_EXTENSION = "jpg";
public static final String USER_FOLDER = System.getProperty("user.home") + "/supportportal/user/";
public static final String USER_FOLDER = "/app/uploads/user/";
public static final String DIRECTORY_CREATED = "Created directory for: ";
public static final String DEFAULT_USER_IMAGE_URI_PATTERN = "/user/%s/profile-image";
public static final String USER_IMAGE_FILENAME = "avatar.jpg";
@ -14,10 +14,15 @@ public class FileConstant {
public static final String NOT_AN_IMAGE_FILE = " is not an image file. Please upload an image file";
public static final String TEMP_PROFILE_IMAGE_BASE_URL = "https://robohash.org/";
// public static final String PROFESSOR_IMAGE_PATH = "/professor/image/";
// public static final String PROFESSOR_FOLDER = System.getProperty("user.home") + "/supportportal/professor/";
// public static final String DEFAULT_PROFESSOR_IMAGE_URI_PATTERN = "/professor/%s/profile-image";
// public static final String PROFESSOR_IMAGE_FILENAME = "avatar.jpg";
// Professor-specific constants
public static final String PROFESSOR_IMAGE_PATH = "/professor/image/";
public static final String PROFESSOR_FOLDER = "/app/uploads/professor/";
public static final String DEFAULT_PROFESSOR_IMAGE_URI_PATTERN = "/professor/%s/profile-image";
public static final String PROFESSOR_IMAGE_FILENAME = "avatar.jpg";
// ✅ NEW: Hero Image constants
public static final String HERO_IMAGE_PATH = "/hero/image/";
public static final String HERO_FOLDER = "/app/uploads/hero/";
public static final String DEFAULT_HERO_IMAGE_URI_PATTERN = "/hero/%s/image";
public static final String HERO_IMAGE_PREFIX = "hero_";
}

View File

@ -0,0 +1,97 @@
// CourseApplicationController.java - REST Controller for Course Applications
package net.shyshkin.study.fullstack.supportportal.backend.controller;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Course;
import net.shyshkin.study.fullstack.supportportal.backend.domain.CourseApplication;
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.CourseApplicationDto;
import net.shyshkin.study.fullstack.supportportal.backend.repository.CourseRepository;
import net.shyshkin.study.fullstack.supportportal.backend.repository.CourseApplicationRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import com.amazonaws.services.secretsmanager.model.ResourceNotFoundException;
import java.util.List;
@RestController
@RequestMapping("/api/course-applications")
@CrossOrigin(origins = "*")
public class CourseApplicationController {
@Autowired
private CourseApplicationRepository courseApplicationRepository;
@Autowired
private CourseRepository courseRepository;
// Get all applications (for admin)
@GetMapping
public List<CourseApplication> getAllApplications() {
return courseApplicationRepository.findAll();
}
// Get applications by course ID
@GetMapping("/course/{courseId}")
public ResponseEntity<List<CourseApplication>> getApplicationsByCourseId(@PathVariable Long courseId) {
try {
List<CourseApplication> applications = courseApplicationRepository.findAllByCourseId(courseId);
return ResponseEntity.ok(applications);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
// Get a single application by ID
@GetMapping("/{id}")
public ResponseEntity<CourseApplication> getApplicationById(@PathVariable Long id) {
return courseApplicationRepository.findById(id)
.map(application -> ResponseEntity.ok(application))
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<?> createApplication(@RequestBody CourseApplicationDto applicationDto) {
Course course = courseRepository.findById(applicationDto.getCourseId())
.orElseThrow(() -> new ResourceNotFoundException("Course not found"));
CourseApplication application = new CourseApplication();
application.setCourse(course);
application.setFullName(applicationDto.getFullName());
application.setEmail(applicationDto.getEmail());
application.setPhone(applicationDto.getPhone());
application.setQualification(applicationDto.getQualification());
application.setExperience(applicationDto.getExperience());
application.setCoverLetter(applicationDto.getCoverLetter());
application.setResumeUrl(applicationDto.getResumeUrl());
courseApplicationRepository.save(application);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
@PutMapping("/{id}/status")
public ResponseEntity<?> updateApplicationStatus(@PathVariable Long id, @RequestParam String status) {
CourseApplication application = courseApplicationRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Application not found"));
try {
application.setStatus(CourseApplication.ApplicationStatus.valueOf(status.toUpperCase()));
courseApplicationRepository.save(application);
return ResponseEntity.ok().build();
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body("Invalid status: " + status);
}
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteApplication(@PathVariable Long id) {
return courseApplicationRepository.findById(id)
.map(application -> {
courseApplicationRepository.delete(application);
return ResponseEntity.noContent().<Void>build();
})
.orElse(ResponseEntity.notFound().build());
}
}

View File

@ -0,0 +1,122 @@
// CourseController.java - REST Controller for Courses
package net.shyshkin.study.fullstack.supportportal.backend.controller;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Course;
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.CourseDto;
import net.shyshkin.study.fullstack.supportportal.backend.repository.CourseRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import com.amazonaws.services.secretsmanager.model.ResourceNotFoundException;
import java.util.List;
@RestController
@RequestMapping("/api/courses")
@CrossOrigin(origins = "*")
public class CourseController {
@Autowired
private CourseRepository courseRepository;
// Get all active courses (for public display)
@GetMapping("/active")
public ResponseEntity<List<Course>> getActiveCourses() {
try {
List<Course> courses = courseRepository.findAllByIsActiveTrue();
return ResponseEntity.ok(courses);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
// Get all past/inactive courses (for public display)
@GetMapping("/past")
public ResponseEntity<List<Course>> getPastCourses() {
try {
List<Course> courses = courseRepository.findAllByIsActiveFalse();
return ResponseEntity.ok(courses);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
// Get all courses (for admin)
@GetMapping
public List<Course> getAllCourses() {
return courseRepository.findAll();
}
// Get a single course by ID
@GetMapping("/{id}")
public ResponseEntity<Course> getCourseById(@PathVariable Long id) {
return courseRepository.findById(id)
.map(course -> ResponseEntity.ok(course))
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<?> createCourse(@RequestBody CourseDto courseDto) {
Course course = new Course();
course.setTitle(courseDto.getTitle());
course.setDescription(courseDto.getDescription());
course.setDuration(courseDto.getDuration());
course.setSeats(courseDto.getSeats());
course.setCategory(courseDto.getCategory());
course.setLevel(courseDto.getLevel());
course.setInstructor(courseDto.getInstructor());
course.setPrice(courseDto.getPrice());
course.setStartDate(courseDto.getStartDate());
course.setImageUrl(courseDto.getImageUrl());
course.setEligibility(courseDto.getEligibility());
course.setObjectives(courseDto.getObjectives());
// FIXED: Use getIsActive() and setIsActive() for both DTO and Entity
// Handle null case - default to true
Boolean isActiveValue = courseDto.getIsActive() != null ? courseDto.getIsActive() : true;
course.setIsActive(isActiveValue);
Course savedCourse = courseRepository.save(course);
return ResponseEntity.status(HttpStatus.CREATED).body(savedCourse);
}
@PutMapping("/{id}")
public ResponseEntity<?> updateCourse(@PathVariable Long id, @RequestBody CourseDto courseDto) {
Course course = courseRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Course not found"));
course.setTitle(courseDto.getTitle());
course.setDescription(courseDto.getDescription());
course.setDuration(courseDto.getDuration());
course.setSeats(courseDto.getSeats());
course.setCategory(courseDto.getCategory());
course.setLevel(courseDto.getLevel());
course.setInstructor(courseDto.getInstructor());
course.setPrice(courseDto.getPrice());
course.setStartDate(courseDto.getStartDate());
course.setImageUrl(courseDto.getImageUrl());
course.setEligibility(courseDto.getEligibility());
course.setObjectives(courseDto.getObjectives());
// FIXED: Use getIsActive() and setIsActive() for both DTO and Entity
// Handle null case - default to true
Boolean isActiveValue = courseDto.getIsActive() != null ? courseDto.getIsActive() : true;
course.setIsActive(isActiveValue);
Course updatedCourse = courseRepository.save(course);
return ResponseEntity.ok(updatedCourse);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteCourse(@PathVariable Long id) {
return courseRepository.findById(id)
.map(course -> {
courseRepository.delete(course);
return ResponseEntity.noContent().<Void>build();
})
.orElse(ResponseEntity.notFound().build());
}
}

View File

@ -1,3 +1,4 @@
// EventController.java - FIXED
package net.shyshkin.study.fullstack.supportportal.backend.controller;
import org.springframework.beans.factory.annotation.Autowired;
@ -13,6 +14,7 @@ import java.util.Optional;
@RestController
@RequestMapping("/api/events")
@CrossOrigin(origins = "*", methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE, RequestMethod.OPTIONS})
public class EventController {
@Autowired
@ -33,8 +35,12 @@ public class EventController {
@PostMapping
public ResponseEntity<Event> createEvent(@RequestBody Event event) {
try {
Event savedEvent = eventRepository.save(event);
return new ResponseEntity<>(savedEvent, HttpStatus.CREATED);
} catch (Exception e) {
return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@PutMapping("/{id}")
@ -42,9 +48,13 @@ public class EventController {
if (!eventRepository.existsById(id)) {
return ResponseEntity.notFound().build();
}
try {
event.setId(id);
Event updatedEvent = eventRepository.save(event);
return new ResponseEntity<>(updatedEvent, HttpStatus.OK);
} catch (Exception e) {
return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@DeleteMapping("/{id}")
@ -52,7 +62,25 @@ public class EventController {
if (!eventRepository.existsById(id)) {
return ResponseEntity.notFound().build();
}
try {
eventRepository.deleteById(id);
return ResponseEntity.noContent().build();
} catch (Exception e) {
return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
// Upcoming events - ACTIVE events ordered by date ASC
@GetMapping("/upcoming")
public ResponseEntity<List<Event>> getUpcomingEvents() {
List<Event> events = eventRepository.findByIsActiveTrueOrderByDateAsc();
return new ResponseEntity<>(events, HttpStatus.OK);
}
// FIXED: Past events - INACTIVE events ordered by date DESC
@GetMapping("/past")
public ResponseEntity<List<Event>> getPastEvents() {
List<Event> events = eventRepository.findByIsActiveFalseOrderByDateDesc();
return new ResponseEntity<>(events, HttpStatus.OK);
}
}

View File

@ -0,0 +1,213 @@
package net.shyshkin.study.fullstack.supportportal.backend.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/api/files")
@CrossOrigin(origins = {
"https://cmcbackend.rootxwire.com",
"https://maincmc.rootxwire.com",
"https://cmctrauma.com"
})
public class FileController {
@Value("${file.upload.directory:uploads}")
private String uploadDirectory;
@Value("${app.base-url:http://localhost:8080}")
private String baseUrl;
private static final List<String> ALLOWED_EXTENSIONS = Arrays.asList(
".jpg", ".jpeg", ".png", ".gif", ".webp",
".pdf", ".doc", ".docx"
);
private static final List<String> ALLOWED_CONTENT_TYPES = Arrays.asList(
"image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp",
"application/pdf",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
);
private static final long MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
@PostMapping("/upload")
public ResponseEntity<?> uploadFile(@RequestParam("file") MultipartFile file) {
try {
// 1. Check empty
if (file.isEmpty()) {
return ResponseEntity.badRequest().body("File is empty");
}
// 2. Check file size
if (file.getSize() > MAX_FILE_SIZE) {
return ResponseEntity.badRequest().body("File size exceeds 5MB limit");
}
// 3. Get and sanitize original filename
String originalFilename = file.getOriginalFilename();
if (originalFilename == null || originalFilename.isBlank()) {
return ResponseEntity.badRequest().body("Invalid filename");
}
// Prevent path traversal - only take the actual filename
String sanitizedFilename = Paths.get(originalFilename).getFileName().toString();
// 4. Validate extension (cannot be spoofed by client unlike Content-Type)
String extension = getExtension(sanitizedFilename);
if (extension == null || !ALLOWED_EXTENSIONS.contains(extension.toLowerCase())) {
return ResponseEntity.badRequest()
.body("Invalid file type. Allowed: JPG, PNG, GIF, WEBP, PDF, DOC, DOCX");
}
// 5. Validate Content-Type (secondary check)
String contentType = file.getContentType();
if (!ALLOWED_CONTENT_TYPES.contains(contentType)) {
return ResponseEntity.badRequest()
.body("Invalid content type.");
}
// 6. Validate Content-Type matches extension
if (!isContentTypeMatchingExtension(contentType, extension)) {
return ResponseEntity.badRequest()
.body("File type mismatch detected.");
}
// 7. Create upload directory if needed
Path uploadPath = Paths.get(uploadDirectory);
if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath);
}
// 8. Generate safe unique filename
String uniqueFilename = UUID.randomUUID().toString() + extension.toLowerCase();
// 9. Resolve path safely (prevent path traversal)
Path filePath = uploadPath.resolve(uniqueFilename).normalize();
if (!filePath.startsWith(uploadPath.normalize())) {
return ResponseEntity.badRequest().body("Invalid file path");
}
// 10. Save file
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
String fileUrl = baseUrl + "/uploads/" + uniqueFilename;
return ResponseEntity.ok(Map.of(
"url", fileUrl,
"filename", uniqueFilename
));
} catch (IOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Failed to upload file: " + e.getMessage());
}
}
@DeleteMapping("/images/{filename}")
public ResponseEntity<?> deleteImage(@PathVariable String filename) {
try {
// Prevent path traversal
String sanitized = Paths.get(filename).getFileName().toString();
Path uploadPath = Paths.get(uploadDirectory).normalize();
Path filePath = uploadPath.resolve(sanitized).normalize();
if (!filePath.startsWith(uploadPath)) {
return ResponseEntity.badRequest().body("Invalid filename");
}
if (!Files.exists(filePath)) {
return ResponseEntity.notFound().build();
}
Files.delete(filePath);
return ResponseEntity.ok(Map.of("message", "File deleted successfully"));
} catch (IOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Failed to delete file: " + e.getMessage());
}
}
@GetMapping("/images/{filename}")
public ResponseEntity<byte[]> getImage(@PathVariable String filename) {
try {
// Prevent path traversal
String sanitized = Paths.get(filename).getFileName().toString();
Path uploadPath = Paths.get(uploadDirectory).normalize();
Path filePath = uploadPath.resolve(sanitized).normalize();
if (!filePath.startsWith(uploadPath)) {
return ResponseEntity.badRequest().build();
}
if (!Files.exists(filePath)) {
return ResponseEntity.notFound().build();
}
// Validate it's an allowed file type before serving
String ext = getExtension(sanitized);
if (ext == null || !ALLOWED_EXTENSIONS.contains(ext.toLowerCase())) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
byte[] fileBytes = Files.readAllBytes(filePath);
String contentType = Files.probeContentType(filePath);
return ResponseEntity.ok()
.header("Content-Type", contentType != null ? contentType : "application/octet-stream")
.header("Content-Disposition", "inline; filename=\"" + sanitized + "\"")
.body(fileBytes);
} catch (IOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
// --- Helpers ---
private String getExtension(String filename) {
int dotIndex = filename.lastIndexOf(".");
if (dotIndex < 0 || dotIndex == filename.length() - 1) return null;
return filename.substring(dotIndex);
}
private boolean isContentTypeMatchingExtension(String contentType, String extension) {
String ext = extension.toLowerCase();
switch (ext) {
case ".jpg":
case ".jpeg":
return contentType.equals("image/jpeg") || contentType.equals("image/jpg");
case ".png":
return contentType.equals("image/png");
case ".gif":
return contentType.equals("image/gif");
case ".webp":
return contentType.equals("image/webp");
case ".pdf":
return contentType.equals("application/pdf");
case ".doc":
return contentType.equals("application/msword");
case ".docx":
return contentType.equals("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
default:
return false;
}
}
}

View File

@ -0,0 +1,110 @@
package net.shyshkin.study.fullstack.supportportal.backend.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.shyshkin.study.fullstack.supportportal.backend.domain.HeroImage;
import net.shyshkin.study.fullstack.supportportal.backend.domain.HttpResponse;
import net.shyshkin.study.fullstack.supportportal.backend.service.HeroImageService;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import static net.shyshkin.study.fullstack.supportportal.backend.constant.FileConstant.HERO_FOLDER;
@RestController
@RequestMapping("/hero")
@RequiredArgsConstructor
@Slf4j
@CrossOrigin
public class HeroImageResource {
private final HeroImageService heroImageService;
@PostMapping
@PreAuthorize("hasAnyAuthority('user:create')")
public ResponseEntity<HeroImage> addHeroImage(
@RequestParam(required = false) String title,
@RequestParam(required = false) String subtitle,
@RequestParam(required = false) String description,
@RequestParam(required = false) MultipartFile image) throws IOException {
log.info("Adding new hero image with title: {}", title);
HeroImage heroImage = heroImageService.addHeroImage(title, subtitle, description, image);
return ResponseEntity.status(HttpStatus.CREATED).body(heroImage);
}
@PutMapping("/{id}")
@PreAuthorize("hasAnyAuthority('user:update')")
public ResponseEntity<HeroImage> updateHeroImage(
@PathVariable Long id,
@RequestParam(required = false) String title,
@RequestParam(required = false) String subtitle,
@RequestParam(required = false) String description,
@RequestParam(required = false) MultipartFile image) throws IOException {
log.info("Updating hero image with id: {}", id);
HeroImage heroImage = heroImageService.updateHeroImage(id, title, subtitle, description, image);
return ResponseEntity.ok(heroImage);
}
@GetMapping("/active")
public ResponseEntity<HeroImage> getActiveHeroImage() {
log.info("Getting active hero image");
HeroImage heroImage = heroImageService.getActiveHeroImage();
return ResponseEntity.ok(heroImage);
}
@GetMapping("/{id}")
public ResponseEntity<HeroImage> getHeroImageById(@PathVariable Long id) {
log.info("Getting hero image with id: {}", id);
HeroImage heroImage = heroImageService.getHeroImageById(id);
return ResponseEntity.ok(heroImage);
}
@GetMapping
public ResponseEntity<List<HeroImage>> getAllHeroImages() {
log.info("Getting all hero images");
List<HeroImage> heroImages = heroImageService.getAllHeroImages();
return ResponseEntity.ok(heroImages);
}
@DeleteMapping("/{id}")
@PreAuthorize("hasAnyAuthority('user:delete')")
public ResponseEntity<HttpResponse> deleteHeroImage(@PathVariable Long id) {
log.info("Deleting hero image with id: {}", id);
heroImageService.deleteHeroImage(id);
return ResponseEntity.ok(
HttpResponse.builder()
.httpStatusCode(HttpStatus.OK.value())
.httpStatus(HttpStatus.OK)
.reason(HttpStatus.OK.getReasonPhrase())
.message("Hero image deleted successfully")
.build()
);
}
@PutMapping("/{id}/activate")
@PreAuthorize("hasAnyAuthority('user:update')")
public ResponseEntity<HeroImage> setActiveHeroImage(@PathVariable Long id) {
log.info("Setting hero image as active with id: {}", id);
HeroImage heroImage = heroImageService.setActiveHeroImage(id);
return ResponseEntity.ok(heroImage);
}
@GetMapping(path = "/image/{filename}", produces = {MediaType.IMAGE_JPEG_VALUE, MediaType.IMAGE_PNG_VALUE, MediaType.IMAGE_GIF_VALUE})
public byte[] getHeroImage(@PathVariable String filename) throws IOException {
log.info("Getting hero image file: {}", filename);
Path imagePath = Paths.get(HERO_FOLDER).resolve(filename);
return Files.readAllBytes(imagePath);
}
}

View File

@ -0,0 +1,127 @@
package net.shyshkin.study.fullstack.supportportal.backend.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.shyshkin.study.fullstack.supportportal.backend.service.ProfileImageService;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.Duration;
import java.util.UUID;
@Slf4j
@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
@CrossOrigin(origins = "*")
public class ImageController {
private final ProfileImageService profileImageService;
@GetMapping("/{professorId}/profile-image/{filename}")
public ResponseEntity<byte[]> getProfileImage(
@PathVariable UUID professorId,
@PathVariable String filename) {
try {
log.debug("Fetching profile image for professor: {} with filename: {}", professorId, filename);
byte[] imageBytes = profileImageService.retrieveProfileImage(professorId, filename);
if (imageBytes == null || imageBytes.length == 0) {
log.warn("No image data found for professor: {} with filename: {}", professorId, filename);
return ResponseEntity.notFound().build();
}
HttpHeaders headers = new HttpHeaders();
// Determine content type based on filename
String contentType = getContentTypeFromFilename(filename);
headers.setContentType(MediaType.parseMediaType(contentType));
// Set cache control
headers.setCacheControl(CacheControl.maxAge(Duration.ofHours(1)).cachePublic());
log.debug("Successfully retrieved image for professor: {}, size: {} bytes", professorId, imageBytes.length);
return ResponseEntity.ok()
.headers(headers)
.body(imageBytes);
} catch (Exception e) {
log.error("Error retrieving profile image for professor: {} with filename: {}", professorId, filename, e);
return ResponseEntity.notFound().build();
}
}
@GetMapping("/{professorId}/profile-image")
public ResponseEntity<byte[]> getDefaultProfileImage(@PathVariable UUID professorId) {
try {
log.debug("Fetching default profile image for professor: {}", professorId);
// Try to get the default image (avatar.jpg or similar)
byte[] imageBytes = profileImageService.retrieveProfileImage(professorId, "avatar.jpg");
if (imageBytes == null || imageBytes.length == 0) {
log.warn("No default image found for professor: {}", professorId);
return ResponseEntity.notFound().build();
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.IMAGE_JPEG);
headers.setCacheControl(CacheControl.maxAge(Duration.ofHours(1)).cachePublic());
log.debug("Successfully retrieved default image for professor: {}, size: {} bytes", professorId, imageBytes.length);
return ResponseEntity.ok()
.headers(headers)
.body(imageBytes);
} catch (Exception e) {
log.error("Error retrieving default profile image for professor: {}", professorId, e);
return ResponseEntity.notFound().build();
}
}
/**
* Endpoint to check if a profile image exists
*/
@GetMapping("/{professorId}/profile-image/exists")
public ResponseEntity<Boolean> checkProfileImageExists(@PathVariable UUID professorId) {
try {
byte[] imageBytes = profileImageService.retrieveProfileImage(professorId, "avatar.jpg");
boolean exists = imageBytes != null && imageBytes.length > 0;
return ResponseEntity.ok(exists);
} catch (Exception e) {
log.error("Error checking if profile image exists for professor: {}", professorId, e);
return ResponseEntity.ok(false);
}
}
/**
* Determine content type based on file extension
*/
private String getContentTypeFromFilename(String filename) {
if (filename == null) {
return MediaType.IMAGE_JPEG_VALUE;
}
String lowerCaseFilename = filename.toLowerCase();
if (lowerCaseFilename.endsWith(".png")) {
return MediaType.IMAGE_PNG_VALUE;
} else if (lowerCaseFilename.endsWith(".gif")) {
return MediaType.IMAGE_GIF_VALUE;
} else if (lowerCaseFilename.endsWith(".webp")) {
return "image/webp";
} else if (lowerCaseFilename.endsWith(".bmp")) {
return "image/bmp";
} else {
// Default to JPEG
return MediaType.IMAGE_JPEG_VALUE;
}
}
}

View File

@ -0,0 +1,170 @@
package net.shyshkin.study.fullstack.supportportal.backend.controller;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Job;
import net.shyshkin.study.fullstack.supportportal.backend.domain.JobApplication;
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.JobApplicationDto;
import net.shyshkin.study.fullstack.supportportal.backend.repository.JobRepository;
import net.shyshkin.study.fullstack.supportportal.backend.repository.JobApplicationRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import com.amazonaws.services.secretsmanager.model.ResourceNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
@RestController
@RequestMapping("/api/job-applications")
@CrossOrigin(origins = "*")
public class JobApplicationController {
@Autowired
private JobApplicationRepository jobApplicationRepository;
@Autowired
private JobRepository jobRepository;
// Base path for resume storage - adjust this to your actual path
private final String RESUME_UPLOAD_DIR = "uploads/resumes/";
// Get all applications (for admin)
@GetMapping
public List<JobApplication> getAllApplications() {
return jobApplicationRepository.findAll();
}
// Get applications by job ID
@GetMapping("/job/{jobId}")
public ResponseEntity<List<JobApplication>> getApplicationsByJobId(@PathVariable Long jobId) {
try {
List<JobApplication> applications = jobApplicationRepository.findAllByJobId(jobId);
return ResponseEntity.ok(applications);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
// Get a single application by ID
@GetMapping("/{id}")
public ResponseEntity<JobApplication> getApplicationById(@PathVariable Long id) {
return jobApplicationRepository.findById(id)
.map(application -> ResponseEntity.ok(application))
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<?> createApplication(@RequestBody JobApplicationDto applicationDto) {
Job job = jobRepository.findById(applicationDto.getJobId())
.orElseThrow(() -> new ResourceNotFoundException("Job not found"));
JobApplication application = new JobApplication();
application.setJob(job);
application.setFullName(applicationDto.getFullName());
application.setEmail(applicationDto.getEmail());
application.setPhone(applicationDto.getPhone());
application.setExperience(applicationDto.getExperience());
application.setCoverLetter(applicationDto.getCoverLetter());
application.setResumeUrl(applicationDto.getResumeUrl());
jobApplicationRepository.save(application);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
@PutMapping("/{id}/status")
public ResponseEntity<?> updateApplicationStatus(@PathVariable Long id, @RequestParam String status) {
JobApplication application = jobApplicationRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Application not found"));
try {
application.setStatus(JobApplication.ApplicationStatus.valueOf(status.toUpperCase()));
jobApplicationRepository.save(application);
return ResponseEntity.ok().build();
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body("Invalid status: " + status);
}
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteApplication(@PathVariable Long id) {
return jobApplicationRepository.findById(id)
.map(application -> {
jobApplicationRepository.delete(application);
return ResponseEntity.noContent().<Void>build();
})
.orElse(ResponseEntity.notFound().build());
}
// Download/View Resume
@GetMapping("/resume/{filename:.+}")
public ResponseEntity<Resource> downloadResume(@PathVariable String filename) {
try {
// Construct the file path
Path filePath = Paths.get(RESUME_UPLOAD_DIR).resolve(filename).normalize();
Resource resource = new UrlResource(filePath.toUri());
if (resource.exists() && resource.isReadable()) {
// Determine content type
String contentType = "application/octet-stream";
try {
contentType = Files.probeContentType(filePath);
if (contentType == null) {
contentType = "application/pdf"; // Default to PDF
}
} catch (IOException ex) {
contentType = "application/pdf";
}
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION,
"inline; filename=\"" + resource.getFilename() + "\"")
.body(resource);
} else {
return ResponseEntity.notFound().build();
}
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
// Alternative endpoint for forced download (not inline view)
@GetMapping("/resume/download/{filename:.+}")
public ResponseEntity<Resource> forceDownloadResume(@PathVariable String filename) {
try {
Path filePath = Paths.get(RESUME_UPLOAD_DIR).resolve(filename).normalize();
Resource resource = new UrlResource(filePath.toUri());
if (resource.exists() && resource.isReadable()) {
String contentType = "application/octet-stream";
try {
contentType = Files.probeContentType(filePath);
if (contentType == null) {
contentType = "application/pdf";
}
} catch (IOException ex) {
contentType = "application/pdf";
}
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + resource.getFilename() + "\"")
.body(resource);
} else {
return ResponseEntity.notFound().build();
}
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
}

View File

@ -0,0 +1,106 @@
// JobController.java - REST Controller for Jobs
package net.shyshkin.study.fullstack.supportportal.backend.controller;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Job;
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.JobDto;
import net.shyshkin.study.fullstack.supportportal.backend.repository.JobRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import com.amazonaws.services.secretsmanager.model.ResourceNotFoundException;
import java.util.List;
@RestController
@RequestMapping("/api/jobs")
@CrossOrigin(origins = "*")
public class JobController {
@Autowired
private JobRepository jobRepository;
@GetMapping("/active")
public ResponseEntity<List<Job>> getActiveJobs() {
try {
List<Job> jobs = jobRepository.findAllByIsActiveTrue();
return ResponseEntity.ok(jobs);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@GetMapping
public List<Job> getAllJobs() {
return jobRepository.findAll();
}
@GetMapping("/{id}")
public ResponseEntity<Job> getJobById(@PathVariable Long id) {
return jobRepository.findById(id)
.map(job -> ResponseEntity.ok(job))
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<?> createJob(@RequestBody JobDto jobDto) {
System.out.println("=== BACKEND DEBUG ===");
System.out.println("Received JobDto: " + jobDto);
System.out.println("isActive value: " + jobDto.getIsActive());
Job job = new Job();
job.setTitle(jobDto.getTitle());
job.setDepartment(jobDto.getDepartment());
job.setLocation(jobDto.getLocation());
job.setType(jobDto.getType());
job.setExperience(jobDto.getExperience());
job.setSalary(jobDto.getSalary());
job.setDescription(jobDto.getDescription());
job.setRequirements(jobDto.getRequirements());
job.setResponsibilities(jobDto.getResponsibilities());
Boolean isActiveValue = jobDto.getIsActive() != null ? jobDto.getIsActive() : true;
job.setIsActive(isActiveValue);
System.out.println("Job before save - isActive: " + job.getIsActive());
Job savedJob = jobRepository.save(job);
System.out.println("Job after save - isActive: " + savedJob.getIsActive());
return ResponseEntity.status(HttpStatus.CREATED).body(savedJob);
}
@PutMapping("/{id}")
public ResponseEntity<?> updateJob(@PathVariable Long id, @RequestBody JobDto jobDto) {
Job job = jobRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Job not found"));
job.setTitle(jobDto.getTitle());
job.setDepartment(jobDto.getDepartment());
job.setLocation(jobDto.getLocation());
job.setType(jobDto.getType());
job.setExperience(jobDto.getExperience());
job.setSalary(jobDto.getSalary());
job.setDescription(jobDto.getDescription());
job.setRequirements(jobDto.getRequirements());
job.setResponsibilities(jobDto.getResponsibilities());
Boolean isActiveValue = jobDto.getIsActive() != null ? jobDto.getIsActive() : true;
job.setIsActive(isActiveValue);
Job updatedJob = jobRepository.save(job);
return ResponseEntity.ok(updatedJob);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteJob(@PathVariable Long id) {
return jobRepository.findById(id)
.map(job -> {
jobRepository.delete(job);
return ResponseEntity.noContent().<Void>build();
})
.orElse(ResponseEntity.notFound().build());
}
}

View File

@ -0,0 +1,63 @@
package net.shyshkin.study.fullstack.supportportal.backend.controller;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Milestone;
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.MilestoneDTO;
import net.shyshkin.study.fullstack.supportportal.backend.service.MilestoneService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;
@RestController
@RequestMapping("/api/milestones")
@RequiredArgsConstructor
@CrossOrigin(origins = {"http://localhost:4200", "http://localhost:3000"})
public class MilestoneController {
private final MilestoneService milestoneService;
// Public endpoint - for user interface
@GetMapping("/public")
public ResponseEntity<List<Milestone>> getActiveMilestones() {
return ResponseEntity.ok(milestoneService.getActiveMilestones());
}
// Admin endpoints - NO SECURITY (matching EventController pattern)
@GetMapping
public ResponseEntity<List<Milestone>> getAllMilestones() {
return ResponseEntity.ok(milestoneService.getAllMilestones());
}
@GetMapping("/{id}")
public ResponseEntity<Milestone> getMilestoneById(@PathVariable Long id) {
return ResponseEntity.ok(milestoneService.getMilestoneById(id));
}
@PostMapping
public ResponseEntity<Milestone> createMilestone(@Valid @RequestBody MilestoneDTO milestoneDTO) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(milestoneService.createMilestone(milestoneDTO));
}
@PutMapping("/{id}")
public ResponseEntity<Milestone> updateMilestone(
@PathVariable Long id,
@Valid @RequestBody MilestoneDTO milestoneDTO) {
return ResponseEntity.ok(milestoneService.updateMilestone(id, milestoneDTO));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteMilestone(@PathVariable Long id) {
milestoneService.deleteMilestone(id);
return ResponseEntity.noContent().build();
}
// @PutMapping("/reorder")
// public ResponseEntity<Void> reorderMilestones(@RequestBody List<Long> orderedIds) {
// milestoneService.reorderMilestones(orderedIds);
// return ResponseEntity.ok().build();
// }
}

View File

@ -1,6 +1,5 @@
package net.shyshkin.study.fullstack.supportportal.backend.controller;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Post;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Professor;
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.PostDto;
@ -12,6 +11,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import lombok.extern.slf4j.Slf4j;
import com.amazonaws.services.secretsmanager.model.ResourceNotFoundException;
@ -21,21 +21,28 @@ import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
@Slf4j
@RestController
@RequestMapping("/api/posts")
@CrossOrigin(origins = "*", methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE, RequestMethod.OPTIONS})
public class PostController {
@Autowired
private PostRepository postRepository;
@Autowired
private ProfessorRepository professorRepository;
// Get all posts where isPosted is true
@GetMapping("/posted")
public ResponseEntity<List<Post>> getAllPostedPosts() {
try {
log.info("Fetching all posted posts");
List<Post> posts = postRepository.findAllByIsPostedTrue();
log.info("Retrieved {} posted posts", posts.size());
return ResponseEntity.ok(posts);
} catch (Exception e) {
log.error("Error fetching posted posts: ", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@ -44,13 +51,16 @@ public class PostController {
@GetMapping("/tags/count")
public ResponseEntity<Map<String, Long>> getTagsWithCount() {
try {
log.info("Fetching tag counts");
List<Object[]> tagCounts = postRepository.findTagsWithCount();
Map<String, Long> tagCountMap = new HashMap<>();
for (Object[] tagCount : tagCounts) {
tagCountMap.put((String) tagCount[0], (Long) tagCount[1]);
}
log.info("Retrieved {} unique tags", tagCountMap.size());
return ResponseEntity.ok(tagCountMap);
} catch (Exception e) {
log.error("Error fetching tag counts: ", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@ -59,37 +69,67 @@ public class PostController {
@GetMapping("/tag/{tag}")
public ResponseEntity<List<Post>> getPostsByTag(@PathVariable String tag) {
try {
log.info("Fetching posts by tag: {}", tag);
List<Post> posts = postRepository.findAllByTagAndIsPostedTrue(tag);
log.info("Retrieved {} posts for tag: {}", posts.size(), tag);
return ResponseEntity.ok(posts);
} catch (Exception e) {
log.error("Error fetching posts by tag {}: ", tag, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
// Get all posts
@GetMapping
public List<Post> getAllPosts() {
return postRepository.findAll();
public ResponseEntity<List<Post>> getAllPosts() {
try {
log.info("Fetching all posts");
List<Post> posts = postRepository.findAll();
log.info("Retrieved {} posts", posts.size());
return ResponseEntity.ok(posts);
} catch (Exception e) {
log.error("Error fetching all posts: ", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
// Get a single post by ID
@GetMapping("/{id}")
public ResponseEntity<Post> getPostById(@PathVariable Long id) {
try {
log.info("Fetching post with id: {}", id);
return postRepository.findById(id)
.map(post -> ResponseEntity.ok(post))
.orElse(ResponseEntity.notFound().build());
.map(post -> {
log.info("Found post with id: {}", id);
return ResponseEntity.ok(post);
})
.orElseGet(() -> {
log.warn("Post not found with id: {}", id);
return ResponseEntity.notFound().build();
});
} catch (Exception e) {
log.error("Error fetching post with id {}: ", id, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@Autowired
private ProfessorRepository professorRepository;
@PostMapping
public ResponseEntity<?> createPost(@RequestBody PostDto postDto) {
try {
// Debug logging to see what data is being received
log.info("Creating new post with data:");
log.info("Title: {}", postDto.getTitle());
log.info("Content: {}", postDto.getContent());
log.info("Posted: {}", postDto.isPosted());
log.info("ImageUrl: {}", postDto.getImageUrl());
log.info("Professors: {}", postDto.getProfessors());
log.info("Tags: {}", postDto.getTags());
Post post = new Post();
post.setTitle(postDto.getTitle());
post.setContent(postDto.getContent());
post.setPosted(postDto.isPosted());
post.setImageUrl(postDto.getImageUrl());
// Fetch professors from IDs, filter out null IDs
List<Long> validProfessorIds = postDto.getProfessors().stream()
@ -102,17 +142,33 @@ public class PostController {
post.setTags(postDto.getTags());
// Save the post
postRepository.save(post);
return ResponseEntity.status(HttpStatus.CREATED).build();
Post savedPost = postRepository.save(post);
log.info("Successfully created post with id: {}", savedPost.getId());
return ResponseEntity.status(HttpStatus.CREATED).body(savedPost);
} catch (Exception e) {
log.error("Error creating post: ", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Failed to create post: " + e.getMessage());
}
}
@PutMapping("/{id}")
public ResponseEntity<?> updatePost(@PathVariable Long id, @RequestBody PostDto postDto) {
Post post = postRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Post not found"));
try {
log.info("Updating post with id: {}", id);
log.info("New Title: {}", postDto.getTitle());
log.info("New Content length: {}", postDto.getContent() != null ? postDto.getContent().length() : 0);
log.info("New Posted status: {}", postDto.isPosted());
log.info("New ImageUrl: {}", postDto.getImageUrl());
Post post = postRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Post not found with id: " + id));
post.setTitle(postDto.getTitle());
post.setContent(postDto.getContent());
post.setPosted(postDto.isPosted());
post.setImageUrl(postDto.getImageUrl());
// Fetch professors from IDs, filter out null IDs
List<Long> validProfessorIds = postDto.getProfessors().stream()
@ -125,18 +181,47 @@ public class PostController {
post.setTags(postDto.getTags());
// Save the updated post
postRepository.save(post);
return ResponseEntity.ok().build();
Post updatedPost = postRepository.save(post);
log.info("Successfully updated post with id: {}", id);
return ResponseEntity.ok(updatedPost);
} catch (ResourceNotFoundException e) {
log.warn("Post not found for update: {}", e.getMessage());
return ResponseEntity.notFound().build();
} catch (Exception e) {
log.error("Error updating post with id {}: ", id, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Failed to update post: " + e.getMessage());
}
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deletePost(@PathVariable Long id) {
try {
log.info("Deleting post with id: {}", id);
return postRepository.findById(id)
.map(post -> {
postRepository.delete(post);
return ResponseEntity.noContent().<Void>build(); // Explicitly specify the type parameter
log.info("Successfully deleted post with id: {}", id);
return ResponseEntity.noContent().<Void>build();
})
.orElse(ResponseEntity.notFound().build());
.orElseGet(() -> {
log.warn("Post not found for deletion with id: {}", id);
return ResponseEntity.notFound().build();
});
} catch (Exception e) {
log.error("Error deleting post with id {}: ", id, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
// Handle preflight OPTIONS requests
@RequestMapping(method = RequestMethod.OPTIONS)
public ResponseEntity<?> handleOptions() {
return ResponseEntity.ok()
.header("Access-Control-Allow-Origin", "*")
.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
.header("Access-Control-Allow-Headers", "Content-Type, Authorization")
.build();
}
}

View File

@ -1,36 +0,0 @@
package net.shyshkin.study.fullstack.supportportal.backend.controller;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class PostDTO
{
@NotNull
private String title;
@NotNull
private String content;
@NotEmpty(message = "At least one professor must be selected.")
private List<Long> professors;
private List<String> tags;
private boolean posted;
// Getters and setters
// ...
}

View File

@ -0,0 +1,71 @@
package net.shyshkin.study.fullstack.supportportal.backend.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Professor;
import net.shyshkin.study.fullstack.supportportal.backend.domain.ProfessorCategory;
import net.shyshkin.study.fullstack.supportportal.backend.service.ProfessorService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.UUID;
@Slf4j
@RestController
@RequestMapping("public/professor")
@RequiredArgsConstructor
@CrossOrigin(origins = "*") // Configure this properly for production
public class PublicProfessorController {
private final ProfessorService professorService;
@GetMapping
public ResponseEntity<Page<Professor>> getAllProfessors(Pageable pageable) {
Page<Professor> professors = professorService.findAll(pageable);
return ResponseEntity.ok(professors);
}
@GetMapping("{professorId}")
public ResponseEntity<Professor> getProfessorById(@PathVariable UUID professorId) {
Professor professor = professorService.findByProfessorId(professorId);
return ResponseEntity.ok(professor);
}
@GetMapping("active")
public ResponseEntity<Page<Professor>> getActiveProfessors(Pageable pageable) {
Page<Professor> activeProfessors = professorService.findActiveProfessors(pageable);
return ResponseEntity.ok(activeProfessors);
}
// Add the missing endpoint that your Next.js frontend is calling
@GetMapping("active/category/{category}")
public ResponseEntity<Page<Professor>> getActiveProfessorsByCategory(
@PathVariable String category,
Pageable pageable) {
try {
ProfessorCategory professorCategory = ProfessorCategory.valueOf(category.toUpperCase());
Page<Professor> professors = professorService.findActiveProfessorsByCategory(professorCategory, pageable);
return ResponseEntity.ok(professors);
} catch (IllegalArgumentException e) {
log.warn("Invalid category provided: {}", category);
return ResponseEntity.badRequest().build();
}
}
// Additional endpoint for all professors by category (active and inactive)
@GetMapping("category/{category}")
public ResponseEntity<Page<Professor>> getProfessorsByCategory(
@PathVariable String category,
Pageable pageable) {
try {
ProfessorCategory professorCategory = ProfessorCategory.valueOf(category.toUpperCase());
Page<Professor> professors = professorService.findByCategory(professorCategory, pageable);
return ResponseEntity.ok(professors);
} catch (IllegalArgumentException e) {
log.warn("Invalid category provided: {}", category);
return ResponseEntity.badRequest().build();
}
}
}

View File

@ -0,0 +1,140 @@
package net.shyshkin.study.fullstack.supportportal.backend.resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.shyshkin.study.fullstack.supportportal.backend.domain.HttpResponse;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Publication;
import net.shyshkin.study.fullstack.supportportal.backend.service.PublicationService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/publications")
@RequiredArgsConstructor
@Slf4j
@CrossOrigin
public class PublicationResource {
private final PublicationService publicationService;
@PostMapping
@PreAuthorize("hasAnyAuthority('user:create')")
public ResponseEntity<Publication> addPublication(
@RequestParam String title,
@RequestParam String authors,
@RequestParam Integer year,
@RequestParam String journal,
@RequestParam(required = false) String doi,
@RequestParam(required = false) String category,
@RequestParam(required = false) String abstractText,
@RequestParam(required = false) String publicationDate,
@RequestParam(required = false) String keywords,
@RequestParam(required = false) Integer displayOrder) {
log.info("Adding new publication: {}", title);
Publication publication = publicationService.addPublication(
title, authors, year, journal, doi, category, abstractText, publicationDate, keywords, displayOrder
);
return ResponseEntity.status(HttpStatus.CREATED).body(publication);
}
@PutMapping("/{id}")
@PreAuthorize("hasAnyAuthority('user:update')")
public ResponseEntity<Publication> updatePublication(
@PathVariable Long id,
@RequestParam String title,
@RequestParam String authors,
@RequestParam Integer year,
@RequestParam String journal,
@RequestParam(required = false) String doi,
@RequestParam(required = false) String category,
@RequestParam(required = false) String abstractText,
@RequestParam(required = false) String publicationDate,
@RequestParam(required = false) String keywords,
@RequestParam(required = false) Integer displayOrder) {
log.info("Updating publication with id: {}", id);
Publication publication = publicationService.updatePublication(
id, title, authors, year, journal, doi, category, abstractText, publicationDate, keywords, displayOrder
);
return ResponseEntity.ok(publication);
}
@GetMapping("/active")
public ResponseEntity<List<Publication>> getActivePublications() {
log.info("Getting active publications");
List<Publication> publications = publicationService.getActivePublications();
return ResponseEntity.ok(publications);
}
@GetMapping("/{id}")
public ResponseEntity<Publication> getPublicationById(@PathVariable Long id) {
log.info("Getting publication with id: {}", id);
Publication publication = publicationService.getPublicationById(id);
return ResponseEntity.ok(publication);
}
@GetMapping
@PreAuthorize("hasAnyAuthority('user:read')")
public ResponseEntity<List<Publication>> getAllPublications() {
log.info("Getting all publications");
List<Publication> publications = publicationService.getAllPublications();
return ResponseEntity.ok(publications);
}
@GetMapping("/category/{category}")
public ResponseEntity<List<Publication>> getPublicationsByCategory(@PathVariable String category) {
log.info("Getting publications by category: {}", category);
List<Publication> publications = publicationService.getPublicationsByCategory(category);
return ResponseEntity.ok(publications);
}
@GetMapping("/year/{year}")
public ResponseEntity<List<Publication>> getPublicationsByYear(@PathVariable Integer year) {
log.info("Getting publications by year: {}", year);
List<Publication> publications = publicationService.getPublicationsByYear(year);
return ResponseEntity.ok(publications);
}
@DeleteMapping("/{id}")
@PreAuthorize("hasAnyAuthority('user:delete')")
public ResponseEntity<HttpResponse> deletePublication(@PathVariable Long id) {
log.info("Deleting publication with id: {}", id);
publicationService.deletePublication(id);
return ResponseEntity.ok(
HttpResponse.builder()
.httpStatusCode(HttpStatus.OK.value())
.httpStatus(HttpStatus.OK)
.reason(HttpStatus.OK.getReasonPhrase())
.message("Publication deleted successfully")
.build()
);
}
@PutMapping("/{id}/toggle-active")
@PreAuthorize("hasAnyAuthority('user:update')")
public ResponseEntity<Publication> toggleActiveStatus(@PathVariable Long id) {
log.info("Toggling active status for publication with id: {}", id);
Publication publication = publicationService.toggleActiveStatus(id);
return ResponseEntity.ok(publication);
}
@PutMapping("/reorder")
@PreAuthorize("hasAnyAuthority('user:update')")
public ResponseEntity<HttpResponse> reorderPublications(@RequestBody List<Long> orderedIds) {
log.info("Reordering publications");
publicationService.reorderPublications(orderedIds);
return ResponseEntity.ok(
HttpResponse.builder()
.httpStatusCode(HttpStatus.OK.value())
.httpStatus(HttpStatus.OK)
.reason(HttpStatus.OK.getReasonPhrase())
.message("Publications reordered successfully")
.build()
);
}
}

View File

@ -0,0 +1,111 @@
package net.shyshkin.study.fullstack.supportportal.backend.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.shyshkin.study.fullstack.supportportal.backend.domain.ServiceTile;
import net.shyshkin.study.fullstack.supportportal.backend.domain.HttpResponse;
import net.shyshkin.study.fullstack.supportportal.backend.enumeration.ServiceTileCategory;
import net.shyshkin.study.fullstack.supportportal.backend.service.ServiceTileService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/service-tiles")
@RequiredArgsConstructor
@Slf4j
@CrossOrigin
public class ServiceTileResource {
private final ServiceTileService serviceTileService;
@PostMapping
@PreAuthorize("hasAnyAuthority('user:create')")
public ResponseEntity<ServiceTile> addServiceTile(
@RequestParam String title,
@RequestParam(required = false) String description,
@RequestParam ServiceTileCategory category,
@RequestParam(required = false) Integer displayOrder) {
log.info("Adding new service tile with title: {} and category: {}", title, category);
ServiceTile serviceTile = serviceTileService.addServiceTile(title, description, category, displayOrder);
return ResponseEntity.status(HttpStatus.CREATED).body(serviceTile);
}
@PutMapping("/{id}")
@PreAuthorize("hasAnyAuthority('user:update')")
public ResponseEntity<ServiceTile> updateServiceTile(
@PathVariable Long id,
@RequestParam String title,
@RequestParam(required = false) String description,
@RequestParam ServiceTileCategory category,
@RequestParam(required = false) Integer displayOrder) {
log.info("Updating service tile with id: {}", id);
ServiceTile serviceTile = serviceTileService.updateServiceTile(id, title, description, category, displayOrder);
return ResponseEntity.ok(serviceTile);
}
@GetMapping("/active")
public ResponseEntity<List<ServiceTile>> getActiveServiceTiles() {
log.info("Getting active service tiles");
List<ServiceTile> serviceTiles = serviceTileService.getActiveServiceTiles();
return ResponseEntity.ok(serviceTiles);
}
@GetMapping("/{id}")
public ResponseEntity<ServiceTile> getServiceTileById(@PathVariable Long id) {
log.info("Getting service tile with id: {}", id);
ServiceTile serviceTile = serviceTileService.getServiceTileById(id);
return ResponseEntity.ok(serviceTile);
}
@GetMapping
@PreAuthorize("hasAnyAuthority('user:read')")
public ResponseEntity<List<ServiceTile>> getAllServiceTiles() {
log.info("Getting all service tiles");
List<ServiceTile> serviceTiles = serviceTileService.getAllServiceTiles();
return ResponseEntity.ok(serviceTiles);
}
@DeleteMapping("/{id}")
@PreAuthorize("hasAnyAuthority('user:delete')")
public ResponseEntity<HttpResponse> deleteServiceTile(@PathVariable Long id) {
log.info("Deleting service tile with id: {}", id);
serviceTileService.deleteServiceTile(id);
return ResponseEntity.ok(
HttpResponse.builder()
.httpStatusCode(HttpStatus.OK.value())
.httpStatus(HttpStatus.OK)
.reason(HttpStatus.OK.getReasonPhrase())
.message("Service tile deleted successfully")
.build()
);
}
@PutMapping("/{id}/toggle-active")
@PreAuthorize("hasAnyAuthority('user:update')")
public ResponseEntity<ServiceTile> toggleActiveStatus(@PathVariable Long id) {
log.info("Toggling active status for service tile with id: {}", id);
ServiceTile serviceTile = serviceTileService.toggleActiveStatus(id);
return ResponseEntity.ok(serviceTile);
}
@PutMapping("/reorder")
@PreAuthorize("hasAnyAuthority('user:update')")
public ResponseEntity<HttpResponse> reorderServiceTiles(@RequestBody List<Long> orderedIds) {
log.info("Reordering service tiles");
serviceTileService.reorderServiceTiles(orderedIds);
return ResponseEntity.ok(
HttpResponse.builder()
.httpStatusCode(HttpStatus.OK.value())
.httpStatus(HttpStatus.OK)
.reason(HttpStatus.OK.getReasonPhrase())
.message("Service tiles reordered successfully")
.build()
);
}
}

View File

@ -0,0 +1,31 @@
package com.shyshkin.study.fullstack.supportportal.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* Simple test controller to verify Spring Boot is working
* Test URL: http://localhost:8080/api/test
*/
@RestController
@RequestMapping("/api")
public class TestController {
@GetMapping("/test")
public Map<String, String> test() {
Map<String, String> response = new HashMap<>();
response.put("status", "success");
response.put("message", "Spring Boot is working!");
response.put("timestamp", String.valueOf(System.currentTimeMillis()));
return response;
}
@GetMapping("/health")
public String health() {
return "Application is healthy!";
}
}

View File

@ -0,0 +1,57 @@
package net.shyshkin.study.fullstack.supportportal.backend.controller;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Testimonial;
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.TestimonialDTO;
import net.shyshkin.study.fullstack.supportportal.backend.service.TestimonialService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;
@RestController
@RequestMapping("/api/testimonials")
@RequiredArgsConstructor
@CrossOrigin(origins = {"http://localhost:4200", "http://localhost:3000"})
public class TestimonialController {
private final TestimonialService testimonialService;
// Public endpoint - for user interface
@GetMapping("/public")
public ResponseEntity<List<Testimonial>> getActiveTestimonials() {
return ResponseEntity.ok(testimonialService.getActiveTestimonials());
}
// Admin endpoints - no security (matching your pattern)
@GetMapping
public ResponseEntity<List<Testimonial>> getAllTestimonials() {
return ResponseEntity.ok(testimonialService.getAllTestimonials());
}
@GetMapping("/{id}")
public ResponseEntity<Testimonial> getTestimonialById(@PathVariable Long id) {
return ResponseEntity.ok(testimonialService.getTestimonialById(id));
}
@PostMapping
public ResponseEntity<Testimonial> createTestimonial(@Valid @RequestBody TestimonialDTO testimonialDTO) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(testimonialService.createTestimonial(testimonialDTO));
}
@PutMapping("/{id}")
public ResponseEntity<Testimonial> updateTestimonial(
@PathVariable Long id,
@Valid @RequestBody TestimonialDTO testimonialDTO) {
return ResponseEntity.ok(testimonialService.updateTestimonial(id, testimonialDTO));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteTestimonial(@PathVariable Long id) {
testimonialService.deleteTestimonial(id);
return ResponseEntity.noContent().build();
}
}

View File

@ -0,0 +1,87 @@
// UpcomingEventController.java - REST Controller for Upcoming Events
package net.shyshkin.study.fullstack.supportportal.backend.controller;
import net.shyshkin.study.fullstack.supportportal.backend.domain.UpcomingEvent;
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.UpcomingEventDto;
import net.shyshkin.study.fullstack.supportportal.backend.repository.UpcomingEventRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import com.amazonaws.services.secretsmanager.model.ResourceNotFoundException;
import java.util.List;
@RestController
@RequestMapping("/api/upcoming-events")
@CrossOrigin(origins = "*")
public class UpcomingEventController {
@Autowired
private UpcomingEventRepository upcomingEventRepository;
// Get all active upcoming events (for public display)
@GetMapping("/active")
public ResponseEntity<List<UpcomingEvent>> getActiveUpcomingEvents() {
try {
List<UpcomingEvent> events = upcomingEventRepository.findAllByIsActiveTrue();
return ResponseEntity.ok(events);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
// Get all upcoming events (for admin)
@GetMapping
public List<UpcomingEvent> getAllUpcomingEvents() {
return upcomingEventRepository.findAll();
}
// Get a single upcoming event by ID
@GetMapping("/{id}")
public ResponseEntity<UpcomingEvent> getUpcomingEventById(@PathVariable Long id) {
return upcomingEventRepository.findById(id)
.map(event -> ResponseEntity.ok(event))
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<?> createUpcomingEvent(@RequestBody UpcomingEventDto eventDto) {
UpcomingEvent event = new UpcomingEvent();
event.setTitle(eventDto.getTitle());
event.setDescription(eventDto.getDescription());
event.setSchedule(eventDto.getSchedule());
event.setEventDate(eventDto.getEventDate());
event.setActive(eventDto.isActive());
upcomingEventRepository.save(event);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
@PutMapping("/{id}")
public ResponseEntity<?> updateUpcomingEvent(@PathVariable Long id, @RequestBody UpcomingEventDto eventDto) {
UpcomingEvent event = upcomingEventRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Upcoming event not found"));
event.setTitle(eventDto.getTitle());
event.setDescription(eventDto.getDescription());
event.setSchedule(eventDto.getSchedule());
event.setEventDate(eventDto.getEventDate());
event.setActive(eventDto.isActive());
upcomingEventRepository.save(event);
return ResponseEntity.ok().build();
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUpcomingEvent(@PathVariable Long id) {
return upcomingEventRepository.findById(id)
.map(event -> {
upcomingEventRepository.delete(event);
return ResponseEntity.noContent().<Void>build();
})
.orElse(ResponseEntity.notFound().build());
}
}

View File

@ -0,0 +1,63 @@
// Course.java - Entity for courses
package net.shyshkin.study.fullstack.supportportal.backend.domain;
import javax.persistence.*;
import java.time.LocalDate;
import java.util.List;
import lombok.*;
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
@EqualsAndHashCode(callSuper = true)
public class Course extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(columnDefinition = "TEXT", nullable = false)
private String description;
@Column(nullable = false)
private String duration;
@Column(nullable = false)
private Integer seats;
@Column(nullable = false)
private String category;
@Column(nullable = false)
private String level;
@Column(nullable = false)
private String instructor;
private String price;
@Column(name = "start_date")
private LocalDate startDate;
private String imageUrl;
@ElementCollection
@CollectionTable(name = "course_eligibility", joinColumns = @JoinColumn(name = "course_id"))
@Column(name = "eligibility")
private List<String> eligibility;
@ElementCollection
@CollectionTable(name = "course_objectives", joinColumns = @JoinColumn(name = "course_id"))
@Column(name = "objective", columnDefinition = "TEXT")
private List<String> objectives;
// FIXED: Changed from primitive boolean to Boolean wrapper class
@Column(name = "is_active", nullable = false)
private Boolean isActive = true;
}

View File

@ -0,0 +1,56 @@
// CourseApplication.java - Entity for course applications
package net.shyshkin.study.fullstack.supportportal.backend.domain;
import javax.persistence.*;
import lombok.*;
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
@EqualsAndHashCode(callSuper = true)
public class CourseApplication extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "course_id", nullable = false)
private Course course;
@Column(nullable = false)
private String fullName;
@Column(nullable = false)
private String email;
@Column(nullable = false)
private String phone;
@Column(name = "resume_url")
private String resumeUrl;
@Column(name = "resume_path")
private String resumePath;
@Column(nullable = false)
private String qualification;
private String experience;
@Column(columnDefinition = "TEXT")
private String coverLetter;
@Enumerated(EnumType.STRING)
private ApplicationStatus status = ApplicationStatus.PENDING;
public enum ApplicationStatus {
PENDING, REVIEWED, SHORTLISTED, ACCEPTED, REJECTED, ENROLLED
}
public String getResumePath() {
return resumePath != null ? resumePath : resumeUrl;
}
}

View File

@ -19,7 +19,7 @@ public class Event {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
@Column
private String code;
@Column(nullable = false)
@ -33,6 +33,18 @@ public class Event {
private String subTitle;
// New fields to match Next.js component
private String description;
private String detail;
private String mainImage;
@ElementCollection
@CollectionTable(name = "event_gallery_images", joinColumns = @JoinColumn(name = "event_id"))
@Column(name = "image_url")
private List<String> galleryImages;
@Column(nullable = false)
private String date;
@ -48,9 +60,10 @@ public class Event {
@CollectionTable(name = "organisers", joinColumns = @JoinColumn(name = "event_id"))
private List<String> organisers;
// @ElementCollection
// @CollectionTable(name = "fees", joinColumns = @JoinColumn(name = "event_id"))
// private List<Fee> fee;
// Fixed Fee mapping with proper column name
@ElementCollection
@CollectionTable(name = "event_fees", joinColumns = @JoinColumn(name = "event_id"))
private List<Fee> fee;
@Column(nullable = false)
private String phone;
@ -61,6 +74,17 @@ public class Event {
@Column(nullable = false)
private Boolean isActive;
@Column(name = "is_deleted", nullable = false)
private Boolean isDeleted = false;
// Registration/Booking Link
@Column(name = "book_seat_link", length = 500)
private String bookSeatLink;
// Additional Information Link
@Column(name = "learn_more_link", length = 500)
private String learnMoreLink;
@ManyToMany
@JoinTable(
name = "event_professors",
@ -69,18 +93,59 @@ public class Event {
)
private List<Professor> professors;
// Assuming you have these classes defined as well
// Embedded classes
@Embeddable
public static class Venue {
private String title;
private String date;
private String address;
private String info;
// Constructors, getters, setters
public Venue() {}
public Venue(String title, String date, String address, String info) {
this.title = title;
this.date = date;
this.address = address;
this.info = info;
}
// Getters and setters
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getDate() { return date; }
public void setDate(String date) { this.date = date; }
public String getAddress() { return address; }
public void setAddress(String address) { this.address = address; }
public String getInfo() { return info; }
public void setInfo(String info) { this.info = info; }
}
@Embeddable
public static class Fee {
private String desc;
@Column(name = "fee_description") // Explicit column mapping to avoid reserved word issues
private String description;
@Column(name = "fee_cost")
private Integer cost;
// Constructors, getters, setters
public Fee() {}
public Fee(String description, Integer cost) {
this.description = description;
this.cost = cost;
}
// Getters and setters
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public Integer getCost() { return cost; }
public void setCost(Integer cost) { this.cost = cost; }
}
}

View File

@ -0,0 +1,47 @@
package net.shyshkin.study.fullstack.supportportal.backend.domain;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "hero_images")
public class HeroImage implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false, updatable = false)
private Long id;
private String title;
private String subtitle;
private String description;
private String imageUrl;
private String imageFilename;
@JsonProperty("isActive")
@Column(name = "is_active")
private boolean isActive;
private Date uploadDate;
private Date lastModified;
@PrePersist
protected void onCreate() {
uploadDate = new Date();
lastModified = new Date();
}
@PreUpdate
protected void onUpdate() {
lastModified = new Date();
}
}

View File

@ -0,0 +1,56 @@
// Job.java - Entity for job positions
package net.shyshkin.study.fullstack.supportportal.backend.domain;
import javax.persistence.*;
import java.util.List;
import lombok.*;
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
@EqualsAndHashCode(callSuper = true)
public class Job extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(nullable = false)
private String department;
@Column(nullable = false)
private String location;
@Column(nullable = false)
private String type; // Full-time, Contract, Observership, etc.
@Column(nullable = false)
private String experience;
@Column(nullable = false)
private String salary;
@Column(columnDefinition = "TEXT", nullable = false)
private String description;
@ElementCollection
@CollectionTable(name = "job_requirements", joinColumns = @JoinColumn(name = "job_id"))
@Column(name = "requirement")
private List<String> requirements;
@ElementCollection
@CollectionTable(name = "job_responsibilities", joinColumns = @JoinColumn(name = "job_id"))
@Column(name = "responsibility")
private List<String> responsibilities;
// CRITICAL FIX: Changed from primitive boolean to Boolean wrapper class
// This ensures proper handling of the value from JSON and prevents default false
@Column(name = "is_active", nullable = false)
private Boolean isActive = true;
}

View File

@ -0,0 +1,47 @@
// JobApplication.java - Entity for job applications
package net.shyshkin.study.fullstack.supportportal.backend.domain;
import javax.persistence.*;
import lombok.*;
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
@EqualsAndHashCode(callSuper = true)
public class JobApplication extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "job_id", nullable = false)
private Job job;
@Column(nullable = false)
private String fullName;
@Column(nullable = false)
private String email;
@Column(nullable = false)
private String phone;
@Column(nullable = false)
private String experience;
@Column(columnDefinition = "TEXT")
private String coverLetter;
private String resumeUrl; // Path to uploaded resume file
@Enumerated(EnumType.STRING)
private ApplicationStatus status = ApplicationStatus.PENDING;
public enum ApplicationStatus {
PENDING, REVIEWED, SHORTLISTED, INTERVIEWED, REJECTED, HIRED
}
}

View File

@ -0,0 +1,58 @@
package net.shyshkin.study.fullstack.supportportal.backend.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
@Entity
@Table(name = "milestones")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Milestone implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(nullable = false, length = 500)
private String description;
// @Column(nullable = false)
// private Integer displayOrder;
@Column(nullable = false)
private Boolean isActive = true;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "UTC")
@Temporal(TemporalType.DATE)
private Date milestoneDate;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "UTC")
@Column(nullable = false, updatable = false)
private Date createdAt;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "UTC")
private Date updatedAt;
@PrePersist
protected void onCreate() {
createdAt = new Date();
updatedAt = new Date();
}
@PreUpdate
protected void onUpdate() {
updatedAt = new Date();
}
}

View File

@ -10,7 +10,7 @@ import lombok.*;
@NoArgsConstructor
@AllArgsConstructor
@ToString
@EqualsAndHashCode(callSuper = true)
@EqualsAndHashCode(callSuper = false) // Changed from true since you're extending BaseEntity
public class Post extends BaseEntity {
@Id
@ -38,4 +38,7 @@ public class Post extends BaseEntity {
@CollectionTable(name = "post_tags", joinColumns = @JoinColumn(name = "post_id"))
@Column(name = "tag")
private List<String> tags;
@Column
private String imageUrl; // Add this field
}

View File

@ -1,18 +1,18 @@
package net.shyshkin.study.fullstack.supportportal.backend.domain;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.*;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;
import org.hibernate.annotations.Type;
import javax.persistence.*;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;
import java.util.UUID;
@Entity
@Data
@NoArgsConstructor
@ -25,8 +25,6 @@ public class Professor implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
// @EqualsAndHashCode.Include
// @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private Long id;
@Type(type = "org.hibernate.type.UUIDCharType")
@ -43,11 +41,49 @@ public class Professor implements Serializable {
private String profileImageUrl;
@Enumerated(EnumType.STRING)
private WorkingStatus status; // Use enum to track detailed working status
private WorkingStatus status;
@Enumerated(EnumType.STRING)
private ProfessorCategory category;
// Additional fields for Next.js integration
private String phone;
private String specialty;
@Column(columnDefinition = "TEXT")
private String certification;
@Column(columnDefinition = "TEXT")
private String training;
private String experience;
@Column(columnDefinition = "TEXT")
private String description;
private String designation;
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "professor_work_days", joinColumns = @JoinColumn(name = "professor_id"))
@Column(name = "work_day")
private List<String> workDays;
// ✅ CRITICAL FIX: Added orphanRemoval = true
// This tells JPA to DELETE skills that are removed from the collection
@OneToMany(mappedBy = "professor", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
private Set<ProfessorSkill> skills;
// ✅ CRITICAL FIX: Added orphanRemoval = true
// This tells JPA to DELETE awards that are removed from the collection
@OneToMany(mappedBy = "professor", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
private Set<ProfessorAward> awards;
@ManyToMany(mappedBy = "professors")
@JsonIgnore
private List<Post> posts;
// Convenience method to get full name
public String getName() {
return firstName + " " + lastName;
}
}

View File

@ -0,0 +1,33 @@
package net.shyshkin.study.fullstack.supportportal.backend.domain;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.*;
import javax.persistence.*;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ProfessorAward {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String year;
@Column(columnDefinition = "TEXT")
private String description;
private String imageUrl;
@ManyToOne
@JoinColumn(name = "professor_id")
@JsonIgnore
@ToString.Exclude
@EqualsAndHashCode.Exclude
private Professor professor;
}

View File

@ -0,0 +1,7 @@
package net.shyshkin.study.fullstack.supportportal.backend.domain;
public enum ProfessorCategory {
FACULTY,
SUPPORT_TEAM,
TRAINEE_FELLOW
}

View File

@ -0,0 +1,28 @@
package net.shyshkin.study.fullstack.supportportal.backend.domain;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.*;
import javax.persistence.*;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ProfessorSkill {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Integer level;
@ManyToOne
@JoinColumn(name = "professor_id")
@JsonIgnore
@ToString.Exclude
@EqualsAndHashCode.Exclude
private Professor professor;
}

View File

@ -0,0 +1,74 @@
package net.shyshkin.study.fullstack.supportportal.backend.domain;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "publications")
public class Publication implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false, updatable = false)
private Long id;
@Column(columnDefinition = "TEXT", nullable = false)
private String title;
@Column(columnDefinition = "TEXT")
private String authors; // Stored as comma-separated string
@Column(nullable = false)
private Integer year;
@Column(columnDefinition = "TEXT", nullable = false)
private String journal;
@Column(columnDefinition = "TEXT")
private String doi;
@Column(length = 100)
private String category;
@Column(columnDefinition = "LONGTEXT")
private String abstractText;
@Column(length = 100)
private String publicationDate;
@Column(columnDefinition = "TEXT")
private String keywords; // Stored as comma-separated string
@JsonProperty("isActive")
@Column(name = "is_active")
private boolean isActive;
@Column(name = "display_order")
private Integer displayOrder;
@Temporal(TemporalType.TIMESTAMP)
private Date createdDate;
@Temporal(TemporalType.TIMESTAMP)
private Date lastModified;
@PrePersist
protected void onCreate() {
createdDate = new Date();
lastModified = new Date();
}
@PreUpdate
protected void onUpdate() {
lastModified = new Date();
}
}

View File

@ -0,0 +1,55 @@
package net.shyshkin.study.fullstack.supportportal.backend.domain;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import net.shyshkin.study.fullstack.supportportal.backend.enumeration.ServiceTileCategory;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "service_tiles")
public class ServiceTile implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false, updatable = false)
private Long id;
@Column(nullable = false)
private String title;
@Column(length = 1000)
private String description;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private ServiceTileCategory category;
@JsonProperty("isActive")
@Column(name = "is_active")
private boolean isActive;
@Column(name = "display_order")
private Integer displayOrder;
private Date createdDate;
private Date lastModified;
@PrePersist
protected void onCreate() {
createdDate = new Date();
lastModified = new Date();
}
@PreUpdate
protected void onUpdate() {
lastModified = new Date();
}
}

View File

@ -0,0 +1,63 @@
package net.shyshkin.study.fullstack.supportportal.backend.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "testimonials")
public class Testimonial {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String name;
@Column(nullable = false)
private Integer age;
@Column(nullable = false, length = 200)
private String title;
@Column(nullable = false, columnDefinition = "TEXT")
private String story;
@Column(nullable = false, columnDefinition = "TEXT")
private String outcome;
@Column(nullable = false, columnDefinition = "TEXT")
private String impact;
@Column(nullable = false, length = 100)
private String category;
@Column(name = "is_active")
private Boolean isActive = true;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}

View File

@ -0,0 +1,35 @@
// UpcomingEvent.java - Entity for upcoming events
package net.shyshkin.study.fullstack.supportportal.backend.domain;
import javax.persistence.*;
import java.time.LocalDate;
import lombok.*;
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
@EqualsAndHashCode(callSuper = true)
public class UpcomingEvent extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(columnDefinition = "TEXT", nullable = false)
private String description;
@Column(nullable = false)
private String schedule; // e.g., "Q3 2025", "Monthly Sessions", "Ongoing"
@Column(name = "event_date")
private LocalDate eventDate;
@Column(name = "is_active", nullable = false)
private boolean isActive = true;
}

View File

@ -46,4 +46,5 @@ public class User implements Serializable {
private boolean isActive;
private boolean isNotLocked;
}

View File

@ -0,0 +1,17 @@
package net.shyshkin.study.fullstack.supportportal.backend.domain.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AwardDto {
private String title;
private String year;
private String description;
private String imageUrl;
}

View File

@ -0,0 +1,35 @@
// CourseApplicationDto.java - DTO for Course Application
package net.shyshkin.study.fullstack.supportportal.backend.domain.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Email;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class CourseApplicationDto {
@NotNull
private Long courseId;
@NotNull
private String fullName;
@NotNull
@Email
private String email;
@NotNull
private String phone;
@NotNull
private String qualification;
private String experience;
private String coverLetter;
private String resumeUrl;
}

View File

@ -0,0 +1,49 @@
// CourseDto.java - DTO for Course
package net.shyshkin.study.fullstack.supportportal.backend.domain.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotNull;
import java.time.LocalDate;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class CourseDto {
@NotNull
private String title;
@NotNull
private String description;
@NotNull
private String duration;
@NotNull
private Integer seats;
@NotNull
private String category;
@NotNull
private String level;
@NotNull
private String instructor;
private String price;
private LocalDate startDate;
private String imageUrl;
private List<String> eligibility;
private List<String> objectives;
// FIXED: Changed from primitive boolean to Boolean wrapper
// Changed field name from "active" to "isActive"
// Removed all manual getter/setter methods
// Lombok @Data will generate: getIsActive() and setIsActive()
private Boolean isActive;
}

View File

@ -0,0 +1,34 @@
// JobApplicationDto.java - DTO for Job Application
package net.shyshkin.study.fullstack.supportportal.backend.domain.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Email;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class JobApplicationDto {
@NotNull
private Long jobId;
@NotNull
private String fullName;
@NotNull
@Email
private String email;
@NotNull
private String phone;
@NotNull
private String experience;
private String coverLetter;
private String resumeUrl;
}

View File

@ -0,0 +1,41 @@
// JobDto.java - DTO for Job
package net.shyshkin.study.fullstack.supportportal.backend.domain.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotNull;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class JobDto {
@NotNull
private String title;
@NotNull
private String department;
@NotNull
private String location;
@NotNull
private String type;
@NotNull
private String experience;
@NotNull
private String salary;
@NotNull
private String description;
private List<String> requirements;
private List<String> responsibilities;
private Boolean isActive;
}

View File

@ -0,0 +1,33 @@
package net.shyshkin.study.fullstack.supportportal.backend.domain.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.Date;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MilestoneDTO {
private Long id;
@NotBlank(message = "Title is required")
private String title;
@NotBlank(message = "Description is required")
private String description;
// @NotNull(message = "Display order is required")
// private Integer displayOrder;
@NotNull(message = "Active status is required")
private Boolean isActive;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
private Date milestoneDate;
}

View File

@ -1,23 +1,17 @@
package net.shyshkin.study.fullstack.supportportal.backend.domain.dto;
import java.util.List;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class PostDto {
@NotNull
@ -31,7 +25,5 @@ public class PostDto {
private List<String> tags;
private boolean posted;
// Getters and setters
// ...
private String imageUrl; // Add this field
}

View File

@ -4,12 +4,16 @@ import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Role;
import net.shyshkin.study.fullstack.supportportal.backend.domain.WorkingStatus;
import net.shyshkin.study.fullstack.supportportal.backend.domain.ProfessorCategory;
// Add these imports at the top
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.SkillDto;
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.AwardDto;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDateTime;
import java.util.List;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
@ -29,9 +33,19 @@ public class ProfessorDto {
private String position;
private String officeLocation;
private WorkingStatus status;
private ProfessorCategory category;
private LocalDateTime joinDate;
private MultipartFile profileImage; // Optional field for profile image URL
private MultipartFile profileImage;
// Additional fields for Next.js integration
private String phone;
private String specialty;
private String certification;
private String training;
private String experience;
private String description;
private String designation;
private List<String> workDays;
private List<SkillDto> skills;
private List<AwardDto> awards;
}

View File

@ -0,0 +1,50 @@
package net.shyshkin.study.fullstack.supportportal.backend.domain.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import net.shyshkin.study.fullstack.supportportal.backend.domain.WorkingStatus;
import net.shyshkin.study.fullstack.supportportal.backend.domain.ProfessorCategory;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ProfessorResponseDto {
private Long id;
private UUID professorId;
private String firstName;
private String lastName;
private String email;
private String department;
private String position;
private String officeLocation;
private LocalDateTime joinDate;
private String profileImageUrl;
private WorkingStatus status;
private ProfessorCategory category;
// Additional fields for Next.js integration
private String phone;
private String specialty;
private String certification;
private String training;
private String experience;
private String description;
private String designation;
private List<String> workDays;
// Nested DTOs for skills and awards
private List<SkillDto> skills;
private List<AwardDto> awards;
// Convenience method to get full name
public String getName() {
return firstName + " " + lastName;
}
}

View File

@ -0,0 +1,15 @@
package net.shyshkin.study.fullstack.supportportal.backend.domain.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SkillDto {
private String name;
private Integer level;
}

View File

@ -0,0 +1,47 @@
package net.shyshkin.study.fullstack.supportportal.backend.domain.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Min;
import javax.validation.constraints.Max;
import javax.validation.constraints.Size;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class TestimonialDTO {
@NotBlank(message = "Name is required")
@Size(max = 100, message = "Name must not exceed 100 characters")
private String name;
@NotNull(message = "Age is required")
@Min(value = 1, message = "Age must be at least 1")
@Max(value = 120, message = "Age must not exceed 120")
private Integer age;
@NotBlank(message = "Title is required")
@Size(max = 200, message = "Title must not exceed 200 characters")
private String title;
@NotBlank(message = "Story is required")
private String story;
@NotBlank(message = "Outcome is required")
private String outcome;
@NotBlank(message = "Impact is required")
private String impact;
@NotBlank(message = "Category is required")
@Size(max = 100, message = "Category must not exceed 100 characters")
private String category;
private Boolean isActive;
}

View File

@ -0,0 +1,39 @@
// UpcomingEventDto.java - DTO for Upcoming Event
package net.shyshkin.study.fullstack.supportportal.backend.domain.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotNull;
import java.time.LocalDate;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UpcomingEventDto {
@NotNull
private String title;
@NotNull
private String description;
@NotNull
private String schedule;
private LocalDate eventDate;
private boolean active;
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
public void setIsActive(boolean active) {
this.active = active;
}
}

View File

@ -0,0 +1,16 @@
package net.shyshkin.study.fullstack.supportportal.backend.enumeration;
public enum ServiceTileCategory {
TRAUMA_CARE("Trauma Care"),
ACUTE_CARE_SURGERY("Acute Care Surgery");
private final String displayName;
ServiceTileCategory(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
}

View File

@ -0,0 +1,21 @@
package net.shyshkin.study.fullstack.supportportal.backend.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.persistence.EntityNotFoundException;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<Map<String, String>> handleEntityNotFound(EntityNotFoundException ex) {
Map<String, String> error = new HashMap<>();
error.put("error", ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}

View File

@ -0,0 +1,8 @@
package net.shyshkin.study.fullstack.supportportal.backend.exception.domain;
public class HeroImageNotFoundException extends RuntimeException {
public HeroImageNotFoundException(String message) {
super(message);
}
}

View File

@ -0,0 +1,12 @@
package net.shyshkin.study.fullstack.supportportal.backend.exception.domain;
public class MilestoneNotFoundException extends RuntimeException {
public MilestoneNotFoundException(String message) {
super(message);
}
public MilestoneNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,8 @@
package net.shyshkin.study.fullstack.supportportal.backend.exception.domain;
public class PublicationNotFoundException extends RuntimeException {
public PublicationNotFoundException(String message) {
super(message);
}
}

View File

@ -0,0 +1,8 @@
package net.shyshkin.study.fullstack.supportportal.backend.exception.domain;
public class ServiceTileNotFoundException extends RuntimeException {
public ServiceTileNotFoundException(String message) {
super(message);
}
}

View File

@ -0,0 +1,12 @@
package net.shyshkin.study.fullstack.supportportal.backend.exception.domain;
public class TestimonialNotFoundException extends RuntimeException {
public TestimonialNotFoundException(String message) {
super(message);
}
public TestimonialNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -2,21 +2,27 @@ package net.shyshkin.study.fullstack.supportportal.backend.mapper;
import net.shyshkin.study.fullstack.supportportal.backend.domain.User;
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.UserDto;
import org.mapstruct.AfterMapping;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import java.time.LocalDateTime;
@Mapper(componentModel = "spring") // This is crucial for Spring integration
// @Mapper(componentModel = "spring",imports = {LocalDateTime.class})
@Mapper(componentModel = "spring")
public interface UserMapper {
@Mapping(target = "isNotLocked", source = "notLocked")
@Mapping(target = "isActive", source = "active")
@Mapping(target = "joinDate", expression = "java( LocalDateTime.now() )")
@Mapping(target = "joinDate", ignore = true)
@Mapping(target = "role", source = "role", resultType = String.class)
@Mapping(target = "authorities", source = "role.authorities")
User toEntity(UserDto userDto);
@AfterMapping
default void setJoinDate(@MappingTarget User user) {
if (user.getJoinDate() == null) {
user.setJoinDate(LocalDateTime.now());
}
}
}

View File

@ -0,0 +1,15 @@
// CourseApplicationRepository.java
package net.shyshkin.study.fullstack.supportportal.backend.repository;
import net.shyshkin.study.fullstack.supportportal.backend.domain.CourseApplication;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface CourseApplicationRepository extends JpaRepository<CourseApplication, Long> {
List<CourseApplication> findAllByCourseId(Long courseId);
List<CourseApplication> findAllByStatus(CourseApplication.ApplicationStatus status);
List<CourseApplication> findAllByEmail(String email);
}

View File

@ -0,0 +1,21 @@
// CourseRepository.java - Add this method to your existing repository
package net.shyshkin.study.fullstack.supportportal.backend.repository;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Course;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface CourseRepository extends JpaRepository<Course, Long> {
// Get all active courses
List<Course> findAllByIsActiveTrue();
// Get all past/inactive courses - ADD THIS METHOD
List<Course> findAllByIsActiveFalse();
List<Course> findAllByCategory(String category);
List<Course> findAllByLevel(String level);
}

View File

@ -1,11 +1,40 @@
// EventRepository.java - FIXED
package net.shyshkin.study.fullstack.supportportal.backend.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Event;
import java.util.List;
@Repository
public interface EventRepository extends JpaRepository<Event, Long> {
// Custom query methods can be added here if needed
// Find active events ordered by date ascending (for upcoming events)
List<Event> findByIsActiveTrueOrderByDateAsc();
// FIXED: Find INACTIVE events ordered by date descending (for past events)
List<Event> findByIsActiveFalseOrderByDateDesc();
// Find events by year
List<Event> findByYearAndIsActiveTrue(String year);
// Find events by subject
List<Event> findBySubjectContainingIgnoreCaseAndIsActiveTrue(String subject);
// Find events by title containing keyword
List<Event> findByTitleContainingIgnoreCaseAndIsActiveTrue(String title);
// Custom query to search events by multiple fields
@Query("SELECT e FROM Event e WHERE e.isActive = true AND " +
"(LOWER(e.title) LIKE LOWER(CONCAT('%', ?1, '%')) OR " +
"LOWER(e.description) LIKE LOWER(CONCAT('%', ?1, '%')) OR " +
"LOWER(e.subject) LIKE LOWER(CONCAT('%', ?1, '%')))")
List<Event> searchActiveEvents(String searchTerm);
// Find events by date range (you might need to adjust based on your date format)
@Query("SELECT e FROM Event e WHERE e.isActive = true AND e.date BETWEEN ?1 AND ?2 ORDER BY e.date ASC")
List<Event> findEventsByDateRange(String startDate, String endDate);
}

View File

@ -0,0 +1,17 @@
package net.shyshkin.study.fullstack.supportportal.backend.repository;
import net.shyshkin.study.fullstack.supportportal.backend.domain.HeroImage;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface HeroImageRepository extends JpaRepository<HeroImage, Long> {
@Query("SELECT h FROM HeroImage h WHERE h.isActive = true")
Optional<HeroImage> findByIsActiveTrue();
Optional<HeroImage> findByImageFilename(String imageFilename);
}

View File

@ -0,0 +1,15 @@
// JobApplicationRepository.java
package net.shyshkin.study.fullstack.supportportal.backend.repository;
import net.shyshkin.study.fullstack.supportportal.backend.domain.JobApplication;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface JobApplicationRepository extends JpaRepository<JobApplication, Long> {
List<JobApplication> findAllByJobId(Long jobId);
List<JobApplication> findAllByStatus(JobApplication.ApplicationStatus status);
List<JobApplication> findAllByEmail(String email);
}

View File

@ -0,0 +1,15 @@
// JobRepository.java
package net.shyshkin.study.fullstack.supportportal.backend.repository;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Job;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface JobRepository extends JpaRepository<Job, Long> {
List<Job> findAllByIsActiveTrue();
List<Job> findAllByDepartment(String department);
List<Job> findAllByType(String type);
}

View File

@ -0,0 +1,16 @@
package net.shyshkin.study.fullstack.supportportal.backend.repository;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Milestone;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface MilestoneRepository extends JpaRepository<Milestone, Long> {
// List<Milestone> findAllByIsActiveTrueOrderByDisplayOrderAsc();
// List<Milestone> findAllByOrderByDisplayOrderAsc();
List<Milestone> findAllByIsActiveTrue();
}

View File

@ -1,17 +1,18 @@
package net.shyshkin.study.fullstack.supportportal.backend.repository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
// import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import java.util.Optional;
import java.util.UUID;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Professor;
import net.shyshkin.study.fullstack.supportportal.backend.domain.WorkingStatus;
import net.shyshkin.study.fullstack.supportportal.backend.domain.ProfessorCategory;
// @RepositoryRestResource(collectionResourceRel = "professors", path = "professors")
public interface ProfessorRepository extends JpaRepository<Professor, Long> {
@Query("SELECT p FROM Professor p WHERE p.email = :email")
@ -24,4 +25,15 @@ public interface ProfessorRepository extends JpaRepository<Professor, Long> {
@Query("SELECT p FROM Professor p WHERE p.professorId = :professorId")
Optional<Professor> findByProfessorId(@Param("professorId") UUID professorId);
@Query("SELECT p FROM Professor p WHERE p.status = :status")
Page<Professor> findByStatus(@Param("status") WorkingStatus status, Pageable pageable);
@Query("SELECT p FROM Professor p WHERE p.category = :category")
Page<Professor> findByCategory(@Param("category") ProfessorCategory category, Pageable pageable);
@Query("SELECT p FROM Professor p WHERE p.status = :status AND p.category = :category")
Page<Professor> findByStatusAndCategory(@Param("status") WorkingStatus status,
@Param("category") ProfessorCategory category,
Pageable pageable);
}

View File

@ -0,0 +1,22 @@
package net.shyshkin.study.fullstack.supportportal.backend.repository;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Publication;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface PublicationRepository extends JpaRepository<Publication, Long> {
@Query("SELECT p FROM Publication p WHERE p.isActive = true ORDER BY p.year DESC, p.displayOrder ASC, p.id DESC")
List<Publication> findByIsActiveTrueOrderByYearDesc();
@Query("SELECT p FROM Publication p ORDER BY p.year DESC, p.displayOrder ASC, p.id DESC")
List<Publication> findAllOrderByYearDesc();
List<Publication> findByCategory(String category);
List<Publication> findByYear(Integer year);
}

View File

@ -0,0 +1,15 @@
package net.shyshkin.study.fullstack.supportportal.backend.repository;
import net.shyshkin.study.fullstack.supportportal.backend.domain.ServiceTile;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ServiceTileRepository extends JpaRepository<ServiceTile, Long> {
@Query("SELECT s FROM ServiceTile s WHERE s.isActive = true ORDER BY s.displayOrder ASC, s.id ASC")
List<ServiceTile> findByIsActiveTrueOrderByDisplayOrder();
}

View File

@ -0,0 +1,17 @@
package net.shyshkin.study.fullstack.supportportal.backend.repository;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Testimonial;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface TestimonialRepository extends JpaRepository<Testimonial, Long> {
// Get all active testimonials
List<Testimonial> findAllByIsActiveTrue();
// Get testimonials by category
List<Testimonial> findAllByIsActiveTrueAndCategory(String category);
}

View File

@ -0,0 +1,13 @@
// UpcomingEventRepository.java
package net.shyshkin.study.fullstack.supportportal.backend.repository;
import net.shyshkin.study.fullstack.supportportal.backend.domain.UpcomingEvent;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface UpcomingEventRepository extends JpaRepository<UpcomingEvent, Long> {
List<UpcomingEvent> findAllByIsActiveTrue();
}

View File

@ -0,0 +1,24 @@
package net.shyshkin.study.fullstack.supportportal.backend.service;
import net.shyshkin.study.fullstack.supportportal.backend.domain.HeroImage;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.List;
public interface HeroImageService {
HeroImage addHeroImage(String title, String subtitle, String description, MultipartFile image) throws IOException;
HeroImage updateHeroImage(Long id, String title, String subtitle, String description, MultipartFile image) throws IOException;
HeroImage getActiveHeroImage();
HeroImage getHeroImageById(Long id);
List<HeroImage> getAllHeroImages();
void deleteHeroImage(Long id);
HeroImage setActiveHeroImage(Long id);
}

View File

@ -0,0 +1,23 @@
package net.shyshkin.study.fullstack.supportportal.backend.service;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Milestone;
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.MilestoneDTO;
import java.util.List;
public interface MilestoneService {
List<Milestone> getAllMilestones();
List<Milestone> getActiveMilestones();
Milestone getMilestoneById(Long id);
Milestone createMilestone(MilestoneDTO milestoneDTO);
Milestone updateMilestone(Long id, MilestoneDTO milestoneDTO);
void deleteMilestone(Long id);
// void reorderMilestones(List<Long> orderedIds);
}

View File

@ -1,6 +1,7 @@
package net.shyshkin.study.fullstack.supportportal.backend.service;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Professor;
import net.shyshkin.study.fullstack.supportportal.backend.domain.ProfessorCategory;
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.ProfessorDto;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
@ -29,4 +30,15 @@ public interface ProfessorService {
byte[] getImageByProfessorId(UUID professorId, String filename);
byte[] getDefaultProfileImage(UUID professorId);
// Existing method for active professors
Page<Professor> findActiveProfessors(Pageable pageable);
// New methods for category-based filtering
Page<Professor> findByCategory(ProfessorCategory category, Pageable pageable);
Page<Professor> findActiveProfessorsByCategory(ProfessorCategory category, Pageable pageable);
// Method to find professor with details
Professor findProfessorWithDetailsById(UUID professorId);
}

View File

@ -3,7 +3,11 @@ package net.shyshkin.study.fullstack.supportportal.backend.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Professor;
import net.shyshkin.study.fullstack.supportportal.backend.domain.ProfessorAward;
import net.shyshkin.study.fullstack.supportportal.backend.domain.ProfessorCategory;
import net.shyshkin.study.fullstack.supportportal.backend.domain.ProfessorSkill;
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.ProfessorDto;
import net.shyshkin.study.fullstack.supportportal.backend.domain.WorkingStatus;
import net.shyshkin.study.fullstack.supportportal.backend.exception.domain.EmailExistsException;
import net.shyshkin.study.fullstack.supportportal.backend.exception.domain.NotAnImageFileException;
import net.shyshkin.study.fullstack.supportportal.backend.exception.domain.ProfessorNotFoundException;
@ -25,14 +29,14 @@ import javax.annotation.PostConstruct;
import javax.transaction.Transactional;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import static net.shyshkin.study.fullstack.supportportal.backend.constant.FileConstant.*;
import static org.springframework.http.MediaType.*;
@Slf4j
@ -51,10 +55,8 @@ public class ProfessorServiceImpl implements ProfessorService {
private final ProfileImageService profileImageService;
private final RestTemplateBuilder restTemplateBuilder;
private RestTemplate restTemplate;
@PostConstruct
void init() {
restTemplate = restTemplateBuilder
@ -78,14 +80,14 @@ public class ProfessorServiceImpl implements ProfessorService {
private String generateDefaultProfileImageUrl(UUID professorId) {
return ServletUriComponentsBuilder.fromCurrentContextPath()
.path(String.format(DEFAULT_USER_IMAGE_URI_PATTERN, professorId))
.path(String.format(DEFAULT_PROFESSOR_IMAGE_URI_PATTERN, professorId))
.toUriString();
}
private String generateProfileImageUrl(UUID professorId) {
return ServletUriComponentsBuilder.fromCurrentContextPath()
.path(String.format(DEFAULT_USER_IMAGE_URI_PATTERN, professorId))
.pathSegment(USER_IMAGE_FILENAME)
.path(String.format(DEFAULT_PROFESSOR_IMAGE_URI_PATTERN, professorId))
.pathSegment(PROFESSOR_IMAGE_FILENAME)
.toUriString();
}
@ -108,7 +110,26 @@ public class ProfessorServiceImpl implements ProfessorService {
.orElseThrow(() -> new ProfessorNotFoundException(PROFESSOR_NOT_FOUND_MSG));
}
@Override
public Page<Professor> findActiveProfessors(Pageable pageable) {
return professorRepository.findByStatus(WorkingStatus.ACTIVE, pageable);
}
@Override
public Page<Professor> findByCategory(ProfessorCategory category, Pageable pageable) {
return professorRepository.findByCategory(category, pageable);
}
@Override
public Page<Professor> findActiveProfessorsByCategory(ProfessorCategory category, Pageable pageable) {
return professorRepository.findByStatusAndCategory(WorkingStatus.ACTIVE, category, pageable);
}
@Override
public Professor findProfessorWithDetailsById(UUID professorId) {
return professorRepository.findByProfessorId(professorId)
.orElseThrow(() -> new ProfessorNotFoundException(PROFESSOR_NOT_FOUND_MSG));
}
private void saveProfileImage(Professor professor, MultipartFile profileImage) {
if (profileImage == null) return;
@ -117,7 +138,7 @@ public class ProfessorServiceImpl implements ProfessorService {
throw new NotAnImageFileException(profileImage.getOriginalFilename() + " is not an image file. Please upload an image");
}
String imageUrl = profileImageService.persistProfileImage(professor.getProfessorId(), profileImage, USER_IMAGE_FILENAME);
String imageUrl = profileImageService.persistProfileImage(professor.getProfessorId(), profileImage, PROFESSOR_IMAGE_FILENAME);
if (imageUrl == null)
imageUrl = generateProfileImageUrl(professor.getProfessorId());
@ -135,33 +156,68 @@ public class ProfessorServiceImpl implements ProfessorService {
}
@Override
@Transactional
public Professor addNewProfessor(ProfessorDto professorDto) {
validateNewEmail(professorDto.getEmail());
Professor professor = professorMapper.toEntity(professorDto);
// Set a unique identifier for the professor
professor.setProfessorId(generateUuid());
// professor.setProfessorId(UUID.randomUUID()); // Set a unique identifier for the professor
professor.setJoinDate(LocalDateTime.now()); // Set join date if not provided
professor.setJoinDate(LocalDateTime.now());
professor.setProfileImageUrl(generateDefaultProfileImageUrl(professor.getProfessorId()));
professorRepository.save(professor);
// Save the professor first to get the ID
Professor savedProfessor = professorRepository.save(professor);
// saveProfileImage(professor, professorDto.getProfileImage());
// Handle skills if provided
if (professorDto.getSkills() != null && !professorDto.getSkills().isEmpty()) {
Set<ProfessorSkill> skills = professorDto.getSkills().stream()
.filter(skillDto -> skillDto.getName() != null && !skillDto.getName().trim().isEmpty())
.map(skillDto -> ProfessorSkill.builder()
.name(skillDto.getName().trim())
.level(skillDto.getLevel())
.professor(savedProfessor)
.build())
.collect(Collectors.toSet());
savedProfessor.setSkills(skills);
}
return professor;
// Handle awards if provided
if (professorDto.getAwards() != null && !professorDto.getAwards().isEmpty()) {
Set<ProfessorAward> awards = professorDto.getAwards().stream()
.filter(awardDto -> awardDto.getTitle() != null && !awardDto.getTitle().trim().isEmpty())
.map(awardDto -> ProfessorAward.builder()
.title(awardDto.getTitle().trim())
.year(awardDto.getYear())
.description(awardDto.getDescription())
.imageUrl(awardDto.getImageUrl())
.professor(savedProfessor)
.build())
.collect(Collectors.toSet());
savedProfessor.setAwards(awards);
}
// Save again to persist the relationships
Professor finalProfessor = professorRepository.save(savedProfessor);
// Handle profile image if provided
if (professorDto.getProfileImage() != null) {
saveProfileImage(finalProfessor, professorDto.getProfileImage());
}
return finalProfessor;
}
@Override
@Transactional
public Professor updateProfessor(UUID professorId, ProfessorDto professorDto) {
Professor professor = professorRepository.findByProfessorId(professorId)
.orElseThrow(() -> new RuntimeException("Professor not found with id: " + professorId));
validateUpdateEmail(professorId,professorDto.getEmail());
validateUpdateEmail(professorId, professorDto.getEmail());
// Update basic fields
professor.setFirstName(professorDto.getFirstName());
professor.setLastName(professorDto.getLastName());
professor.setEmail(professorDto.getEmail());
@ -169,13 +225,81 @@ public class ProfessorServiceImpl implements ProfessorService {
professor.setPosition(professorDto.getPosition());
professor.setOfficeLocation(professorDto.getOfficeLocation());
professor.setStatus(professorDto.getStatus());
professor.setJoinDate(professorDto.getJoinDate()); // Update join date if provided
professor.setCategory(professorDto.getCategory());
professorRepository.save(professor);
// Update extended fields
professor.setPhone(professorDto.getPhone());
professor.setSpecialty(professorDto.getSpecialty());
professor.setCertification(professorDto.getCertification());
professor.setTraining(professorDto.getTraining());
professor.setExperience(professorDto.getExperience());
professor.setDescription(professorDto.getDescription());
professor.setDesignation(professorDto.getDesignation());
professor.setWorkDays(professorDto.getWorkDays());
saveProfileImage(professor, professorDto.getProfileImage());
if (professorDto.getJoinDate() != null) {
professor.setJoinDate(professorDto.getJoinDate());
}
return professor;
// Create a final reference for lambda expressions
final Professor professorRef = professor;
// ✅ CORRECT FIX: Update skills with orphanRemoval=true
// Initialize collection if null
if (professor.getSkills() == null) {
professor.setSkills(new HashSet<>());
}
// Clear existing skills (orphanRemoval will delete them from DB)
professor.getSkills().clear();
// Add new skills
if (professorDto.getSkills() != null && !professorDto.getSkills().isEmpty()) {
Set<ProfessorSkill> newSkills = professorDto.getSkills().stream()
.filter(skillDto -> skillDto.getName() != null && !skillDto.getName().trim().isEmpty())
.map(skillDto -> ProfessorSkill.builder()
.name(skillDto.getName().trim())
.level(skillDto.getLevel())
.professor(professorRef)
.build())
.collect(Collectors.toSet());
professor.getSkills().addAll(newSkills);
}
// ✅ CORRECT FIX: Update awards with orphanRemoval=true
// Initialize collection if null
if (professor.getAwards() == null) {
professor.setAwards(new HashSet<>());
}
// Clear existing awards (orphanRemoval will delete them from DB)
professor.getAwards().clear();
// Add new awards
if (professorDto.getAwards() != null && !professorDto.getAwards().isEmpty()) {
Set<ProfessorAward> newAwards = professorDto.getAwards().stream()
.filter(awardDto -> awardDto.getTitle() != null && !awardDto.getTitle().trim().isEmpty())
.map(awardDto -> ProfessorAward.builder()
.title(awardDto.getTitle().trim())
.year(awardDto.getYear())
.description(awardDto.getDescription())
.imageUrl(awardDto.getImageUrl())
.professor(professorRef)
.build())
.collect(Collectors.toSet());
professor.getAwards().addAll(newAwards);
}
Professor savedProfessor = professorRepository.save(professor);
// Handle profile image if provided
if (professorDto.getProfileImage() != null) {
saveProfileImage(savedProfessor, professorDto.getProfileImage());
}
return savedProfessor;
}
@Override

View File

@ -0,0 +1,33 @@
package net.shyshkin.study.fullstack.supportportal.backend.service;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Publication;
import java.util.List;
public interface PublicationService {
Publication addPublication(String title, String authors, Integer year, String journal,
String doi, String category, String abstractText,
String publicationDate, String keywords, Integer displayOrder);
Publication updatePublication(Long id, String title, String authors, Integer year,
String journal, String doi, String category,
String abstractText, String publicationDate,
String keywords, Integer displayOrder);
List<Publication> getActivePublications();
Publication getPublicationById(Long id);
List<Publication> getAllPublications();
List<Publication> getPublicationsByCategory(String category);
List<Publication> getPublicationsByYear(Integer year);
void deletePublication(Long id);
Publication toggleActiveStatus(Long id);
void reorderPublications(List<Long> orderedIds);
}

View File

@ -0,0 +1,25 @@
package net.shyshkin.study.fullstack.supportportal.backend.service;
import net.shyshkin.study.fullstack.supportportal.backend.domain.ServiceTile;
import net.shyshkin.study.fullstack.supportportal.backend.enumeration.ServiceTileCategory;
import java.util.List;
public interface ServiceTileService {
ServiceTile addServiceTile(String title, String description, ServiceTileCategory category, Integer displayOrder);
ServiceTile updateServiceTile(Long id, String title, String description, ServiceTileCategory category, Integer displayOrder);
List<ServiceTile> getActiveServiceTiles();
ServiceTile getServiceTileById(Long id);
List<ServiceTile> getAllServiceTiles();
void deleteServiceTile(Long id);
ServiceTile toggleActiveStatus(Long id);
void reorderServiceTiles(List<Long> orderedIds);
}

View File

@ -0,0 +1,21 @@
package net.shyshkin.study.fullstack.supportportal.backend.service;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Testimonial;
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.TestimonialDTO;
import java.util.List;
public interface TestimonialService {
List<Testimonial> getAllTestimonials();
List<Testimonial> getActiveTestimonials();
Testimonial getTestimonialById(Long id);
Testimonial createTestimonial(TestimonialDTO testimonialDTO);
Testimonial updateTestimonial(Long id, TestimonialDTO testimonialDTO);
void deleteTestimonial(Long id);
}

View File

@ -0,0 +1,179 @@
package net.shyshkin.study.fullstack.supportportal.backend.service.impl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.shyshkin.study.fullstack.supportportal.backend.domain.HeroImage;
import net.shyshkin.study.fullstack.supportportal.backend.exception.domain.HeroImageNotFoundException;
import net.shyshkin.study.fullstack.supportportal.backend.repository.HeroImageRepository;
import net.shyshkin.study.fullstack.supportportal.backend.service.HeroImageService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import static net.shyshkin.study.fullstack.supportportal.backend.constant.FileConstant.*;
import static org.springframework.http.MediaType.*;
@Service
@Slf4j
@RequiredArgsConstructor
@Transactional
public class HeroImageServiceImpl implements HeroImageService {
private final HeroImageRepository heroImageRepository;
@Override
public HeroImage addHeroImage(String title, String subtitle, String description, MultipartFile image) throws IOException {
log.info("Adding new hero image with title: {}", title);
HeroImage heroImage = new HeroImage();
heroImage.setTitle(title);
heroImage.setSubtitle(subtitle);
heroImage.setDescription(description);
heroImage.setActive(false); // New images are inactive by default
if (image != null && !image.isEmpty()) {
String filename = saveHeroImage(image);
heroImage.setImageFilename(filename);
heroImage.setImageUrl(getHeroImageUrl(filename));
}
return heroImageRepository.save(heroImage);
}
@Override
public HeroImage updateHeroImage(Long id, String title, String subtitle, String description, MultipartFile image) throws IOException {
log.info("Updating hero image with id: {}", id);
HeroImage heroImage = getHeroImageById(id);
heroImage.setTitle(title);
heroImage.setSubtitle(subtitle);
heroImage.setDescription(description);
if (image != null && !image.isEmpty()) {
// Delete old image if exists
if (heroImage.getImageFilename() != null) {
deleteHeroImageFile(heroImage.getImageFilename());
}
String filename = saveHeroImage(image);
heroImage.setImageFilename(filename);
heroImage.setImageUrl(getHeroImageUrl(filename));
}
return heroImageRepository.save(heroImage);
}
@Override
@Transactional(readOnly = true)
public HeroImage getActiveHeroImage() {
return heroImageRepository.findByIsActiveTrue()
.orElseThrow(() -> new HeroImageNotFoundException("No active hero image found"));
}
@Override
@Transactional(readOnly = true)
public HeroImage getHeroImageById(Long id) {
return heroImageRepository.findById(id)
.orElseThrow(() -> new HeroImageNotFoundException("Hero image not found with id: " + id));
}
@Override
@Transactional(readOnly = true)
public List<HeroImage> getAllHeroImages() {
return heroImageRepository.findAll();
}
@Override
public void deleteHeroImage(Long id) {
log.info("Deleting hero image with id: {}", id);
HeroImage heroImage = getHeroImageById(id);
if (heroImage.isActive()) {
throw new IllegalStateException("Cannot delete active hero image. Please set another image as active first.");
}
if (heroImage.getImageFilename() != null) {
deleteHeroImageFile(heroImage.getImageFilename());
}
heroImageRepository.deleteById(id);
}
@Override
public HeroImage setActiveHeroImage(Long id) {
log.info("Setting hero image as active with id: {}", id);
// Deactivate current active image
heroImageRepository.findByIsActiveTrue().ifPresent(currentActive -> {
currentActive.setActive(false);
heroImageRepository.save(currentActive);
});
// Activate new image
HeroImage heroImage = getHeroImageById(id);
heroImage.setActive(true);
return heroImageRepository.save(heroImage);
}
private String saveHeroImage(MultipartFile image) throws IOException {
if (!isImageFile(image)) {
throw new IOException(image.getOriginalFilename() + NOT_AN_IMAGE_FILE);
}
Path heroFolder = Paths.get(HERO_FOLDER).toAbsolutePath().normalize();
if (!Files.exists(heroFolder)) {
Files.createDirectories(heroFolder);
log.info(DIRECTORY_CREATED + heroFolder);
}
String filename = HERO_IMAGE_PREFIX + System.currentTimeMillis() + DOT + getFileExtension(image);
Path targetLocation = heroFolder.resolve(filename);
Files.copy(image.getInputStream(), targetLocation, REPLACE_EXISTING);
log.info(FILE_SAVED_IN_FILE_SYSTEM + filename);
return filename;
}
private void deleteHeroImageFile(String filename) {
try {
Path filePath = Paths.get(HERO_FOLDER).resolve(filename);
Files.deleteIfExists(filePath);
log.info("Deleted hero image file: {}", filename);
} catch (IOException e) {
log.error("Error deleting hero image file: {}", filename, e);
}
}
private String getHeroImageUrl(String filename) {
return ServletUriComponentsBuilder.fromCurrentContextPath()
.path(HERO_IMAGE_PATH + filename)
.toUriString();
}
private boolean isImageFile(MultipartFile file) {
String contentType = file.getContentType();
return contentType != null && (
contentType.equals(IMAGE_JPEG_VALUE) ||
contentType.equals(IMAGE_PNG_VALUE) ||
contentType.equals(IMAGE_GIF_VALUE)
);
}
private String getFileExtension(MultipartFile file) {
String originalFilename = file.getOriginalFilename();
if (originalFilename != null && originalFilename.contains(DOT)) {
return originalFilename.substring(originalFilename.lastIndexOf(DOT) + 1);
}
return JPG_EXTENSION;
}
}

View File

@ -0,0 +1,141 @@
package net.shyshkin.study.fullstack.supportportal.backend.service.impl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Milestone;
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.MilestoneDTO;
import net.shyshkin.study.fullstack.supportportal.backend.exception.domain.MilestoneNotFoundException;
import net.shyshkin.study.fullstack.supportportal.backend.repository.MilestoneRepository;
import net.shyshkin.study.fullstack.supportportal.backend.service.MilestoneService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Slf4j
public class MilestoneServiceImpl implements MilestoneService {
private final MilestoneRepository milestoneRepository;
@Override
public List<Milestone> getAllMilestones() {
log.info("Fetching all milestones");
List<Milestone> milestones = milestoneRepository.findAll();
return sortMilestonesByMonthYear(milestones);
}
@Override
public List<Milestone> getActiveMilestones() {
log.info("Fetching active milestones");
List<Milestone> milestones = milestoneRepository.findAllByIsActiveTrue();
return sortMilestonesByMonthYear(milestones);
}
@Override
public Milestone getMilestoneById(Long id) {
log.info("Fetching milestone with id: {}", id);
return milestoneRepository.findById(id)
.orElseThrow(() -> new MilestoneNotFoundException("Milestone not found with id: " + id));
}
@Override
@Transactional
public Milestone createMilestone(MilestoneDTO milestoneDTO) {
log.info("Creating new milestone: {}", milestoneDTO.getTitle());
Milestone milestone = Milestone.builder()
.title(milestoneDTO.getTitle())
.description(milestoneDTO.getDescription())
.isActive(milestoneDTO.getIsActive() != null ? milestoneDTO.getIsActive() : true)
.milestoneDate(milestoneDTO.getMilestoneDate())
.build();
Milestone savedMilestone = milestoneRepository.save(milestone);
log.info("Milestone created successfully with id: {}", savedMilestone.getId());
return savedMilestone;
}
@Override
@Transactional
public Milestone updateMilestone(Long id, MilestoneDTO milestoneDTO) {
log.info("Updating milestone with id: {}", id);
Milestone milestone = getMilestoneById(id);
milestone.setTitle(milestoneDTO.getTitle());
milestone.setDescription(milestoneDTO.getDescription());
milestone.setIsActive(milestoneDTO.getIsActive());
milestone.setMilestoneDate(milestoneDTO.getMilestoneDate());
Milestone updatedMilestone = milestoneRepository.save(milestone);
log.info("Milestone updated successfully");
return updatedMilestone;
}
@Override
@Transactional
public void deleteMilestone(Long id) {
log.info("Deleting milestone with id: {}", id);
Milestone milestone = getMilestoneById(id);
milestoneRepository.delete(milestone);
log.info("Milestone deleted successfully");
}
// REMOVE reorderMilestones method
/**
* Sort milestones by year and month extracted from title
* Most recent first (descending order)
*/
private List<Milestone> sortMilestonesByMonthYear(List<Milestone> milestones) {
return milestones.stream()
.sorted(Comparator.comparing(this::extractYearMonth).reversed())
.collect(Collectors.toList());
}
/**
* Extract YearMonth from milestone title
* Supports formats like:
* - "July 2025"
* - "2025"
* - "December 2023"
*/
private YearMonth extractYearMonth(Milestone milestone) {
String title = milestone.getTitle();
try {
// Try parsing "Month Year" format (e.g., "July 2025", "December 2023")
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMMM yyyy", Locale.ENGLISH);
return YearMonth.parse(title, formatter);
} catch (DateTimeParseException e1) {
try {
// Try parsing "Month Year" with short month (e.g., "Jul 2025")
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMM yyyy", Locale.ENGLISH);
return YearMonth.parse(title, formatter);
} catch (DateTimeParseException e2) {
try {
// Try parsing just year (e.g., "2025", "2020")
int year = Integer.parseInt(title.trim());
return YearMonth.of(year, 1); // Default to January
} catch (NumberFormatException e3) {
// If all parsing fails, default to a very old date
log.warn("Could not parse date from title: {}. Using default date.", title);
return YearMonth.of(1900, 1);
}
}
}
}
}

View File

@ -0,0 +1,129 @@
package net.shyshkin.study.fullstack.supportportal.backend.service.impl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Publication;
import net.shyshkin.study.fullstack.supportportal.backend.exception.domain.PublicationNotFoundException;
import net.shyshkin.study.fullstack.supportportal.backend.repository.PublicationRepository;
import net.shyshkin.study.fullstack.supportportal.backend.service.PublicationService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Slf4j
@RequiredArgsConstructor
@Transactional
public class PublicationServiceImpl implements PublicationService {
private final PublicationRepository publicationRepository;
@Override
public Publication addPublication(String title, String authors, Integer year, String journal,
String doi, String category, String abstractText,
String publicationDate, String keywords, Integer displayOrder) {
log.info("Adding new publication: {}", title);
Publication publication = new Publication();
publication.setTitle(title);
publication.setAuthors(authors);
publication.setYear(year);
publication.setJournal(journal);
publication.setDoi(doi);
publication.setCategory(category);
publication.setAbstractText(abstractText);
publication.setPublicationDate(publicationDate);
publication.setKeywords(keywords);
publication.setDisplayOrder(displayOrder != null ? displayOrder : 0);
publication.setActive(true);
return publicationRepository.save(publication);
}
@Override
public Publication updatePublication(Long id, String title, String authors, Integer year,
String journal, String doi, String category,
String abstractText, String publicationDate,
String keywords, Integer displayOrder) {
log.info("Updating publication with id: {}", id);
Publication publication = getPublicationById(id);
publication.setTitle(title);
publication.setAuthors(authors);
publication.setYear(year);
publication.setJournal(journal);
publication.setDoi(doi);
publication.setCategory(category);
publication.setAbstractText(abstractText);
publication.setPublicationDate(publicationDate);
publication.setKeywords(keywords);
if (displayOrder != null) {
publication.setDisplayOrder(displayOrder);
}
return publicationRepository.save(publication);
}
@Override
@Transactional(readOnly = true)
public List<Publication> getActivePublications() {
return publicationRepository.findByIsActiveTrueOrderByYearDesc();
}
@Override
@Transactional(readOnly = true)
public Publication getPublicationById(Long id) {
return publicationRepository.findById(id)
.orElseThrow(() -> new PublicationNotFoundException("Publication not found with id: " + id));
}
@Override
@Transactional(readOnly = true)
public List<Publication> getAllPublications() {
return publicationRepository.findAllOrderByYearDesc();
}
@Override
@Transactional(readOnly = true)
public List<Publication> getPublicationsByCategory(String category) {
return publicationRepository.findByCategory(category);
}
@Override
@Transactional(readOnly = true)
public List<Publication> getPublicationsByYear(Integer year) {
return publicationRepository.findByYear(year);
}
@Override
public void deletePublication(Long id) {
log.info("Deleting publication with id: {}", id);
Publication publication = getPublicationById(id);
publicationRepository.delete(publication);
}
@Override
public Publication toggleActiveStatus(Long id) {
log.info("Toggling active status for publication with id: {}", id);
Publication publication = getPublicationById(id);
publication.setActive(!publication.isActive());
return publicationRepository.save(publication);
}
@Override
public void reorderPublications(List<Long> orderedIds) {
log.info("Reordering {} publications", orderedIds.size());
for (int i = 0; i < orderedIds.size(); i++) {
Long id = orderedIds.get(i);
Publication publication = getPublicationById(id);
publication.setDisplayOrder(i);
publicationRepository.save(publication);
}
}
}

View File

@ -0,0 +1,101 @@
package net.shyshkin.study.fullstack.supportportal.backend.service.impl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.shyshkin.study.fullstack.supportportal.backend.domain.ServiceTile;
import net.shyshkin.study.fullstack.supportportal.backend.enumeration.ServiceTileCategory;
import net.shyshkin.study.fullstack.supportportal.backend.exception.domain.ServiceTileNotFoundException;
import net.shyshkin.study.fullstack.supportportal.backend.repository.ServiceTileRepository;
import net.shyshkin.study.fullstack.supportportal.backend.service.ServiceTileService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Slf4j
@RequiredArgsConstructor
@Transactional
public class ServiceTileServiceImpl implements ServiceTileService {
private final ServiceTileRepository serviceTileRepository;
@Override
public ServiceTile addServiceTile(String title, String description, ServiceTileCategory category, Integer displayOrder) {
log.info("Adding new service tile with title: {} and category: {}", title, category);
ServiceTile serviceTile = new ServiceTile();
serviceTile.setTitle(title);
serviceTile.setDescription(description);
serviceTile.setCategory(category);
serviceTile.setDisplayOrder(displayOrder != null ? displayOrder : 0);
serviceTile.setActive(true);
return serviceTileRepository.save(serviceTile);
}
@Override
public ServiceTile updateServiceTile(Long id, String title, String description, ServiceTileCategory category, Integer displayOrder) {
log.info("Updating service tile with id: {}", id);
ServiceTile serviceTile = getServiceTileById(id);
serviceTile.setTitle(title);
serviceTile.setDescription(description);
serviceTile.setCategory(category);
if (displayOrder != null) {
serviceTile.setDisplayOrder(displayOrder);
}
return serviceTileRepository.save(serviceTile);
}
@Override
@Transactional(readOnly = true)
public List<ServiceTile> getActiveServiceTiles() {
return serviceTileRepository.findByIsActiveTrueOrderByDisplayOrder();
}
@Override
@Transactional(readOnly = true)
public ServiceTile getServiceTileById(Long id) {
return serviceTileRepository.findById(id)
.orElseThrow(() -> new ServiceTileNotFoundException("Service tile not found with id: " + id));
}
@Override
@Transactional(readOnly = true)
public List<ServiceTile> getAllServiceTiles() {
return serviceTileRepository.findAll();
}
@Override
public void deleteServiceTile(Long id) {
log.info("Deleting service tile with id: {}", id);
ServiceTile serviceTile = getServiceTileById(id);
serviceTileRepository.delete(serviceTile);
}
@Override
public ServiceTile toggleActiveStatus(Long id) {
log.info("Toggling active status for service tile with id: {}", id);
ServiceTile serviceTile = getServiceTileById(id);
serviceTile.setActive(!serviceTile.isActive());
return serviceTileRepository.save(serviceTile);
}
@Override
public void reorderServiceTiles(List<Long> orderedIds) {
log.info("Reordering {} service tiles", orderedIds.size());
for (int i = 0; i < orderedIds.size(); i++) {
Long id = orderedIds.get(i);
ServiceTile serviceTile = getServiceTileById(id);
serviceTile.setDisplayOrder(i);
serviceTileRepository.save(serviceTile);
}
}
}

View File

@ -0,0 +1,95 @@
package net.shyshkin.study.fullstack.supportportal.backend.service.impl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.shyshkin.study.fullstack.supportportal.backend.domain.Testimonial;
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.TestimonialDTO;
import net.shyshkin.study.fullstack.supportportal.backend.exception.domain.TestimonialNotFoundException;
import net.shyshkin.study.fullstack.supportportal.backend.repository.TestimonialRepository;
import net.shyshkin.study.fullstack.supportportal.backend.service.TestimonialService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@RequiredArgsConstructor
@Slf4j
public class TestimonialServiceImpl implements TestimonialService {
private final TestimonialRepository testimonialRepository;
@Override
public List<Testimonial> getAllTestimonials() {
log.info("Fetching all testimonials");
return testimonialRepository.findAll();
}
@Override
public List<Testimonial> getActiveTestimonials() {
log.info("Fetching active testimonials");
return testimonialRepository.findAllByIsActiveTrue();
}
@Override
public Testimonial getTestimonialById(Long id) {
log.info("Fetching testimonial with id: {}", id);
return testimonialRepository.findById(id)
.orElseThrow(() -> new TestimonialNotFoundException("Testimonial not found with id: " + id));
}
@Override
@Transactional
public Testimonial createTestimonial(TestimonialDTO testimonialDTO) {
log.info("Creating new testimonial for: {}", testimonialDTO.getName());
Testimonial testimonial = Testimonial.builder()
.name(testimonialDTO.getName())
.age(testimonialDTO.getAge())
.title(testimonialDTO.getTitle())
.story(testimonialDTO.getStory())
.outcome(testimonialDTO.getOutcome())
.impact(testimonialDTO.getImpact())
.category(testimonialDTO.getCategory())
.isActive(testimonialDTO.getIsActive() != null ? testimonialDTO.getIsActive() : true)
.build();
Testimonial savedTestimonial = testimonialRepository.save(testimonial);
log.info("Testimonial created successfully with id: {}", savedTestimonial.getId());
return savedTestimonial;
}
@Override
@Transactional
public Testimonial updateTestimonial(Long id, TestimonialDTO testimonialDTO) {
log.info("Updating testimonial with id: {}", id);
Testimonial testimonial = getTestimonialById(id);
testimonial.setName(testimonialDTO.getName());
testimonial.setAge(testimonialDTO.getAge());
testimonial.setTitle(testimonialDTO.getTitle());
testimonial.setStory(testimonialDTO.getStory());
testimonial.setOutcome(testimonialDTO.getOutcome());
testimonial.setImpact(testimonialDTO.getImpact());
testimonial.setCategory(testimonialDTO.getCategory());
testimonial.setIsActive(testimonialDTO.getIsActive());
Testimonial updatedTestimonial = testimonialRepository.save(testimonial);
log.info("Testimonial updated successfully");
return updatedTestimonial;
}
@Override
@Transactional
public void deleteTestimonial(Long id) {
log.info("Deleting testimonial with id: {}", id);
Testimonial testimonial = getTestimonialById(id);
testimonialRepository.delete(testimonial);
log.info("Testimonial deleted successfully");
}
}

View File

@ -0,0 +1,14 @@
spring:
datasource:
url: jdbc:mysql://localhost:3306/support_portal?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
username: root
password: root
file:
upload:
directory: ${user.home}/support-portal-uploads
app:
base-url: http://localhost:8080
cors:
allowed-origins: http://localhost:4200,http://localhost:3000

View File

@ -1,10 +1,6 @@
server:
error:
path: /error
# whitelabel:
# enabled: false
spring:
mail:
@ -22,22 +18,20 @@ spring:
enable: true
ssl:
enable: false
datasource:
# url: jdbc:mysql://mysql:3306/demo
url: jdbc:mysql://210.18.189.94:8098/demo
# url: jdbc:mysql://${MYSQL_HOST:db}:8098/demo
username: youruser
password: youruserpassword
# url: ${SPRING_DATASOURCE_URL}
# username: ${SPRING_DATASOURCE_USERNAME}
# password: ${SPRING_DATASOURCE_PASSWORD}
url: jdbc:mysql://db:3306/support-portal?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
username: support_portal_user
password: support_portal_password
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL8Dialect
servlet:
multipart:
max-file-size: 10MB
@ -47,139 +41,42 @@ spring:
throw-exception-if-no-handler-found: true
web:
resources:
add-mappings: false
add-mappings: true
# File upload configuration
file:
upload:
directory: /app/uploads
app:
public-urls: /user/login,/user/register,/user/*/profile-image/**,/professors,/professors/**,/api/posts,/api/posts/*,/professor,/professor/*,/api/events,/api/events/*
base-url: ${APP_BASE_URL:http://localhost:8080}
# Fixed public URLs with correct wildcard patterns
public-urls: /user/login,/user/register,/user/*/profile-image,/user/*/profile-image/**,/professors,/professors/**,/api/posts,/api/posts/*,/api/posts/posted,/api/posts/tag/*,/api/posts/tags/count,/api/files/**,/uploads/**,/professor/**,/api/events,/api/events/*,/api/public/**,/api/jobs/active,/api/job-applications/**,/api/job-applications/resume/**,/api/courses/active,/api/courses/*,/api/course-applications,/api/upcoming-events/active,/api/milestones,/api/milestones/**,/api/testimonials,/api/testimonials/**,/hero/image/**,/hero/active/**,/hero/**,/service-tiles/active,/service-tiles/active/**,/publications/active/**,/publications/*/**,/publications/category/**,/publications/year/**
cors:
allowed-origins: http://localhost:4200,https://localhost:4200,http://art-support-portal.s3-website.eu-north-1.amazonaws.com,http://portal.shyshkin.net,*
allowed-origins: http://localhost:4200,http://localhost:3000,https://maincmc.rootxwire.com,https://dashboard.cmctrauma.com,https://www.dashboard.cmctrauma.com,https://cmctrauma.com,https://www.cmctrauma.com,https://cmcbackend.rootxwire.com,https://cmcadminfrontend.rootxwire.com
jwt:
secret: custom_text
# secret: ${random.value} #Does not work - every time generates new value
# jasypt:
# encryptor:
# # password: ${JASYPT_PASSWORD}
# password: custom_text
# algorithm: PBEWITHHMACSHA512ANDAES_256
# iv-generator-classname: org.jasypt.iv.RandomIvGenerator
---
# Production file upload configuration
spring:
config:
activate:
on-profile: production
file:
upload:
directory: /var/uploads/blog-images
app:
base-url: https://cmcbackend.rootxwire.com
cors:
allowed-origins: https://maincmc.rootxwire.com,https://dashboard.cmctrauma.com,https://www.dashboard.cmctrauma.com,https://cmctrauma.com,https://www.cmctrauma.com,https://cmcbackend.rootxwire.com,https://cmcadminfrontend.rootxwire.com
---
# spring:
# config:
# activate:
# on-profile: local
# datasource:
# url: jdbc:mysql://210.18.189.94:8098/demo
# username: youruser
# password: youruserpassword
# jpa:
# show-sql: true
# logging:
# level:
# net.shyshkin: debug
---
# spring:
# config:
# activate:
# on-profile: aws-local
# datasource:
# url: jdbc:mysql://210.18.189.94:8098/demo
# username: youruser
# password: youruserpassword
# mail:
# host: email-smtp.eu-north-1.amazonaws.com
# port: 587
# username: AKIAVW7XGDOWFHHCELIH
# password: BJyWOWS1xWYR35MRCFn3BuuQ6vY+k7DRsdAvOfqDs/Fk
# we want to test (1) from localhost, (2) from S3 bucket Static Web Site, (3) from our EC2 instance
# app:
# email:
# from: d.art.shishkin@gmail.com
# carbon-copy: d.art.shishkin@gmail.com
# cors:
# allowed-origins: http://localhost:4200,http://art-support-portal.s3-website.eu-north-1.amazonaws.com,http://support-portal.shyshkin.net,http://portal.shyshkin.net
# server:
# port: 5000
# logging:
# level:
# net.shyshkin: debug
---
# spring:
# config:
# activate:
# on-profile: aws-rds
# datasource:
# url: jdbc:mysql://210.18.189.94:8098/demo
# username: youruser
# password: youruserpassword
# mail:
# host: email-smtp.eu-north-1.amazonaws.com
# port: 587
# username: custom_text
# password: custom_text
# we want to test (1) from localhost, (2) from S3 bucket Static Web Site, (3) from our EC2 instance
# app:
# email:
# from: d.art.shishkin@gmail.com
# carbon-copy: d.art.shishkin@gmail.com
# cors:
# allowed-origins: http://localhost:4200,http://art-support-portal.s3-website.eu-north-1.amazonaws.com,http://support-portal.shyshkin.net,http://portal.shyshkin.net
# server:
# port: 5000
# logging:
# level:
# net.shyshkin: debug
#####
#
# HTTPS configuration
#
#####
# server.ssl:
# enabled: true # Enable HTTPS support (only accept HTTPS requests)
# key-alias: securedPortal # Alias that identifies the key in the key store
# key-store: classpath:securedPortal-keystore.p12 # Keystore location
# key-store-password: custom_text
# key-store-type: PKCS12 # Keystore format
# ---
# spring:
# config:
# activate:
# on-profile: image-s3
# app:
# amazon-s3:
# bucket-name: portal-user-profile-images
# ---
# spring:
# config:
# activate:
# on-profile: image-s3-localstack
# app:
# amazon-s3:
# bucket-name: portal-user-profile-images
# config:
# aws:
# region: eu-north-1
# s3:
# url: http://127.0.0.1:4566
# bucket-name: portal-user-profile-images
# access-key: localstack
# secret-key: localstack
# Development file upload configuration with custom directory
spring:
config:
activate:
on-profile: dev-custom-upload
file:
upload:
directory: ${user.home}/blog-uploads

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Some files were not shown because too many files have changed in this diff Show More