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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 47 additions & 11 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,20 +1,56 @@
plugins {
id("java")
java
id("org.springframework.boot") version "3.3.5"
id("io.spring.dependency-management") version "1.1.6"
}

group = "org.lab"
version = "1.0-SNAPSHOT"
group = "com.example"
version = "0.0.1"

repositories {
mavenCentral()
}

dependencies {
testImplementation(platform("org.junit:junit-bom:5.10.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.projectlombok:lombok")
compileOnly("org.projectlombok:lombok")
annotationProcessor("org.projectlombok:lombok")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("io.jsonwebtoken:jjwt-api:0.12.6")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.6")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.6")

testImplementation("org.springframework.security:spring-security-test")
developmentOnly("org.springframework.boot:spring-boot-devtools")
testImplementation("org.springframework.boot:spring-boot-starter-test")
runtimeOnly("org.postgresql:postgresql")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")

implementation("org.json:json:20231013")
testImplementation("org.junit.jupiter:junit-jupiter:5.9.1")
}

java {
toolchain {
languageVersion = JavaLanguageVersion.of(23)
}
}

tasks.withType<Test>().configureEach {
useJUnitPlatform()

testLogging {
events("passed", "skipped", "failed")
}
}

tasks.withType<JavaCompile>().configureEach {
options.compilerArgs.add("--enable-preview")
}

tasks.withType<Test>().configureEach {
jvmArgs("--enable-preview")
}

tasks.test {
useJUnitPlatform()
tasks.withType<JavaExec>().configureEach {
jvmArgs("--enable-preview")
}
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
kotlin.code.style=official
Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
5 changes: 3 additions & 2 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#Thu Nov 27 19:51:20 MSK 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
8 changes: 7 additions & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
rootProject.name = "features"
rootProject.name = "features"

dependencyResolutionManagement {
repositories {
mavenCentral()
}
}
13 changes: 13 additions & 0 deletions src/main/java/com/example/Application.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

}
65 changes: 65 additions & 0 deletions src/main/java/com/example/config/JwtAuthenticationFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.example.config;

import com.example.services.JwtService;
import com.example.services.UserService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
public static final String BEARER_PREFIX = "Bearer ";
public static final String HEADER_NAME = "Authorization";
private final JwtService jwtService;
private final UserService userService;

@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {

var authHeader = request.getHeader(HEADER_NAME);
if (StringUtils.isEmpty(authHeader) || !StringUtils.startsWithIgnoreCase(authHeader, BEARER_PREFIX)) {
filterChain.doFilter(request, response);
return;
}

var jwt = authHeader.substring(BEARER_PREFIX.length());
var username = jwtService.extractUserName(jwt);

if (!StringUtils.isEmpty(username) && SecurityContextHolder.getContext().getAuthentication() == null) {
var userDetails = userService
.userDetailsService()
.loadUserByUsername(username);

if (jwtService.isTokenValid(jwt, userDetails)) {
var context = SecurityContextHolder.createEmptyContext();

var authToken = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);

authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
context.setAuthentication(authToken);
SecurityContextHolder.setContext(context);
}
}
filterChain.doFilter(request, response);
}
}
64 changes: 64 additions & 0 deletions src/main/java/com/example/config/SecurityConfiguration.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.example.config;

import com.example.services.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS;

@Configuration
@EnableWebSecurity(debug = true)
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfiguration {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final UserService userService;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.cors(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(request -> request
.requestMatchers("/users/login", "/users/register", "/error").permitAll()

.anyRequest().authenticated()
)
.sessionManagement(manager -> manager.sessionCreationPolicy(STATELESS))
.authenticationProvider(authenticationProvider())
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userService.userDetailsService());
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config)
throws Exception {
return config.getAuthenticationManager();
}
}
52 changes: 52 additions & 0 deletions src/main/java/com/example/controllers/BugReportController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.example.controllers;

import com.example.dto.project.BugReportDto;
import com.example.dto.project.StatusDto;
import com.example.model.enums.BugReportStatus;
import com.example.services.ProjectService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;

import java.util.Arrays;
import java.util.List;

@RestController
@RequestMapping("/bugreports")
@RequiredArgsConstructor
public class BugReportController {

private final ProjectService projectService;

@GetMapping
public List<BugReportDto> getBugReports(@AuthenticationPrincipal UserDetails user) {
return projectService.findBugReportsForUser(user.getUsername()).stream()
.map(br ->
new BugReportDto(
br.getId(),
br.getDescription(),
br.getStatus()
)
)
.toList();
}

@PutMapping("/{bugReportId}/status")
public void updateStatus(
@PathVariable("bugReportId") Long bugReportId,
@RequestBody StatusDto statusDto,
@AuthenticationPrincipal UserDetails userDetails
) {
projectService.updateBugReportStatus(bugReportId, toBugReportStatus(statusDto), userDetails.getUsername());
}

private BugReportStatus toBugReportStatus(StatusDto statusDto) {
return Arrays.stream(BugReportStatus.values())
.filter(it -> it.getValue().equals(statusDto.status()))
.findAny()
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "status not found"));
}
}
42 changes: 42 additions & 0 deletions src/main/java/com/example/controllers/MilestoneController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.example.controllers;

import com.example.dto.project.StatusDto;
import com.example.dto.ticket.CreateTicketDto;
import com.example.dto.ticket.TicketIdDto;
import com.example.model.enums.MilestoneStatus;
import com.example.services.ProjectService;
import com.example.services.TicketService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;

import java.util.Arrays;

@RestController
@RequestMapping("/milestones")
@RequiredArgsConstructor
public class MilestoneController {

private final TicketService ticketService;
private final ProjectService projectService;

@PostMapping("/{milestoneId}/tickets")
public ResponseEntity<TicketIdDto> createTicket(@RequestBody CreateTicketDto createTicketDto, @PathVariable Long milestoneId) {
var ticket = ticketService.createTicket(createTicketDto, milestoneId);
return new ResponseEntity<>(new TicketIdDto(ticket.getId().toString()), HttpStatus.CREATED);
}

@PutMapping("/{milestoneId}/status")
public void updateStatus(@PathVariable Long milestoneId, @RequestBody StatusDto statusDto) {
projectService.updateMilestoneStatus(milestoneId, toMilestoneStatus(statusDto));
}

private MilestoneStatus toMilestoneStatus(StatusDto statusDto) {
return Arrays.stream(MilestoneStatus.values())
.filter(it -> it.getValue().equals(statusDto.status()))
.findAny()
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "status not found"));
}
}
Loading