Compare commits
87 Commits
7b153f4cf6
...
feature/my
| Author | SHA1 | Date | |
|---|---|---|---|
| 39e49a40cb | |||
| a10803ee73 | |||
| 3856524152 | |||
| fc973e86d2 | |||
| 3e96bd4fb8 | |||
| 08d3a8b9f4 | |||
| a65bf2dfaa | |||
| 8eb755abdb | |||
| 0f9473595d | |||
| a8b54c1209 | |||
| 0a60f016e9 | |||
| b97e57e070 | |||
| 152ea94034 | |||
| 9348e456a7 | |||
| 96f56fd1ca | |||
| fdab880de2 | |||
| 4286934d9d | |||
| 1aa92b7501 | |||
| d1173ed400 | |||
| 8eeed12d0b | |||
| a9cc0a9122 | |||
| 311ca61dea | |||
| c228fee79c | |||
| 71af6e4268 | |||
| 0df3fc6ae3 | |||
| 6d46457973 | |||
| 46234722f8 | |||
| 931e49e58b | |||
| 434d95eeaf | |||
| 91cb073c78 | |||
| 53ba9d1beb | |||
| 953c9dea16 | |||
| 3d1fab5c72 | |||
| ed17ec6c7e | |||
| 74ea5a62bf | |||
| 42ed53db2c | |||
| 41824cfc38 | |||
| 9643c762d5 | |||
| 1386459e03 | |||
| f5210206f6 | |||
| 0ad84e030a | |||
| 5a88ba6993 | |||
| b5df742a80 | |||
| e6b9a44f9b | |||
| 677462879e | |||
| c7bf66814f | |||
| 90e4c719fb | |||
| 3804f39632 | |||
| 0c3054df1b | |||
| 279f025432 | |||
| 94c4f03455 | |||
| 8e20b100eb | |||
| 10ee87fce4 | |||
| 10b80e12ba | |||
| 43ed620a0a | |||
| 7763940b2a | |||
| 2f6c0a08b4 | |||
| 55bacb9d2d | |||
| 46343199d8 | |||
| 45640454bc | |||
| e1abf8631f | |||
| 93917b63e1 | |||
| b81b4b49ae | |||
| df444a5db2 | |||
| d2dc7eb83b | |||
| c0e6685005 | |||
| 189d4cec77 | |||
| 464531aad6 | |||
| 158f97c657 | |||
| 1728167833 | |||
| 6525c50679 | |||
| badde1665d | |||
| 0e45929b5e | |||
| e72a3ea6ba | |||
| 106d42860c | |||
| b8b004bf31 | |||
| bd2f5b95ce | |||
| cfe68a276f | |||
| 911933074b | |||
| 9712439261 | |||
| 7d701d3747 | |||
| d8be4e7ca1 | |||
| 279c6f9c1a | |||
| f41a1043c9 | |||
| e206968993 | |||
| bd5119fc3c | |||
| 9c6c3abc32 |
70
docker-compose.yml
Normal file
70
docker-compose.yml
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: support-portal-backend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
environment:
|
||||||
|
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:
|
||||||
|
- 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:
|
||||||
|
angular-spring: {}
|
||||||
|
spring-mysql: {}
|
||||||
@ -17,7 +17,7 @@ services:
|
|||||||
image: adminer
|
image: adminer
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- 8081:8080
|
- 8082:8080
|
||||||
|
|
||||||
#https://medium.com/dandelion-tutorials/using-s3-localstack-with-spring-boot-and-r2dbc-5ea201a18aea
|
#https://medium.com/dandelion-tutorials/using-s3-localstack-with-spring-boot-and-r2dbc-5ea201a18aea
|
||||||
|
|
||||||
|
|||||||
46
support-portal-backend/Dockerfile
Normal file
46
support-portal-backend/Dockerfile
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# =======================
|
||||||
|
# Builder stage
|
||||||
|
# =======================
|
||||||
|
FROM maven:3.8.5-eclipse-temurin-17 AS builder
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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)"]
|
||||||
@ -15,8 +15,11 @@
|
|||||||
<description>Demo project for Spring Boot</description>
|
<description>Demo project for Spring Boot</description>
|
||||||
<properties>
|
<properties>
|
||||||
<java.version>11</java.version>
|
<java.version>11</java.version>
|
||||||
|
<maven.compiler.source>11</maven.compiler.source>
|
||||||
|
<maven.compiler.target>11</maven.compiler.target>
|
||||||
|
|
||||||
<mapstruct.version>1.4.2.Final</mapstruct.version>
|
|
||||||
|
<mapstruct.version>1.5.5.Final</mapstruct.version>
|
||||||
|
|
||||||
<docker.image.prefix>artarkatesoft</docker.image.prefix>
|
<docker.image.prefix>artarkatesoft</docker.image.prefix>
|
||||||
<docker.image.name>angular-${project.artifactId}</docker.image.name>
|
<docker.image.name>angular-${project.artifactId}</docker.image.name>
|
||||||
@ -29,6 +32,12 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- <dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-rest</artifactId>
|
||||||
|
</dependency> -->
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-security</artifactId>
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
@ -100,11 +109,11 @@
|
|||||||
<version>1.12.75</version>
|
<version>1.12.75</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<!-- <dependency>
|
||||||
<groupId>com.github.ulisesbocchio</groupId>
|
<groupId>com.github.ulisesbocchio</groupId>
|
||||||
<artifactId>jasypt-spring-boot-starter</artifactId>
|
<artifactId>jasypt-spring-boot-starter</artifactId>
|
||||||
<version>3.0.3</version>
|
<version>3.0.3</version>
|
||||||
</dependency>
|
</dependency> -->
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
@ -157,96 +166,56 @@
|
|||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<finalName>support-portal-backend</finalName>
|
||||||
<plugin>
|
<plugins>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<!-- Spring Boot plugin (repackage to make it executable) -->
|
||||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
<plugin>
|
||||||
<configuration>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<excludes>
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
<exclude>
|
<version>2.5.4</version>
|
||||||
<groupId>org.projectlombok</groupId>
|
<executions>
|
||||||
<artifactId>lombok</artifactId>
|
<execution>
|
||||||
</exclude>
|
<goals>
|
||||||
</excludes>
|
<goal>repackage</goal>
|
||||||
<!-- <executable>true</executable>-->
|
</goals>
|
||||||
<finalName>support-portal</finalName>
|
</execution>
|
||||||
</configuration>
|
</executions>
|
||||||
</plugin>
|
<configuration>
|
||||||
|
<excludes>
|
||||||
|
<exclude>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
</exclude>
|
||||||
|
</excludes>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
<plugin>
|
<!-- Compiler plugin for MapStruct and Lombok -->
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<plugin>
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<configuration>
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
<annotationProcessorPaths>
|
<configuration>
|
||||||
<path>
|
<annotationProcessorPaths>
|
||||||
<groupId>org.projectlombok</groupId>
|
<path>
|
||||||
<artifactId>lombok-mapstruct-binding</artifactId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<version>0.2.0</version>
|
<artifactId>lombok-mapstruct-binding</artifactId>
|
||||||
</path>
|
<version>0.2.0</version>
|
||||||
<path>
|
</path>
|
||||||
<groupId>org.mapstruct</groupId>
|
<path>
|
||||||
<artifactId>mapstruct-processor</artifactId>
|
<groupId>org.mapstruct</groupId>
|
||||||
<version>${mapstruct.version}</version>
|
<artifactId>mapstruct-processor</artifactId>
|
||||||
</path>
|
<version>${mapstruct.version}</version>
|
||||||
<path>
|
</path>
|
||||||
<groupId>org.projectlombok</groupId>
|
<path>
|
||||||
<artifactId>lombok</artifactId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<version>${lombok.version}</version>
|
<artifactId>lombok</artifactId>
|
||||||
</path>
|
<version>1.18.28</version>
|
||||||
|
</path>
|
||||||
|
</annotationProcessorPaths>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
</annotationProcessorPaths>
|
|
||||||
<compilerArgs>
|
|
||||||
<compilerArg>
|
|
||||||
-Amapstruct.defaultComponentModel=spring
|
|
||||||
</compilerArg>
|
|
||||||
</compilerArgs>
|
|
||||||
</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>
|
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@ -0,0 +1,74 @@
|
|||||||
|
|
||||||
|
package net.shyshkin.study.fullstack.supportportal.backend.config;
|
||||||
|
|
||||||
|
import net.shyshkin.study.fullstack.supportportal.backend.domain.User;
|
||||||
|
import net.shyshkin.study.fullstack.supportportal.backend.repository.UserRepository;
|
||||||
|
import org.springframework.boot.CommandLineRunner;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static net.shyshkin.study.fullstack.supportportal.backend.constant.Authority.*;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class DataInitializer {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public CommandLineRunner init(UserRepository userRepository, PasswordEncoder passwordEncoder) {
|
||||||
|
return args -> {
|
||||||
|
System.out.println("Running DataInitializer...");
|
||||||
|
|
||||||
|
// Initialize users
|
||||||
|
createUserIfNotExists(userRepository, passwordEncoder, "admin", "adminpassword", "admin@example.com", "Admin", "User", "ROLE_ADMIN");
|
||||||
|
createUserIfNotExists(userRepository, passwordEncoder, "hr", "hrpassword", "hr@example.com", "HR", "User", "ROLE_HR");
|
||||||
|
createUserIfNotExists(userRepository, passwordEncoder, "manager", "managerpassword", "manager@example.com", "Manager", "User", "ROLE_MANAGER");
|
||||||
|
createUserIfNotExists(userRepository, passwordEncoder, "user", "userpassword", "user@example.com", "Regular", "User", "ROLE_USER");
|
||||||
|
createUserIfNotExists(userRepository, passwordEncoder, "superadmin", "superadminpassword", "superadmin@example.com", "Super", "Admin", "ROLE_SUPER_ADMIN");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createUserIfNotExists(UserRepository userRepository, PasswordEncoder passwordEncoder,
|
||||||
|
String username, String password, String email,
|
||||||
|
String firstName, String lastName, String role) {
|
||||||
|
if (userRepository.findByUsername(username).isEmpty()) {
|
||||||
|
String encodedPassword = passwordEncoder.encode(password);
|
||||||
|
|
||||||
|
User user = User.builder()
|
||||||
|
.email(email)
|
||||||
|
.firstName(firstName)
|
||||||
|
.lastName(lastName)
|
||||||
|
.username(username)
|
||||||
|
.password(encodedPassword)
|
||||||
|
.userId(UUID.randomUUID())
|
||||||
|
.isActive(true)
|
||||||
|
.isNotLocked(true)
|
||||||
|
.role(role)
|
||||||
|
.authorities(getAuthoritiesByRole(role))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
userRepository.save(user);
|
||||||
|
System.out.println(role + " user created successfully.");
|
||||||
|
} else {
|
||||||
|
System.out.println(role + " user already exists.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String[] getAuthoritiesByRole(String role) {
|
||||||
|
switch (role) {
|
||||||
|
case "ROLE_ADMIN":
|
||||||
|
return ADMIN_AUTHORITIES;
|
||||||
|
case "ROLE_HR":
|
||||||
|
return HR_AUTHORITIES;
|
||||||
|
case "ROLE_MANAGER":
|
||||||
|
return MANAGER_AUTHORITIES;
|
||||||
|
case "ROLE_USER":
|
||||||
|
return USER_AUTHORITIES;
|
||||||
|
case "ROLE_SUPER_ADMIN":
|
||||||
|
return SUPER_ADMIN_AUTHORITIES;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Invalid role: " + role);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
// package com.example.tamilnadureservoir.config;
|
||||||
|
|
||||||
|
|
||||||
|
package net.shyshkin.study.fullstack.supportportal.backend.config;
|
||||||
|
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class RestTemplateConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RestTemplate restTemplate() {
|
||||||
|
return new RestTemplate();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -7,6 +7,7 @@ import net.shyshkin.study.fullstack.supportportal.backend.filter.JwtAuthenticati
|
|||||||
import net.shyshkin.study.fullstack.supportportal.backend.filter.JwtAuthorizationFilter;
|
import net.shyshkin.study.fullstack.supportportal.backend.filter.JwtAuthorizationFilter;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.security.authentication.AuthenticationManager;
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
|
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.core.userdetails.UserDetailsService;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
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
|
@EnableWebSecurity
|
||||||
@EnableGlobalMethodSecurity(prePostEnabled = true)
|
@EnableGlobalMethodSecurity(prePostEnabled = true)
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
|
@Configuration
|
||||||
public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||||
|
|
||||||
private final JwtAuthorizationFilter jwtAuthorizationFilter;
|
private final JwtAuthorizationFilter jwtAuthorizationFilter;
|
||||||
@ -38,17 +41,19 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|||||||
@Value("${app.public-urls}")
|
@Value("${app.public-urls}")
|
||||||
private String[] publicUrls;
|
private String[] publicUrls;
|
||||||
|
|
||||||
|
@Value("${app.cors.allowed-origins}")
|
||||||
|
private String[] allowedOrigins;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void configure(HttpSecurity http) throws Exception {
|
protected void configure(HttpSecurity http) throws Exception {
|
||||||
|
|
||||||
http.csrf().disable();
|
http.csrf().disable();
|
||||||
|
|
||||||
http.cors();
|
http.cors();
|
||||||
|
|
||||||
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
|
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
|
||||||
|
|
||||||
http.authorizeRequests()
|
http.authorizeRequests()
|
||||||
.antMatchers(publicUrls).permitAll()
|
.antMatchers(publicUrls).permitAll()
|
||||||
|
.antMatchers(POST, "/api/files/upload").permitAll() // Allow file uploads
|
||||||
.anyRequest().authenticated();
|
.anyRequest().authenticated();
|
||||||
|
|
||||||
http.exceptionHandling()
|
http.exceptionHandling()
|
||||||
@ -60,9 +65,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
|
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
|
||||||
auth
|
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
|
||||||
.userDetailsService(userDetailsService)
|
|
||||||
.passwordEncoder(passwordEncoder);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ -72,24 +75,17 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public WebMvcConfigurer corsConfigurer(@Value("${app.cors.allowed-origins}") String[] allowedOrigins) {
|
public CorsConfigurationSource corsConfigurationSource() {
|
||||||
return new WebMvcConfigurer() {
|
CorsConfiguration config = new CorsConfiguration();
|
||||||
@Override
|
config.setAllowedOrigins(Arrays.asList(allowedOrigins));
|
||||||
public void addCorsMappings(CorsRegistry registry) {
|
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||||
registry.addMapping("/user/login")
|
config.setAllowedHeaders(Arrays.asList("*"));
|
||||||
.allowedOrigins(allowedOrigins)
|
config.setExposedHeaders(Arrays.asList(SecurityConstants.JWT_TOKEN_HEADER));
|
||||||
.exposedHeaders(SecurityConstants.JWT_TOKEN_HEADER);
|
config.setAllowCredentials(true);
|
||||||
|
config.setMaxAge(3600L); // 1 hour
|
||||||
|
|
||||||
String[] allowedMethods = List.of(GET, POST, PUT, DELETE)
|
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||||
.stream()
|
source.registerCorsConfiguration("/**", config);
|
||||||
.map(Enum::name)
|
return source;
|
||||||
.toArray(String[]::new);
|
|
||||||
|
|
||||||
registry.addMapping("/**")
|
|
||||||
.allowedMethods(allowedMethods)
|
|
||||||
.allowedOrigins(allowedOrigins);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,7 +4,7 @@ public class FileConstant {
|
|||||||
|
|
||||||
public static final String USER_IMAGE_PATH = "/user/image/";
|
public static final String USER_IMAGE_PATH = "/user/image/";
|
||||||
public static final String JPG_EXTENSION = "jpg";
|
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 DIRECTORY_CREATED = "Created directory for: ";
|
||||||
public static final String DEFAULT_USER_IMAGE_URI_PATTERN = "/user/%s/profile-image";
|
public static final String DEFAULT_USER_IMAGE_URI_PATTERN = "/user/%s/profile-image";
|
||||||
public static final String USER_IMAGE_FILENAME = "avatar.jpg";
|
public static final String USER_IMAGE_FILENAME = "avatar.jpg";
|
||||||
@ -14,4 +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 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 TEMP_PROFILE_IMAGE_BASE_URL = "https://robohash.org/";
|
||||||
|
|
||||||
|
// 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_";
|
||||||
}
|
}
|
||||||
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
// EventController.java - FIXED
|
||||||
|
package net.shyshkin.study.fullstack.supportportal.backend.controller;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import net.shyshkin.study.fullstack.supportportal.backend.domain.Event;
|
||||||
|
import net.shyshkin.study.fullstack.supportportal.backend.repository.EventRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
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
|
||||||
|
private EventRepository eventRepository;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<List<Event>> getAllEvents() {
|
||||||
|
List<Event> events = eventRepository.findAll();
|
||||||
|
return new ResponseEntity<>(events, HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity<Event> getEventById(@PathVariable Long id) {
|
||||||
|
Optional<Event> event = eventRepository.findById(id);
|
||||||
|
return event.map(ResponseEntity::ok)
|
||||||
|
.orElseGet(() -> ResponseEntity.notFound().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@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}")
|
||||||
|
public ResponseEntity<Event> updateEvent(@PathVariable Long id, @RequestBody Event event) {
|
||||||
|
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}")
|
||||||
|
public ResponseEntity<Void> deleteEvent(@PathVariable Long id) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
// }
|
||||||
|
}
|
||||||
@ -0,0 +1,227 @@
|
|||||||
|
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;
|
||||||
|
import net.shyshkin.study.fullstack.supportportal.backend.repository.PostRepository;
|
||||||
|
import net.shyshkin.study.fullstack.supportportal.backend.repository.ProfessorRepository;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
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;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all unique tags with count
|
||||||
|
@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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all posts associated with a specific tag where isPosted is true
|
||||||
|
@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 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 -> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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()
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
List<Professor> professors = professorRepository.findAllById(validProfessorIds);
|
||||||
|
post.setProfessors(professors);
|
||||||
|
|
||||||
|
// Set tags
|
||||||
|
post.setTags(postDto.getTags());
|
||||||
|
|
||||||
|
// Save the post
|
||||||
|
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) {
|
||||||
|
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()
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
List<Professor> professors = professorRepository.findAllById(validProfessorIds);
|
||||||
|
post.setProfessors(professors);
|
||||||
|
|
||||||
|
// Set tags
|
||||||
|
post.setTags(postDto.getTags());
|
||||||
|
|
||||||
|
// Save the updated post
|
||||||
|
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);
|
||||||
|
log.info("Successfully deleted post with id: {}", id);
|
||||||
|
return ResponseEntity.noContent().<Void>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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,116 @@
|
|||||||
|
package net.shyshkin.study.fullstack.supportportal.backend.controller;
|
||||||
|
|
||||||
|
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.Professor;
|
||||||
|
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.ProfessorDto;
|
||||||
|
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.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.springframework.http.HttpStatus.OK;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("professor")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ProfessorResource {
|
||||||
|
|
||||||
|
private final ProfessorService professorService;
|
||||||
|
|
||||||
|
@GetMapping("home")
|
||||||
|
public String showProfessor() {
|
||||||
|
return "Application works";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("register")
|
||||||
|
public Professor register(@RequestBody Professor professor) {
|
||||||
|
return professorService.register(professor.getFirstName(), professor.getLastName(),
|
||||||
|
professor.getEmail(), professor.getDepartment(), professor.getPosition());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("add")
|
||||||
|
public ResponseEntity<Professor> addNewProfessor(@Valid ProfessorDto professorDto) {
|
||||||
|
log.debug("Professor DTO: {}", professorDto);
|
||||||
|
Professor professor = professorService.addNewProfessor(professorDto);
|
||||||
|
return ResponseEntity.ok(professor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("{professorId}")
|
||||||
|
public Professor updateProfessor(@PathVariable UUID professorId, @Valid ProfessorDto professorDto) {
|
||||||
|
log.debug("Professor DTO: {}", professorDto);
|
||||||
|
return professorService.updateProfessor(professorId, professorDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("{professorId}")
|
||||||
|
public Professor findProfessorById(@PathVariable UUID professorId) {
|
||||||
|
return professorService.findByProfessorId(professorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("email/{email}")
|
||||||
|
public Professor findProfessorByEmail(@PathVariable String email) {
|
||||||
|
return professorService.findByEmail(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public Page<Professor> getAllProfessors(Pageable pageable) {
|
||||||
|
return professorService.findAll(pageable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("{professorId}")
|
||||||
|
public HttpResponse deleteProfessor(@PathVariable UUID professorId) {
|
||||||
|
professorService.deleteProfessor(professorId);
|
||||||
|
return HttpResponse.builder()
|
||||||
|
.httpStatusCode(OK.value())
|
||||||
|
.httpStatus(OK)
|
||||||
|
.reason(OK.getReasonPhrase())
|
||||||
|
.message("Professor deleted successfully")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("{professorId}/profile-image")
|
||||||
|
public Professor updateProfileImage(@PathVariable UUID professorId,
|
||||||
|
@RequestParam MultipartFile profileImage) {
|
||||||
|
return professorService.updateProfileImage(professorId, profileImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(path = "{professorId}/profile-image/{filename}", produces = MediaType.IMAGE_JPEG_VALUE)
|
||||||
|
public byte[] getProfileImageByProfessorId(@PathVariable UUID professorId,
|
||||||
|
@PathVariable String filename) {
|
||||||
|
return professorService.getImageByProfessorId(professorId, filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(path = "{professorId}/profile-image", produces = MediaType.IMAGE_JPEG_VALUE)
|
||||||
|
public byte[] getDefaultProfileImage(@PathVariable UUID professorId) {
|
||||||
|
return professorService.getDefaultProfileImage(professorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bulk-update displayOrder.
|
||||||
|
*
|
||||||
|
* Accepts a JSON array of professor UUIDs in the desired display order.
|
||||||
|
* The array index becomes each professor's displayOrder value.
|
||||||
|
*
|
||||||
|
* Example request body:
|
||||||
|
* ["uuid-A", "uuid-B", "uuid-C"]
|
||||||
|
*
|
||||||
|
* Returns the full professor list sorted by the new displayOrder.
|
||||||
|
*
|
||||||
|
* Requires ADMIN or MANAGER role (configure in your SecurityConfig).
|
||||||
|
*/
|
||||||
|
@PutMapping("order")
|
||||||
|
public ResponseEntity<List<Professor>> updateDisplayOrder(@RequestBody List<UUID> orderedIds) {
|
||||||
|
log.info("Updating display order for {} professors", orderedIds.size());
|
||||||
|
List<Professor> updated = professorService.updateDisplayOrder(orderedIds);
|
||||||
|
return ResponseEntity.ok(updated);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,198 @@
|
|||||||
|
// package com.example.tamilnadureservoir.controller;
|
||||||
|
|
||||||
|
package net.shyshkin.study.fullstack.supportportal.backend.controller;
|
||||||
|
|
||||||
|
import java.io.StringReader;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import javax.xml.parsers.DocumentBuilder;
|
||||||
|
import javax.xml.parsers.DocumentBuilderFactory;
|
||||||
|
import javax.xml.transform.dom.DOMResult;
|
||||||
|
import javax.xml.transform.dom.DOMSource;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.HttpEntity;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PatchMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestHeader;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
import org.w3c.dom.Document;
|
||||||
|
import org.xml.sax.InputSource;
|
||||||
|
|
||||||
|
import net.shyshkin.study.fullstack.supportportal.backend.domain.ConferenceData;
|
||||||
|
import net.shyshkin.study.fullstack.supportportal.backend.repository.ConferenceDataRepository;
|
||||||
|
|
||||||
|
|
||||||
|
@CrossOrigin(origins = "*", maxAge = 3600)
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/soap")
|
||||||
|
public class SoapController {
|
||||||
|
|
||||||
|
private final RestTemplate restTemplate;
|
||||||
|
private final ConferenceDataRepository conferenceDataRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public SoapController(RestTemplate restTemplate, ConferenceDataRepository conferenceDataRepository) {
|
||||||
|
this.restTemplate = restTemplate;
|
||||||
|
this.conferenceDataRepository = conferenceDataRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/insertConferenceData")
|
||||||
|
public ResponseEntity<String> insertConferenceData(@RequestBody ConferenceData conferenceData) {
|
||||||
|
try {
|
||||||
|
// Save the conferenceData object to the database
|
||||||
|
ConferenceData savedConferenceData = conferenceDataRepository.save(conferenceData);
|
||||||
|
return new ResponseEntity<>("ConferenceData inserted with ID: " + savedConferenceData.getId(),
|
||||||
|
HttpStatus.CREATED);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return new ResponseEntity<>("Failed to insert ConferenceData: " + e.getMessage(),
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/getAllConferenceData")
|
||||||
|
public ResponseEntity<List<ConferenceData>> getAllConferenceData() {
|
||||||
|
List<ConferenceData> conferenceDataList = conferenceDataRepository.findAll();
|
||||||
|
return new ResponseEntity<>(conferenceDataList, HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/updateConferenceData/{id}")
|
||||||
|
public ResponseEntity<ConferenceData> updateConferenceData(@PathVariable Long id,
|
||||||
|
@RequestBody ConferenceData updatedData) {
|
||||||
|
// Implement the logic to update conference data by ID
|
||||||
|
Optional<ConferenceData> existingData = conferenceDataRepository.findById(id);
|
||||||
|
|
||||||
|
if (existingData.isPresent()) {
|
||||||
|
ConferenceData dataToUpdate = existingData.get();
|
||||||
|
// Update the fields of dataToUpdate with values from updatedData
|
||||||
|
// e.g., dataToUpdate.setName(updatedData.getName());
|
||||||
|
// ...
|
||||||
|
|
||||||
|
// Save the updated data
|
||||||
|
conferenceDataRepository.save(dataToUpdate);
|
||||||
|
|
||||||
|
return new ResponseEntity<>(dataToUpdate, HttpStatus.OK);
|
||||||
|
} else {
|
||||||
|
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PatchMapping("/partialUpdateConferenceData/{id}")
|
||||||
|
public ResponseEntity<ConferenceData> partialUpdateConferenceData(@PathVariable Long id,
|
||||||
|
@RequestBody Map<String, Object> updates) {
|
||||||
|
// Implement the logic to partially update conference data by ID
|
||||||
|
Optional<ConferenceData> existingData = conferenceDataRepository.findById(id);
|
||||||
|
|
||||||
|
if (existingData.isPresent()) {
|
||||||
|
ConferenceData dataToUpdate = existingData.get();
|
||||||
|
|
||||||
|
// Apply updates from the request body to dataToUpdate
|
||||||
|
for (Map.Entry<String, Object> entry : updates.entrySet()) {
|
||||||
|
String key = entry.getKey();
|
||||||
|
Object value = entry.getValue();
|
||||||
|
|
||||||
|
// Update specific fields based on the key-value pairs
|
||||||
|
// e.g., if (key.equals("name")) dataToUpdate.setName((String) value);
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the partially updated data
|
||||||
|
conferenceDataRepository.save(dataToUpdate);
|
||||||
|
|
||||||
|
return new ResponseEntity<>(dataToUpdate, HttpStatus.OK);
|
||||||
|
} else {
|
||||||
|
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/deleteConferenceData/{id}")
|
||||||
|
public ResponseEntity<Void> deleteConferenceData(@PathVariable Long id) {
|
||||||
|
// Implement the logic to delete conference data by ID
|
||||||
|
conferenceDataRepository.deleteById(id);
|
||||||
|
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/getConferenceDataByPhone/{phone}")
|
||||||
|
public ResponseEntity<Object> getConferenceDataByPhone(@PathVariable("phone") String phone) {
|
||||||
|
Optional<ConferenceData> conferenceDataOptional = conferenceDataRepository.findByPhone(phone);
|
||||||
|
|
||||||
|
if (conferenceDataOptional.isPresent()) {
|
||||||
|
ConferenceData conferenceData = conferenceDataOptional.get();
|
||||||
|
return new ResponseEntity<>(conferenceData, HttpStatus.OK);
|
||||||
|
} else {
|
||||||
|
return new ResponseEntity<>("ConferenceData not found for phone: " + phone, HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "/callWebService", consumes = "application/soap+xml", produces = MediaType.APPLICATION_XML_VALUE)
|
||||||
|
public String callWebService(
|
||||||
|
@RequestBody String soapRequest,
|
||||||
|
@RequestHeader("Content-Type") String contentType,
|
||||||
|
@RequestHeader("SOAPAction") String soapAction) {
|
||||||
|
|
||||||
|
// Log or use the 'Content-Type' and 'SOAPAction' headers as needed
|
||||||
|
System.out.println("Content-Type: " + contentType);
|
||||||
|
System.out.println("SOAPAction: " + soapAction);
|
||||||
|
|
||||||
|
// Specify the SOAP action for your ASMX web service
|
||||||
|
String soapActionValue = soapAction;
|
||||||
|
String url = "https://clin.cmcvellore.ac.in/newconference/ConferencePay.asmx";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create a DocumentBuilder to parse the SOAP request string
|
||||||
|
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||||
|
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||||
|
Document requestDoc = builder.parse(new InputSource(new StringReader(soapRequest)));
|
||||||
|
|
||||||
|
// Create a DOMSource from the parsed SOAP request
|
||||||
|
DOMSource requestSource = new DOMSource(requestDoc);
|
||||||
|
|
||||||
|
// Create a DOMResult to capture the response
|
||||||
|
DOMResult responseResult = new DOMResult();
|
||||||
|
|
||||||
|
// Set the Content-Type header to specify the SOAP format
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.TEXT_XML);
|
||||||
|
|
||||||
|
// Set the SOAPAction header to specify the SOAP action
|
||||||
|
headers.set("SOAPAction", soapActionValue);
|
||||||
|
|
||||||
|
// Create a HttpEntity with the headers
|
||||||
|
HttpEntity<DOMSource> httpEntity = new HttpEntity<>(requestSource, headers);
|
||||||
|
|
||||||
|
// Send the SOAP request to the external ASMX web service
|
||||||
|
ResponseEntity<String> responseEntity = restTemplate.exchange(
|
||||||
|
url,
|
||||||
|
HttpMethod.POST,
|
||||||
|
httpEntity,
|
||||||
|
String.class);
|
||||||
|
|
||||||
|
// Extract the response XML from the ResponseEntity
|
||||||
|
String responseXml = responseEntity.getBody();
|
||||||
|
|
||||||
|
// Handle the SOAP response as needed
|
||||||
|
return responseXml;
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Handle exceptions
|
||||||
|
e.printStackTrace(); // You can log the exception details
|
||||||
|
return null; // Return an appropriate response or handle differently
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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!";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
package net.shyshkin.study.fullstack.supportportal.backend.controller;
|
package net.shyshkin.study.fullstack.supportportal.backend.controller;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import net.shyshkin.study.fullstack.supportportal.backend.constant.SecurityConstants;
|
import net.shyshkin.study.fullstack.supportportal.backend.constant.SecurityConstants;
|
||||||
@ -32,6 +33,13 @@ import static org.springframework.http.HttpStatus.OK;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class UserResource {
|
public class UserResource {
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class UserDTO{
|
||||||
|
String username;
|
||||||
|
String password;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
private final AuthenticationManager authenticationManager;
|
private final AuthenticationManager authenticationManager;
|
||||||
private final JwtTokenProvider jwtTokenProvider;
|
private final JwtTokenProvider jwtTokenProvider;
|
||||||
@ -47,7 +55,8 @@ public class UserResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("login")
|
@PostMapping("login")
|
||||||
public ResponseEntity<User> login(@RequestBody User user) {
|
public ResponseEntity<User> login(@RequestBody UserDTO user) {
|
||||||
|
// public ResponseEntity<User> login(@RequestBody User user) {
|
||||||
|
|
||||||
authenticate(user.getUsername(), user.getPassword());
|
authenticate(user.getUsername(), user.getPassword());
|
||||||
User byUsername = userService.findByUsername(user.getUsername());
|
User byUsername = userService.findByUsername(user.getUsername());
|
||||||
|
|||||||
@ -0,0 +1,45 @@
|
|||||||
|
package net.shyshkin.study.fullstack.supportportal.backend.domain;
|
||||||
|
|
||||||
|
|
||||||
|
import javax.persistence.*;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@MappedSuperclass
|
||||||
|
public abstract class BaseEntity {
|
||||||
|
|
||||||
|
@Column(nullable = false, updatable = false)
|
||||||
|
private LocalDateTime createdDate;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private LocalDateTime updatedDate;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private boolean isDeleted = false;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
createdDate = LocalDateTime.now();
|
||||||
|
updatedDate = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
protected void onUpdate() {
|
||||||
|
updatedDate = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedDate() {
|
||||||
|
return createdDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getUpdatedDate() {
|
||||||
|
return updatedDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDeleted() {
|
||||||
|
return isDeleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDeleted(boolean deleted) {
|
||||||
|
isDeleted = deleted;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
package net.shyshkin.study.fullstack.supportportal.backend.domain;
|
||||||
|
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.GeneratedValue;
|
||||||
|
import javax.persistence.GenerationType;
|
||||||
|
import javax.persistence.Id;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Data
|
||||||
|
public class ConferenceData {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
// Request fields
|
||||||
|
private String conferencecode;
|
||||||
|
private String conferenceyear;
|
||||||
|
private String bankname;
|
||||||
|
private String remoteip;
|
||||||
|
private String regno;
|
||||||
|
private String candidatename;
|
||||||
|
private String nameinreceipt;
|
||||||
|
private String address1;
|
||||||
|
private String address2;
|
||||||
|
private String city;
|
||||||
|
private String state;
|
||||||
|
private String country;
|
||||||
|
private String pincode;
|
||||||
|
private String phone;
|
||||||
|
private String mobile;
|
||||||
|
private String email;
|
||||||
|
private String foodtype;
|
||||||
|
private String participanttype;
|
||||||
|
private String practicetype;
|
||||||
|
private String accompanymembers;
|
||||||
|
private String paymentamount;
|
||||||
|
private String ToWards;
|
||||||
|
private String Allow80G;
|
||||||
|
private String PanCardNo;
|
||||||
|
private String hasgst;
|
||||||
|
private String GSTReg;
|
||||||
|
private String gstnumber;
|
||||||
|
private String gstmobileno;
|
||||||
|
private String gstemailid;
|
||||||
|
private String inputcaption1;
|
||||||
|
private String inputvalue1;
|
||||||
|
private String inputcaption2;
|
||||||
|
private String inputvalue2;
|
||||||
|
private String inputcaption3;
|
||||||
|
private String inputvalue3;
|
||||||
|
private String inputcaption4;
|
||||||
|
private String inputvalue4;
|
||||||
|
private String inputcaption5;
|
||||||
|
private String inputvalue5;
|
||||||
|
|
||||||
|
// Response fields
|
||||||
|
private String responseTransid;
|
||||||
|
private String responseResultCode;
|
||||||
|
private String responseResult;
|
||||||
|
private String responseURL;
|
||||||
|
|
||||||
|
// Constructors, getters, and setters
|
||||||
|
|
||||||
|
// You can generate getters and setters for each field using your IDE or
|
||||||
|
// manually.
|
||||||
|
}
|
||||||
@ -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")
|
||||||
|
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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,151 @@
|
|||||||
|
package net.shyshkin.study.fullstack.supportportal.backend.domain;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
|
||||||
|
import javax.persistence.*;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Entity
|
||||||
|
@Table(name = "events")
|
||||||
|
public class Event {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private String code;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String year;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String subject;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
@ElementCollection
|
||||||
|
@CollectionTable(name = "venues", joinColumns = @JoinColumn(name = "event_id"))
|
||||||
|
private List<Venue> venue;
|
||||||
|
|
||||||
|
@ElementCollection
|
||||||
|
@CollectionTable(name = "highlights", joinColumns = @JoinColumn(name = "event_id"))
|
||||||
|
private List<String> highlights;
|
||||||
|
|
||||||
|
@ElementCollection
|
||||||
|
@CollectionTable(name = "organisers", joinColumns = @JoinColumn(name = "event_id"))
|
||||||
|
private List<String> organisers;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
@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",
|
||||||
|
joinColumns = @JoinColumn(name = "event_id"),
|
||||||
|
inverseJoinColumns = @JoinColumn(name = "professor_id")
|
||||||
|
)
|
||||||
|
private List<Professor> professors;
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
@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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
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 = false) // Changed from true since you're extending BaseEntity
|
||||||
|
public class Post extends BaseEntity {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Column(columnDefinition = "LONGTEXT", nullable = false)
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
@ManyToMany
|
||||||
|
@JoinTable(
|
||||||
|
name = "post_professors",
|
||||||
|
joinColumns = @JoinColumn(name = "post_id"),
|
||||||
|
inverseJoinColumns = @JoinColumn(name = "professor_id")
|
||||||
|
)
|
||||||
|
private List<Professor> professors;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private boolean isPosted;
|
||||||
|
|
||||||
|
@ElementCollection
|
||||||
|
@CollectionTable(name = "post_tags", joinColumns = @JoinColumn(name = "post_id"))
|
||||||
|
@Column(name = "tag")
|
||||||
|
private List<String> tags;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private String imageUrl; // Add this field
|
||||||
|
}
|
||||||
@ -0,0 +1,91 @@
|
|||||||
|
package net.shyshkin.study.fullstack.supportportal.backend.domain;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import lombok.*;
|
||||||
|
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
|
||||||
|
@AllArgsConstructor
|
||||||
|
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
|
||||||
|
@Builder
|
||||||
|
public class Professor implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = -4372214856545239049L;
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Type(type = "org.hibernate.type.UUIDCharType")
|
||||||
|
@Column(length = 36, columnDefinition = "varchar(36)", updatable = false, nullable = false)
|
||||||
|
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;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private WorkingStatus status;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private ProfessorCategory category;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controls display order within each category section on the public frontend.
|
||||||
|
* Lower values appear first. Defaults to 0 (new professors appear at the top).
|
||||||
|
* Admins can drag-and-drop rows in the management UI to reorder.
|
||||||
|
*/
|
||||||
|
@Column(nullable = false, columnDefinition = "INT DEFAULT 0")
|
||||||
|
@Builder.Default
|
||||||
|
private Integer displayOrder = 0;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "professor", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
|
||||||
|
private Set<ProfessorSkill> skills;
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "professor", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
|
||||||
|
private Set<ProfessorAward> awards;
|
||||||
|
|
||||||
|
@ManyToMany(mappedBy = "professors")
|
||||||
|
@JsonIgnore
|
||||||
|
private List<Post> posts;
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return firstName + " " + lastName;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package net.shyshkin.study.fullstack.supportportal.backend.domain;
|
||||||
|
|
||||||
|
public enum ProfessorCategory {
|
||||||
|
FACULTY,
|
||||||
|
SUPPORT_TEAM,
|
||||||
|
TRAINEE_FELLOW,
|
||||||
|
RESIGNED,
|
||||||
|
GUIDES,
|
||||||
|
FRIENDS,
|
||||||
|
PATRONS
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -46,4 +46,5 @@ public class User implements Serializable {
|
|||||||
private boolean isActive;
|
private boolean isActive;
|
||||||
private boolean isNotLocked;
|
private boolean isNotLocked;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,8 @@
|
|||||||
|
package net.shyshkin.study.fullstack.supportportal.backend.domain;
|
||||||
|
|
||||||
|
public enum WorkingStatus {
|
||||||
|
ACTIVE,
|
||||||
|
ON_LEAVE,
|
||||||
|
RETIRED,
|
||||||
|
INACTIVE
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
// 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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
@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;
|
||||||
|
private String imageUrl; // Add this field
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
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 org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotEmpty;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class ProfessorDto {
|
||||||
|
|
||||||
|
@NotEmpty(message = "Should not be empty")
|
||||||
|
private String firstName;
|
||||||
|
private String lastName;
|
||||||
|
private String email;
|
||||||
|
private String department;
|
||||||
|
private String position;
|
||||||
|
private String officeLocation;
|
||||||
|
private WorkingStatus status;
|
||||||
|
private ProfessorCategory category;
|
||||||
|
|
||||||
|
// Received as ISO string from multipart form e.g. "2026-04-22T23:17:58.831Z"
|
||||||
|
private String joinDate;
|
||||||
|
|
||||||
|
private MultipartFile profileImage;
|
||||||
|
|
||||||
|
// Additional fields
|
||||||
|
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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
package net.shyshkin.study.fullstack.supportportal.backend.exception.domain;
|
||||||
|
|
||||||
|
public class HeroImageNotFoundException extends RuntimeException {
|
||||||
|
|
||||||
|
public HeroImageNotFoundException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
package net.shyshkin.study.fullstack.supportportal.backend.exception.domain;
|
||||||
|
|
||||||
|
public class ProfessorNotFoundException extends RuntimeException {
|
||||||
|
public ProfessorNotFoundException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
package net.shyshkin.study.fullstack.supportportal.backend.exception.domain;
|
||||||
|
|
||||||
|
public class PublicationNotFoundException extends RuntimeException {
|
||||||
|
|
||||||
|
public PublicationNotFoundException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
package net.shyshkin.study.fullstack.supportportal.backend.exception.domain;
|
||||||
|
|
||||||
|
public class ServiceTileNotFoundException extends RuntimeException {
|
||||||
|
|
||||||
|
public ServiceTileNotFoundException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
package net.shyshkin.study.fullstack.supportportal.backend.mapper;
|
||||||
|
|
||||||
|
import net.shyshkin.study.fullstack.supportportal.backend.domain.Professor;
|
||||||
|
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.ProfessorDto;
|
||||||
|
import net.shyshkin.study.fullstack.supportportal.backend.domain.WorkingStatus;
|
||||||
|
import org.mapstruct.Mapper;
|
||||||
|
import org.mapstruct.Mapping;
|
||||||
|
import org.mapstruct.Named;
|
||||||
|
|
||||||
|
@Mapper(componentModel = "spring")
|
||||||
|
public interface ProfessorMapper {
|
||||||
|
|
||||||
|
@Mapping(target = "joinDate", ignore = true) // Handled in service
|
||||||
|
@Mapping(target = "status", source = "status", qualifiedByName = "stringToWorkingStatus")
|
||||||
|
Professor toEntity(ProfessorDto professorDto);
|
||||||
|
|
||||||
|
@Mapping(target = "profileImage", ignore = true)
|
||||||
|
@Mapping(target = "joinDate", expression = "java(professor.getJoinDate() != null ? professor.getJoinDate().toString() + 'Z' : null)")
|
||||||
|
@Mapping(target = "status", source = "status", qualifiedByName = "workingStatusToString")
|
||||||
|
ProfessorDto toDto(Professor professor);
|
||||||
|
|
||||||
|
@Named("stringToWorkingStatus")
|
||||||
|
default WorkingStatus stringToWorkingStatus(String status) {
|
||||||
|
return status == null ? null : WorkingStatus.valueOf(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Named("workingStatusToString")
|
||||||
|
default String workingStatusToString(WorkingStatus status) {
|
||||||
|
return status == null ? null : status.name();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,19 +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.User;
|
||||||
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.UserDto;
|
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.UserDto;
|
||||||
|
import org.mapstruct.AfterMapping;
|
||||||
import org.mapstruct.Mapper;
|
import org.mapstruct.Mapper;
|
||||||
import org.mapstruct.Mapping;
|
import org.mapstruct.Mapping;
|
||||||
|
import org.mapstruct.MappingTarget;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Mapper(imports = {LocalDateTime.class})
|
@Mapper(componentModel = "spring")
|
||||||
public interface UserMapper {
|
public interface UserMapper {
|
||||||
|
|
||||||
@Mapping(target = "isNotLocked", source = "notLocked")
|
@Mapping(target = "isNotLocked", source = "notLocked")
|
||||||
@Mapping(target = "isActive", source = "active")
|
@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 = "role", source = "role", resultType = String.class)
|
||||||
@Mapping(target = "authorities", source = "role.authorities")
|
@Mapping(target = "authorities", source = "role.authorities")
|
||||||
User toEntity(UserDto userDto);
|
User toEntity(UserDto userDto);
|
||||||
|
|
||||||
|
@AfterMapping
|
||||||
|
default void setJoinDate(@MappingTarget User user) {
|
||||||
|
if (user.getJoinDate() == null) {
|
||||||
|
user.setJoinDate(LocalDateTime.now());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
package net.shyshkin.study.fullstack.supportportal.backend.mapper;
|
||||||
|
|
||||||
|
// import javax.xml.bind.annotation.XmlElement;
|
||||||
|
import javax.xml.bind.annotation.XmlRootElement;
|
||||||
|
|
||||||
|
@XmlRootElement(name = "YourSoapRequest")
|
||||||
|
public class YourSoapRequest {
|
||||||
|
private String conferencecode;
|
||||||
|
private String conferenceyear;
|
||||||
|
private String bankname;
|
||||||
|
private String remoteip;
|
||||||
|
private String regno;
|
||||||
|
private String candidatename;
|
||||||
|
private String nameinreceipt;
|
||||||
|
private String address1;
|
||||||
|
private String address2;
|
||||||
|
private String city;
|
||||||
|
private String state;
|
||||||
|
private String country;
|
||||||
|
private String pincode;
|
||||||
|
private String phone;
|
||||||
|
private String mobile;
|
||||||
|
private String email;
|
||||||
|
private String foodtype;
|
||||||
|
private String participanttype;
|
||||||
|
private String practicetype;
|
||||||
|
private String accompanymembers;
|
||||||
|
private String paymentamount;
|
||||||
|
private String ToWards;
|
||||||
|
private String Allow80G;
|
||||||
|
private String PanCardNo;
|
||||||
|
private String hasgst;
|
||||||
|
private String GSTReg;
|
||||||
|
private String gstnumber;
|
||||||
|
private String gstmobileno;
|
||||||
|
private String gstemailid;
|
||||||
|
private String inputcaption1;
|
||||||
|
private String inputvalue1;
|
||||||
|
private String inputcaption2;
|
||||||
|
private String inputvalue2;
|
||||||
|
private String inputcaption3;
|
||||||
|
private String inputvalue3;
|
||||||
|
private String inputcaption4;
|
||||||
|
private String inputvalue4;
|
||||||
|
private String inputcaption5;
|
||||||
|
private String inputvalue5;
|
||||||
|
|
||||||
|
// Add getters and setters for each property
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package net.shyshkin.study.fullstack.supportportal.backend.repository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import net.shyshkin.study.fullstack.supportportal.backend.domain.ConferenceData;
|
||||||
|
|
||||||
|
// import org.springframework.data.rest.core.annotation.RepositoryRestResource;
|
||||||
|
|
||||||
|
|
||||||
|
// @RepositoryRestResource(path = "conferences")
|
||||||
|
public interface ConferenceDataRepository extends JpaRepository<ConferenceData, Long> {
|
||||||
|
Optional<ConferenceData> findByPhone(String phone); // Change 'phone' to your actual field name
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
@ -0,0 +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> {
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
package net.shyshkin.study.fullstack.supportportal.backend.repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import net.shyshkin.study.fullstack.supportportal.backend.domain.Post;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface PostRepository extends JpaRepository<Post, Long> {
|
||||||
|
|
||||||
|
// Additional query methods can be defined here if needed
|
||||||
|
|
||||||
|
// 1. Find all posts where isPosted is true
|
||||||
|
List<Post> findAllByIsPostedTrue();
|
||||||
|
|
||||||
|
// 3. Find all posts associated with a specific tag where isPosted is true
|
||||||
|
@Query("SELECT p FROM Post p JOIN p.tags t WHERE t = :tag AND p.isPosted = true")
|
||||||
|
List<Post> findAllByTagAndIsPostedTrue(@Param("tag") String tag);
|
||||||
|
|
||||||
|
// Custom query to count unique tags
|
||||||
|
@Query("SELECT t, COUNT(t) FROM Post p JOIN p.tags t GROUP BY t")
|
||||||
|
List<Object[]> findTagsWithCount();
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
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.Modifying;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
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;
|
||||||
|
|
||||||
|
public interface ProfessorRepository extends JpaRepository<Professor, Long> {
|
||||||
|
|
||||||
|
@Query("SELECT p FROM Professor p WHERE p.email = :email")
|
||||||
|
Optional<Professor> findByEmail(@Param("email") String email);
|
||||||
|
|
||||||
|
@Query("SELECT CASE WHEN COUNT(p) > 0 THEN TRUE ELSE FALSE END FROM Professor p WHERE p.email = :email")
|
||||||
|
Boolean existsByEmail(@Param("email") String email);
|
||||||
|
|
||||||
|
Boolean existsByProfessorId(UUID professorId);
|
||||||
|
|
||||||
|
@Query("SELECT p FROM Professor p WHERE p.professorId = :professorId")
|
||||||
|
Optional<Professor> findByProfessorId(@Param("professorId") UUID professorId);
|
||||||
|
|
||||||
|
// ── Status / category filters, ordered by displayOrder then lastName ────────
|
||||||
|
|
||||||
|
@Query("SELECT p FROM Professor p WHERE p.status = :status ORDER BY p.displayOrder ASC, p.lastName ASC")
|
||||||
|
Page<Professor> findByStatus(@Param("status") WorkingStatus status, Pageable pageable);
|
||||||
|
|
||||||
|
@Query("SELECT p FROM Professor p WHERE p.category = :category ORDER BY p.displayOrder ASC, p.lastName ASC")
|
||||||
|
Page<Professor> findByCategory(@Param("category") ProfessorCategory category, Pageable pageable);
|
||||||
|
|
||||||
|
@Query("SELECT p FROM Professor p WHERE p.status = :status AND p.category = :category ORDER BY p.displayOrder ASC, p.lastName ASC")
|
||||||
|
Page<Professor> findByStatusAndCategory(@Param("status") WorkingStatus status,
|
||||||
|
@Param("category") ProfessorCategory category,
|
||||||
|
Pageable pageable);
|
||||||
|
|
||||||
|
// ── Bulk display-order update ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets displayOrder for a single professor identified by professorId.
|
||||||
|
* Used by the bulk-reorder service method.
|
||||||
|
*/
|
||||||
|
@Modifying
|
||||||
|
@Query("UPDATE Professor p SET p.displayOrder = :displayOrder WHERE p.professorId = :professorId")
|
||||||
|
void updateDisplayOrder(@Param("professorId") UUID professorId,
|
||||||
|
@Param("displayOrder") int displayOrder);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all professors ordered by displayOrder ASC, then lastName ASC.
|
||||||
|
* Used by the admin reorder endpoint to return the updated list.
|
||||||
|
*/
|
||||||
|
@Query("SELECT p FROM Professor p ORDER BY p.displayOrder ASC, p.lastName ASC")
|
||||||
|
List<Professor> findAllOrderedByDisplayOrder();
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
@ -18,7 +18,6 @@ public class EmailService {
|
|||||||
private final Environment environment;
|
private final Environment environment;
|
||||||
|
|
||||||
public void sendNewPasswordEmail(String firstName, String password, String email) {
|
public void sendNewPasswordEmail(String firstName, String password, String email) {
|
||||||
|
|
||||||
// Create a Simple MailMessage.
|
// Create a Simple MailMessage.
|
||||||
SimpleMailMessage message = new SimpleMailMessage();
|
SimpleMailMessage message = new SimpleMailMessage();
|
||||||
message.setTo(email);
|
message.setTo(email);
|
||||||
@ -35,7 +34,20 @@ public class EmailService {
|
|||||||
message.setSubject(EMAIL_SUBJECT);
|
message.setSubject(EMAIL_SUBJECT);
|
||||||
message.setText("Hello " + firstName + "!\n\nYour new account password is: " + password + "\n\nThe Support Team");
|
message.setText("Hello " + firstName + "!\n\nYour new account password is: " + password + "\n\nThe Support Team");
|
||||||
|
|
||||||
// Send Message!
|
// Log the email details before sending
|
||||||
this.emailSender.send(message);
|
log.info("Preparing to send email:");
|
||||||
|
log.info("From: {}", message.getFrom());
|
||||||
|
log.info("To: {}", String.join(", ", message.getTo()));
|
||||||
|
log.info("Cc: {}", String.join(", ", message.getCc()));
|
||||||
|
log.info("Subject: {}", message.getSubject());
|
||||||
|
log.info("Text: {}", message.getText());
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Send Message!
|
||||||
|
this.emailSender.send(message);
|
||||||
|
log.info("Email successfully sent to {}", email);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to send email to {}. Error: {}", email, e.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
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;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface ProfessorService {
|
||||||
|
|
||||||
|
Professor register(String firstName, String lastName, String email, String department, String position);
|
||||||
|
|
||||||
|
Page<Professor> findAll(Pageable pageable);
|
||||||
|
|
||||||
|
Professor findByEmail(String email);
|
||||||
|
|
||||||
|
Professor findByProfessorId(UUID professorId);
|
||||||
|
|
||||||
|
Professor addNewProfessor(ProfessorDto professorDto);
|
||||||
|
|
||||||
|
Professor updateProfessor(UUID professorId, ProfessorDto professorDto);
|
||||||
|
|
||||||
|
void deleteProfessor(UUID professorId);
|
||||||
|
|
||||||
|
Professor updateProfileImage(UUID professorId, MultipartFile profileImage);
|
||||||
|
|
||||||
|
byte[] getImageByProfessorId(UUID professorId, String filename);
|
||||||
|
|
||||||
|
byte[] getDefaultProfileImage(UUID professorId);
|
||||||
|
|
||||||
|
Page<Professor> findActiveProfessors(Pageable pageable);
|
||||||
|
|
||||||
|
Page<Professor> findByCategory(ProfessorCategory category, Pageable pageable);
|
||||||
|
|
||||||
|
Page<Professor> findActiveProfessorsByCategory(ProfessorCategory category, Pageable pageable);
|
||||||
|
|
||||||
|
Professor findProfessorWithDetailsById(UUID professorId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bulk-update displayOrder for all professors.
|
||||||
|
*
|
||||||
|
* @param orderedIds Professor UUIDs in the desired display order (index 0 = first).
|
||||||
|
* @return All professors sorted by their new displayOrder.
|
||||||
|
*/
|
||||||
|
List<Professor> updateDisplayOrder(List<UUID> orderedIds);
|
||||||
|
}
|
||||||
@ -0,0 +1,346 @@
|
|||||||
|
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.NotAnImageFileException;
|
||||||
|
import net.shyshkin.study.fullstack.supportportal.backend.exception.domain.ProfessorNotFoundException;
|
||||||
|
import net.shyshkin.study.fullstack.supportportal.backend.mapper.ProfessorMapper;
|
||||||
|
import net.shyshkin.study.fullstack.supportportal.backend.repository.ProfessorRepository;
|
||||||
|
|
||||||
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.http.RequestEntity;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
|
||||||
|
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
|
import javax.transaction.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
import static net.shyshkin.study.fullstack.supportportal.backend.constant.FileConstant.*;
|
||||||
|
import static org.springframework.http.MediaType.*;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ProfessorServiceImpl implements ProfessorService {
|
||||||
|
|
||||||
|
public static final String EMAIL_NOT_FOUND_MSG = "Professor with email `%s` not found";
|
||||||
|
public static final String PROFESSOR_NOT_FOUND_MSG = "Professor not found";
|
||||||
|
|
||||||
|
private final ProfessorRepository professorRepository;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
private final EmailService emailService;
|
||||||
|
private final ProfessorMapper professorMapper;
|
||||||
|
private final ProfileImageService profileImageService;
|
||||||
|
private final RestTemplateBuilder restTemplateBuilder;
|
||||||
|
|
||||||
|
private RestTemplate restTemplate;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
void init() {
|
||||||
|
restTemplate = restTemplateBuilder
|
||||||
|
.rootUri(TEMP_PROFILE_IMAGE_BASE_URL)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalDateTime parseJoinDate(String joinDate) {
|
||||||
|
if (joinDate == null || joinDate.isBlank()) {
|
||||||
|
return LocalDateTime.now();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return ZonedDateTime.parse(joinDate, DateTimeFormatter.ISO_DATE_TIME)
|
||||||
|
.toLocalDateTime();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Could not parse joinDate '{}', defaulting to now()", joinDate);
|
||||||
|
return LocalDateTime.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public Professor register(String firstName, String lastName, String email, String department, String position) {
|
||||||
|
ProfessorDto professorDto = ProfessorDto.builder()
|
||||||
|
.firstName(firstName)
|
||||||
|
.lastName(lastName)
|
||||||
|
.email(email)
|
||||||
|
.department(department)
|
||||||
|
.position(position)
|
||||||
|
.build();
|
||||||
|
return addNewProfessor(professorDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateDefaultProfileImageUrl(UUID professorId) {
|
||||||
|
return ServletUriComponentsBuilder.fromCurrentContextPath()
|
||||||
|
.path(String.format(DEFAULT_PROFESSOR_IMAGE_URI_PATTERN, professorId))
|
||||||
|
.toUriString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateProfileImageUrl(UUID professorId) {
|
||||||
|
return ServletUriComponentsBuilder.fromCurrentContextPath()
|
||||||
|
.path(String.format(DEFAULT_PROFESSOR_IMAGE_URI_PATTERN, professorId))
|
||||||
|
.pathSegment(PROFESSOR_IMAGE_FILENAME)
|
||||||
|
.toUriString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Page<Professor> findAll(Pageable pageable) {
|
||||||
|
return professorRepository.findAll(pageable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Professor findByEmail(String email) {
|
||||||
|
return professorRepository
|
||||||
|
.findByEmail(email)
|
||||||
|
.orElseThrow(() -> new ProfessorNotFoundException(String.format(EMAIL_NOT_FOUND_MSG, email)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Professor findByProfessorId(UUID professorId) {
|
||||||
|
return professorRepository
|
||||||
|
.findByProfessorId(professorId)
|
||||||
|
.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;
|
||||||
|
|
||||||
|
if (!List.of(IMAGE_JPEG_VALUE, IMAGE_GIF_VALUE, IMAGE_PNG_VALUE).contains(profileImage.getContentType())) {
|
||||||
|
throw new NotAnImageFileException(profileImage.getOriginalFilename() + " is not an image file. Please upload an image");
|
||||||
|
}
|
||||||
|
|
||||||
|
String imageUrl = profileImageService.persistProfileImage(professor.getProfessorId(), profileImage, PROFESSOR_IMAGE_FILENAME);
|
||||||
|
|
||||||
|
if (imageUrl == null)
|
||||||
|
imageUrl = generateProfileImageUrl(professor.getProfessorId());
|
||||||
|
|
||||||
|
professor.setProfileImageUrl(imageUrl);
|
||||||
|
professorRepository.save(professor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearProfessorStorage(Professor professor) {
|
||||||
|
profileImageService.clearUserStorage(professor.getProfessorId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private UUID generateUuid() {
|
||||||
|
return UUID.randomUUID();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public Professor addNewProfessor(ProfessorDto professorDto) {
|
||||||
|
Professor professor = professorMapper.toEntity(professorDto);
|
||||||
|
|
||||||
|
professor.setProfessorId(generateUuid());
|
||||||
|
professor.setJoinDate(parseJoinDate(professorDto.getJoinDate()));
|
||||||
|
professor.setProfileImageUrl(generateDefaultProfileImageUrl(professor.getProfessorId()));
|
||||||
|
|
||||||
|
// New professors get displayOrder = 0 by default (appear first).
|
||||||
|
// Admins can re-order via the drag-and-drop endpoint.
|
||||||
|
if (professor.getDisplayOrder() == null) {
|
||||||
|
professor.setDisplayOrder(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Professor savedProfessor = professorRepository.save(professor);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
Professor finalProfessor = professorRepository.save(savedProfessor);
|
||||||
|
|
||||||
|
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 ProfessorNotFoundException("Professor not found with id: " + professorId));
|
||||||
|
|
||||||
|
professor.setFirstName(professorDto.getFirstName());
|
||||||
|
professor.setLastName(professorDto.getLastName());
|
||||||
|
professor.setEmail(professorDto.getEmail());
|
||||||
|
professor.setDepartment(professorDto.getDepartment());
|
||||||
|
professor.setPosition(professorDto.getPosition());
|
||||||
|
professor.setOfficeLocation(professorDto.getOfficeLocation());
|
||||||
|
professor.setStatus(professorDto.getStatus());
|
||||||
|
professor.setCategory(professorDto.getCategory());
|
||||||
|
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());
|
||||||
|
// displayOrder is intentionally NOT updated here — only via the reorder endpoint.
|
||||||
|
|
||||||
|
if (professorDto.getJoinDate() != null && !professorDto.getJoinDate().isBlank()) {
|
||||||
|
professor.setJoinDate(parseJoinDate(professorDto.getJoinDate()));
|
||||||
|
}
|
||||||
|
|
||||||
|
final Professor professorRef = professor;
|
||||||
|
|
||||||
|
if (professor.getSkills() == null) professor.setSkills(new HashSet<>());
|
||||||
|
professor.getSkills().clear();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (professor.getAwards() == null) professor.setAwards(new HashSet<>());
|
||||||
|
professor.getAwards().clear();
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (professorDto.getProfileImage() != null) {
|
||||||
|
saveProfileImage(savedProfessor, professorDto.getProfileImage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return savedProfessor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteProfessor(UUID professorId) {
|
||||||
|
Professor professorToBeDeleted = professorRepository
|
||||||
|
.findByProfessorId(professorId)
|
||||||
|
.orElseThrow(() -> new ProfessorNotFoundException(PROFESSOR_NOT_FOUND_MSG));
|
||||||
|
|
||||||
|
clearProfessorStorage(professorToBeDeleted);
|
||||||
|
professorRepository.delete(professorToBeDeleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Professor updateProfileImage(UUID professorId, MultipartFile profileImage) {
|
||||||
|
Professor professor = findByProfessorId(professorId);
|
||||||
|
saveProfileImage(professor, profileImage);
|
||||||
|
return professor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getImageByProfessorId(UUID professorId, String filename) {
|
||||||
|
if (!professorRepository.existsByProfessorId(professorId)) {
|
||||||
|
throw new ProfessorNotFoundException(PROFESSOR_NOT_FOUND_MSG);
|
||||||
|
}
|
||||||
|
return profileImageService.retrieveProfileImage(professorId, filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getDefaultProfileImage(UUID professorId) {
|
||||||
|
if (!professorRepository.existsByProfessorId(professorId)) {
|
||||||
|
throw new ProfessorNotFoundException(PROFESSOR_NOT_FOUND_MSG);
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestEntity<Void> requestEntity = RequestEntity
|
||||||
|
.get("/{professorId}", professorId)
|
||||||
|
.accept(IMAGE_JPEG)
|
||||||
|
.build();
|
||||||
|
var responseEntity = restTemplate.exchange(requestEntity, new ParameterizedTypeReference<byte[]>() {});
|
||||||
|
return responseEntity.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persists a new displayOrder for every professor in the given list.
|
||||||
|
* The list position (index) becomes the professor's displayOrder value.
|
||||||
|
*
|
||||||
|
* @param orderedIds UUIDs in desired display order; index 0 → displayOrder=0, etc.
|
||||||
|
* @return All professors sorted by their updated displayOrder.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public List<Professor> updateDisplayOrder(List<UUID> orderedIds) {
|
||||||
|
IntStream.range(0, orderedIds.size())
|
||||||
|
.forEach(i -> professorRepository.updateDisplayOrder(orderedIds.get(i), i));
|
||||||
|
return professorRepository.findAllOrderedByDisplayOrder();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package net.shyshkin.study.fullstack.supportportal.backend.service;
|
package net.shyshkin.study.fullstack.supportportal.backend.service;
|
||||||
|
|
||||||
import com.amazonaws.AmazonServiceException;
|
import com.amazonaws.AmazonServiceException;
|
||||||
import com.amazonaws.SdkClientException;
|
import com.amazonaws.SdkClientException;
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
@ -35,3 +35,5 @@ public interface UserService extends UserDetailsService {
|
|||||||
|
|
||||||
byte[] getDefaultProfileImage(UUID userId);
|
byte[] getDefaultProfileImage(UUID userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import net.shyshkin.study.fullstack.supportportal.backend.domain.UserPrincipal;
|
|||||||
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.UserDto;
|
import net.shyshkin.study.fullstack.supportportal.backend.domain.dto.UserDto;
|
||||||
import net.shyshkin.study.fullstack.supportportal.backend.exception.domain.*;
|
import net.shyshkin.study.fullstack.supportportal.backend.exception.domain.*;
|
||||||
import net.shyshkin.study.fullstack.supportportal.backend.mapper.UserMapper;
|
import net.shyshkin.study.fullstack.supportportal.backend.mapper.UserMapper;
|
||||||
|
import net.shyshkin.study.fullstack.supportportal.backend.repository.ProfessorRepository;
|
||||||
import net.shyshkin.study.fullstack.supportportal.backend.repository.UserRepository;
|
import net.shyshkin.study.fullstack.supportportal.backend.repository.UserRepository;
|
||||||
import org.apache.commons.lang3.RandomStringUtils;
|
import org.apache.commons.lang3.RandomStringUtils;
|
||||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||||
@ -45,6 +46,7 @@ public class UserServiceImpl implements UserService {
|
|||||||
public static final String EMAIL_EXISTS_MSG = "User with email `%s` is already registered";
|
public static final String EMAIL_EXISTS_MSG = "User with email `%s` is already registered";
|
||||||
|
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
|
private final ProfessorRepository professorRepository;
|
||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
private final LoginAttemptService loginAttemptService;
|
private final LoginAttemptService loginAttemptService;
|
||||||
private final EmailService emailService;
|
private final EmailService emailService;
|
||||||
@ -264,10 +266,14 @@ public class UserServiceImpl implements UserService {
|
|||||||
@Override
|
@Override
|
||||||
public byte[] getDefaultProfileImage(UUID userId) {
|
public byte[] getDefaultProfileImage(UUID userId) {
|
||||||
|
|
||||||
if (!userRepository.existsByUserId(userId)) {
|
if (!userRepository.existsByUserId(userId) && !professorRepository.existsByProfessorId(userId)) {
|
||||||
throw new UserNotFoundException(USER_NOT_FOUND_MSG);
|
throw new UserNotFoundException(USER_NOT_FOUND_MSG);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if (!professorRepository.existsByProfessorId(userId)) {
|
||||||
|
// throw new UserNotFoundException(USER_NOT_FOUND_MSG);
|
||||||
|
// }
|
||||||
|
|
||||||
// "https://robohash.org/11951691-d373-4126-bef2-84d157a6546b"
|
// "https://robohash.org/11951691-d373-4126-bef2-84d157a6546b"
|
||||||
RequestEntity<Void> requestEntity = RequestEntity
|
RequestEntity<Void> requestEntity = RequestEntity
|
||||||
.get("/{userId}", userId)
|
.get("/{userId}", userId)
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user