Skip to content
Merged
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
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ this project is inspired by the structure of Kanban boards and tools like Trello

## ✨ Key Features
* **Visual Kanban Workflow –** Organize your learning in BACKLOG, IN PROGRESS, and MASTERED.
* **Identity & Account Management (New!) –** Secure user registration, authentication, and personalized profile management.
* **Intuitive Skill Tracking –** Easily add new topics and document your journey towards mastery.
* **Smart Filtering & Pagination –** Quickly find what you need with real-time filtering and dynamic "Load More" functionality.
* **Resource Management –** Keep essential documentation, source links and notes attached to every skill for quick reference.

## 🛠️ Tech Stack
* **Backend –** Java 25 & Spring Boot 4.0
* **Backend –** Java 25, Spring Boot 4.0, and Spring Security (Authentication & BCrypt Hashing)
* **Frontend –** Thymeleaf, Vanilla JavaScript, Modern CSS
* **Database –** PostgreSQL
* **Data Handling –** Spring Data JPA with Pagination support
Expand All @@ -36,11 +37,13 @@ this project is inspired by the structure of Kanban boards and tools like Trello

## 🗺️ Upcoming Features:
* **Drag & Drop –** Move skill cards between columns for a more dynamic and interactive experience.
* **User Authentication –** Personal accounts to secure your data and enable private roadmaps.


## 🚀 Quick Start
1. Clone the repository.
2. Run `docker-compose up`.
3. Open `http://localhost:8080` in your browser.
4. **Explore –** The app comes pre-loaded with sample data to help you get started right away!
4. **Log in or Sign up –** You can create a new account instantly, or use the pre-loaded developer profile:
* **Username:** `test`
* **Password:** `secret`
5. Let your journey begin!
Comment on lines +46 to +49

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Test credentials documented in README.

Documenting the test/secret credentials is convenient for development, but ensure this seeded account is removed or disabled before any production deployment. Consider noting that these credentials are for local development only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 46 - 49, The README currently documents the seeded
test account credentials, so update the login/signup section to clearly state
that the test/secret profile is for local development only. Make sure the
wording around the pre-loaded developer profile in the README warns that this
account must be removed or disabled before any production deployment.

13 changes: 13 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,19 @@
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>
</dependencies>

<build>
Expand Down
Binary file modified screenshots/app-preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package org.example.devroadmapskilltracker.config;

import org.example.devroadmapskilltracker.skill.Skill;
import org.example.devroadmapskilltracker.skill.SkillRepository;
import org.example.devroadmapskilltracker.skill.SkillStatus;
import org.example.devroadmapskilltracker.user.User;
import org.example.devroadmapskilltracker.user.UserRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.slf4j.LoggerFactory;
import org.slf4j.Logger;
import org.springframework.context.annotation.Profile;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.util.List;

@Configuration
public class DataInitializer {

private static final Logger logger = LoggerFactory.getLogger(DataInitializer.class);
private final PasswordEncoder passwordEncoder;

public DataInitializer(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}

@Bean
@Profile("!prod")
CommandLineRunner initDatabase(SkillRepository skillRepository, UserRepository userRepository) {
return args -> {

if (userRepository.count() == 0) {
logger.info("No skill found. Generating test user and skills...");

User testUser = new User();
testUser.setFullName("Test Developer");
testUser.setUsername("test");
testUser.setPassword(passwordEncoder.encode("secret"));

User savedUser = userRepository.save(testUser);

skillRepository.saveAll(List.of(
// BACKLOG
new Skill("Docker", "Learn containerization and how to manage images.","DevOps", SkillStatus.BACKLOG, savedUser),
new Skill("TypeScript", "Strongly typed JavaScript for better scaling.","Frontend", SkillStatus.BACKLOG,savedUser),

// IN PROGRESS
new Skill("Spring Boot", "Building robust backend services with Java.","Backend", SkillStatus.IN_PROGRESS,savedUser),
new Skill("Thymeleaf", "Server-side template engine for modern web apps.","Web", SkillStatus.IN_PROGRESS, savedUser),
new Skill("CSS Grid", "Mastering complex layouts with grid systems.","Design", SkillStatus.IN_PROGRESS, savedUser),

// MASTERED
new Skill("Java Fundamentals", "Core syntax, OOP, and collections.", "Backend", SkillStatus.MASTERED, savedUser),
new Skill("REST APIs", "Designing and implementing scalable endpoints." ,"Backend", SkillStatus.MASTERED,savedUser)

));

logger.info("🚀Test data has been loaded!");
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package org.example.devroadmapskilltracker.config;

import org.example.devroadmapskilltracker.user.UserRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

private final UserRepository userRepository;

public SecurityConfig(UserRepository userRepository) {
this.userRepository = userRepository;
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) {
http.csrf(csrf -> csrf.disable())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Re-enable CSRF protection for form-based authentication.

Disabling CSRF protection exposes all state-changing operations (skill create/update/delete, account changes) to cross-site request forgery. Since this application uses Thymeleaf with thymeleaf-extras-springsecurity6, CSRF tokens are automatically injected into forms that use th:action, making re-enabling straightforward.

🔒 Proposed fix to re-enable CSRF protection
-       http.csrf(csrf -> csrf.disable())
-               .authorizeHttpRequests(auth -> auth
+       http
+               .authorizeHttpRequests(auth -> auth

Ensure all Thymeleaf forms use th:action instead of plain action so the CSRF token is automatically included:

<!-- Use this -->
<form th:action="@{/skills}" method="post">
<!-- Instead of this -->
<form action="/skills" method="post">
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/org/example/devroadmapskilltracker/config/SecurityConfig.java`
at line 27, CSRF is currently disabled in SecurityConfig via http.csrf(csrf ->
csrf.disable()), which leaves form-based state-changing actions unprotected.
Re-enable CSRF in the security configuration and make sure Thymeleaf forms that
submit to controllers use th:action (not plain action) so Spring Security can
inject the token automatically; verify the affected form templates and keep the
change centered around SecurityConfig and any Thymeleaf form submissions.

Source: Linters/SAST tools

.authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/static/**", "/css/**", "/assets/**").permitAll()
.requestMatchers("/signup","/createAccount").permitAll()
.anyRequest().authenticated())

.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/skills")
.permitAll())
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.permitAll());


return http.build();
}

@Bean
public UserDetailsService userDetailsService() {
return username -> userRepository.findByUsername(username)
.map(user -> User
.withUsername(user.getUsername())
.password(user.getPassword())
.roles("USER")
.build())
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
}


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

This file was deleted.

21 changes: 18 additions & 3 deletions src/main/java/org/example/devroadmapskilltracker/skill/Skill.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import org.example.devroadmapskilltracker.user.User;
import org.hibernate.validator.constraints.URL;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
Expand All @@ -22,7 +23,7 @@ public class Skill {

@NotBlank(message = "A title is required") private String title;

@NotNull(message = "Status is required") @Enumerated(EnumType.STRING) // Sparar texten (ex. "BACKLOG") istället för en siffra i databasen
@NotNull(message = "Status is required") @Enumerated(EnumType.STRING)
private SkillStatus status;

@NotBlank(message = "A description is required") @Column(columnDefinition = "TEXT")
Expand All @@ -42,16 +43,21 @@ public class Skill {
@Column(name = "completed_at")
private LocalDateTime completedAt;

@NotBlank(message = "A tag is required") private String tag; // Ex: "Databas", "Testning", "Ramverk"
@NotBlank(message = "A tag is required") private String tag;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;

public Skill() {}


public Skill(String title, String description, String tag, SkillStatus status) {
public Skill(String title, String description, String tag, SkillStatus status, User user) {
this.title = title;
this.description = description;
this.tag = tag;
this.status = status;
this.user = user;
}

// Constructor used for tests
Expand Down Expand Up @@ -135,6 +141,14 @@ public void setCompletedAt(LocalDateTime completedAt) {
this.completedAt = completedAt;
}

public User getUser() {
return user;
}

public void setUser(User user) {
this.user = user;
}

@Override
public boolean equals(Object o) {
if (!(o instanceof Skill skill)) return false;
Expand All @@ -158,6 +172,7 @@ public String toString() {
", updatedAt=" + updatedAt +
", completedAt=" + completedAt +
", tag='" + tag + '\'' +
", user=" + user +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,30 @@

import org.springframework.data.domain.Page;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.stereotype.Repository;
import org.springframework.data.domain.Pageable;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Repository
public interface SkillRepository extends JpaRepository<Skill, Long> {

// Standardmetoden --> Finns i JpaRepository, deklarerad för tydlighet
Page<Skill> findAll(Pageable pageable);
@Modifying
@Transactional
void deleteByUserId(Long userId);

boolean existsByTitle(String title);
Page<Skill> findAllByUserId(Long userid,Pageable pageable);

// Kombinerad sökning --> Title + Tag
Page<Skill> findByTitleContainingIgnoreCaseOrTagIgnoreCase(String title, String tag,Pageable pageable);
boolean existsByTitleAndUserId(String title, Long userId);

// Filtrera på Status
Page<Skill> findByStatus(SkillStatus status, Pageable pageable);
Page<Skill> findByTitleContainingIgnoreCaseAndUserId(String title, Long userId, Pageable pageable);

// Specifik tagg-sökning -> För framtida behov?
Page<Skill> findByTagContainingIgnoreCase(String tag,Pageable pageable);
Page<Skill> findByTitleContainingIgnoreCaseOrTagIgnoreCaseAndUserId(String title, String tag, Long userId, Pageable pageable);

Optional<Skill> findByTitleIgnoreCaseAndUserId(String title, Long userId);

Page<Skill> findByTitleContainingIgnoreCase(String title, Pageable pageable);

Optional<Skill> findByTitleIgnoreCase(String title);

}
Loading