Backend project initialized
All checks were successful
Backend CI / Run Maven Tests (push) Successful in 27s
All checks were successful
Backend CI / Run Maven Tests (push) Successful in 27s
Co-authored-by: Jarno Kiesiläinen <jarnokie@gmail.com> Co-committed-by: Jarno Kiesiläinen <jarnokie@gmail.com>
This commit was merged in pull request #4.
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
package com.vibing.backend;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* Main Spring Boot application class for the Vibing backend service.
|
||||
* This class serves as the entry point for the REST API backend.
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class VibingBackendApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(VibingBackendApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.vibing.backend.config;
|
||||
|
||||
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.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
/**
|
||||
* Security configuration for the Vibing backend application.
|
||||
* This is a basic configuration that can be extended for production use.
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
/**
|
||||
* Configure security filter chain.
|
||||
* For development purposes, this allows all requests.
|
||||
* In production, you should implement proper authentication and authorization.
|
||||
*
|
||||
* @param http the HttpSecurity object
|
||||
* @return SecurityFilterChain
|
||||
* @throws Exception if configuration fails
|
||||
*/
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(AbstractHttpConfigurer::disable) // Disable CSRF for REST APIa
|
||||
.authorizeHttpRequests(authz -> authz.anyRequest().permitAll())
|
||||
.headers(headers -> headers.frameOptions().disable()); // Allow H2 console frames
|
||||
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.vibing.backend.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Health check controller for monitoring the application status.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/health")
|
||||
@Tag(name = "Health Check", description = "APIs for checking application health")
|
||||
public class HealthController {
|
||||
|
||||
/**
|
||||
* Get the health status of the application.
|
||||
*
|
||||
* @return ResponseEntity containing health information
|
||||
*/
|
||||
@GetMapping
|
||||
@Operation(summary = "Health check", description = "Check if the application is running")
|
||||
public ResponseEntity<Map<String, Object>> healthCheck() {
|
||||
Map<String, Object> healthInfo = new HashMap<>();
|
||||
healthInfo.put("status", "UP");
|
||||
healthInfo.put("timestamp", LocalDateTime.now());
|
||||
healthInfo.put("service", "Vibing Backend");
|
||||
healthInfo.put("version", "1.0.0");
|
||||
|
||||
return ResponseEntity.ok(healthInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a simple ping response.
|
||||
*
|
||||
* @return ResponseEntity with "pong" message
|
||||
*/
|
||||
@GetMapping("/ping")
|
||||
@Operation(summary = "Ping", description = "Simple ping endpoint")
|
||||
public ResponseEntity<String> ping() {
|
||||
return ResponseEntity.ok("pong");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package com.vibing.backend.controller;
|
||||
|
||||
import com.vibing.backend.model.User;
|
||||
import com.vibing.backend.service.UserService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* REST controller for User-related operations.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/users")
|
||||
@Tag(name = "User Management", description = "APIs for managing users")
|
||||
@CrossOrigin(origins = "*") // Configure this properly for production
|
||||
public class UserController {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
@Autowired
|
||||
public UserController(UserService userService) {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all users.
|
||||
*
|
||||
* @return ResponseEntity containing list of users
|
||||
*/
|
||||
@GetMapping
|
||||
@Operation(summary = "Get all users", description = "Retrieve a list of all users")
|
||||
public ResponseEntity<List<User>> getAllUsers() {
|
||||
List<User> users = userService.getAllUsers();
|
||||
return ResponseEntity.ok(users);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user by ID.
|
||||
*
|
||||
* @param id the user ID
|
||||
* @return ResponseEntity containing the user if found
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
@Operation(summary = "Get user by ID", description = "Retrieve a specific user by their ID")
|
||||
public ResponseEntity<User> getUserById(@PathVariable Long id) {
|
||||
Optional<User> user = userService.getUserById(id);
|
||||
return user.map(ResponseEntity::ok)
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user by username.
|
||||
*
|
||||
* @param username the username
|
||||
* @return ResponseEntity containing the user if found
|
||||
*/
|
||||
@GetMapping("/username/{username}")
|
||||
@Operation(summary = "Get user by username", description = "Retrieve a specific user by their username")
|
||||
public ResponseEntity<User> getUserByUsername(@PathVariable String username) {
|
||||
Optional<User> user = userService.getUserByUsername(username);
|
||||
return user.map(ResponseEntity::ok)
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new user.
|
||||
*
|
||||
* @param user the user to create
|
||||
* @return ResponseEntity containing the created user
|
||||
*/
|
||||
@PostMapping
|
||||
@Operation(summary = "Create a new user", description = "Create a new user account")
|
||||
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
|
||||
try {
|
||||
User createdUser = userService.createUser(user);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(createdUser);
|
||||
} catch (RuntimeException e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing user.
|
||||
*
|
||||
* @param id the user ID
|
||||
* @param user the updated user data
|
||||
* @return ResponseEntity containing the updated user
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
@Operation(summary = "Update user", description = "Update an existing user's information")
|
||||
public ResponseEntity<User> updateUser(@PathVariable Long id, @Valid @RequestBody User user) {
|
||||
Optional<User> updatedUser = userService.updateUser(id, user);
|
||||
return updatedUser.map(ResponseEntity::ok)
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a user by ID.
|
||||
*
|
||||
* @param id the user ID
|
||||
* @return ResponseEntity indicating success or failure
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
@Operation(summary = "Delete user", description = "Delete a user by their ID")
|
||||
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
|
||||
boolean deleted = userService.deleteUser(id);
|
||||
return deleted ? ResponseEntity.noContent().build() : ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a username exists.
|
||||
*
|
||||
* @param username the username to check
|
||||
* @return ResponseEntity with boolean indicating existence
|
||||
*/
|
||||
@GetMapping("/check-username/{username}")
|
||||
@Operation(summary = "Check username availability", description = "Check if a username is already taken")
|
||||
public ResponseEntity<Boolean> checkUsernameExists(@PathVariable String username) {
|
||||
boolean exists = userService.userExistsByUsername(username);
|
||||
return ResponseEntity.ok(exists);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an email exists.
|
||||
*
|
||||
* @param email the email to check
|
||||
* @return ResponseEntity with boolean indicating existence
|
||||
*/
|
||||
@GetMapping("/check-email/{email}")
|
||||
@Operation(summary = "Check email availability", description = "Check if an email is already registered")
|
||||
public ResponseEntity<Boolean> checkEmailExists(@PathVariable String email) {
|
||||
boolean exists = userService.userExistsByEmail(email);
|
||||
return ResponseEntity.ok(exists);
|
||||
}
|
||||
}
|
||||
144
backend/src/main/java/com/vibing/backend/model/User.java
Normal file
144
backend/src/main/java/com/vibing/backend/model/User.java
Normal file
@@ -0,0 +1,144 @@
|
||||
package com.vibing.backend.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* User entity representing a user in the Vibing application.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
public class User {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@NotBlank(message = "Username is required")
|
||||
@Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
|
||||
@Column(unique = true, nullable = false)
|
||||
private String username;
|
||||
|
||||
@NotBlank(message = "Email is required")
|
||||
@Email(message = "Email should be valid")
|
||||
@Column(unique = true, nullable = false)
|
||||
private String email;
|
||||
|
||||
@NotBlank(message = "Password is required")
|
||||
@Size(min = 6, message = "Password must be at least 6 characters")
|
||||
@Column(nullable = false)
|
||||
private String password;
|
||||
|
||||
@Column(name = "first_name")
|
||||
private String firstName;
|
||||
|
||||
@Column(name = "last_name")
|
||||
private String lastName;
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
// Constructors
|
||||
public User() {}
|
||||
|
||||
public User(String username, String email, String password) {
|
||||
this.username = username;
|
||||
this.email = email;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public void setFirstName(String firstName) {
|
||||
this.firstName = firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
public void setLastName(String lastName) {
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "User{" +
|
||||
"id=" + id +
|
||||
", username='" + username + '\'' +
|
||||
", email='" + email + '\'' +
|
||||
", firstName='" + firstName + '\'' +
|
||||
", lastName='" + lastName + '\'' +
|
||||
", createdAt=" + createdAt +
|
||||
", updatedAt=" + updatedAt +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.vibing.backend.repository;
|
||||
|
||||
import com.vibing.backend.model.User;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Repository interface for User entity operations.
|
||||
*/
|
||||
@Repository
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
|
||||
/**
|
||||
* Find a user by username.
|
||||
*
|
||||
* @param username the username to search for
|
||||
* @return Optional containing the user if found
|
||||
*/
|
||||
Optional<User> findByUsername(String username);
|
||||
|
||||
/**
|
||||
* Find a user by email.
|
||||
*
|
||||
* @param email the email to search for
|
||||
* @return Optional containing the user if found
|
||||
*/
|
||||
Optional<User> findByEmail(String email);
|
||||
|
||||
/**
|
||||
* Check if a user exists by username.
|
||||
*
|
||||
* @param username the username to check
|
||||
* @return true if user exists, false otherwise
|
||||
*/
|
||||
boolean existsByUsername(String username);
|
||||
|
||||
/**
|
||||
* Check if a user exists by email.
|
||||
*
|
||||
* @param email the email to check
|
||||
* @return true if user exists, false otherwise
|
||||
*/
|
||||
boolean existsByEmail(String email);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.vibing.backend.service;
|
||||
|
||||
import com.vibing.backend.model.User;
|
||||
import com.vibing.backend.repository.UserRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Service class for User-related business operations.
|
||||
*/
|
||||
@Service
|
||||
public class UserService {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
public UserService(UserRepository userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all users.
|
||||
*
|
||||
* @return List of all users
|
||||
*/
|
||||
public List<User> getAllUsers() {
|
||||
return userRepository.findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user by ID.
|
||||
*
|
||||
* @param id the user ID
|
||||
* @return Optional containing the user if found
|
||||
*/
|
||||
public Optional<User> getUserById(Long id) {
|
||||
return userRepository.findById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user by username.
|
||||
*
|
||||
* @param username the username
|
||||
* @return Optional containing the user if found
|
||||
*/
|
||||
public Optional<User> getUserByUsername(String username) {
|
||||
return userRepository.findByUsername(username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user by email.
|
||||
*
|
||||
* @param email the email
|
||||
* @return Optional containing the user if found
|
||||
*/
|
||||
public Optional<User> getUserByEmail(String email) {
|
||||
return userRepository.findByEmail(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new user.
|
||||
*
|
||||
* @param user the user to create
|
||||
* @return the created user
|
||||
*/
|
||||
public User createUser(User user) {
|
||||
// Check if username already exists
|
||||
if (userRepository.existsByUsername(user.getUsername())) {
|
||||
throw new RuntimeException("Username already exists: " + user.getUsername());
|
||||
}
|
||||
|
||||
// Check if email already exists
|
||||
if (userRepository.existsByEmail(user.getEmail())) {
|
||||
throw new RuntimeException("Email already exists: " + user.getEmail());
|
||||
}
|
||||
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing user.
|
||||
*
|
||||
* @param id the user ID
|
||||
* @param user the updated user data
|
||||
* @return Optional containing the updated user if found
|
||||
*/
|
||||
public Optional<User> updateUser(Long id, User user) {
|
||||
return userRepository.findById(id)
|
||||
.map(existingUser -> {
|
||||
existingUser.setUsername(user.getUsername());
|
||||
existingUser.setEmail(user.getEmail());
|
||||
existingUser.setFirstName(user.getFirstName());
|
||||
existingUser.setLastName(user.getLastName());
|
||||
return userRepository.save(existingUser);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a user by ID.
|
||||
*
|
||||
* @param id the user ID
|
||||
* @return true if user was deleted, false if not found
|
||||
*/
|
||||
public boolean deleteUser(Long id) {
|
||||
if (userRepository.existsById(id)) {
|
||||
userRepository.deleteById(id);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user exists by username.
|
||||
*
|
||||
* @param username the username to check
|
||||
* @return true if user exists, false otherwise
|
||||
*/
|
||||
public boolean userExistsByUsername(String username) {
|
||||
return userRepository.existsByUsername(username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user exists by email.
|
||||
*
|
||||
* @param email the email to check
|
||||
* @return true if user exists, false otherwise
|
||||
*/
|
||||
public boolean userExistsByEmail(String email) {
|
||||
return userRepository.existsByEmail(email);
|
||||
}
|
||||
}
|
||||
62
backend/src/main/resources/application.yml
Normal file
62
backend/src/main/resources/application.yml
Normal file
@@ -0,0 +1,62 @@
|
||||
server:
|
||||
port: 8080
|
||||
servlet:
|
||||
context-path: /api
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: vibing-backend
|
||||
|
||||
# Database Configuration
|
||||
datasource:
|
||||
url: jdbc:h2:mem:testdb
|
||||
driver-class-name: org.h2.Driver
|
||||
username: sa
|
||||
password: password
|
||||
|
||||
# JPA Configuration
|
||||
jpa:
|
||||
database-platform: org.hibernate.dialect.H2Dialect
|
||||
hibernate:
|
||||
ddl-auto: create-drop
|
||||
show-sql: true
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
|
||||
# H2 Console (for development)
|
||||
h2:
|
||||
console:
|
||||
enabled: true
|
||||
path: /h2-console
|
||||
|
||||
# Jackson Configuration
|
||||
jackson:
|
||||
default-property-inclusion: non_null
|
||||
serialization:
|
||||
write-dates-as-timestamps: false
|
||||
deserialization:
|
||||
fail-on-unknown-properties: false
|
||||
|
||||
# Logging Configuration
|
||||
logging:
|
||||
level:
|
||||
com.vibing.backend: DEBUG
|
||||
org.springframework.security: DEBUG
|
||||
org.springframework.web: DEBUG
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
|
||||
|
||||
# API Documentation
|
||||
springdoc:
|
||||
api-docs:
|
||||
path: /api-docs
|
||||
swagger-ui:
|
||||
path: /swagger-ui.html
|
||||
operations-sorter: method
|
||||
|
||||
# Security Configuration
|
||||
security:
|
||||
jwt:
|
||||
secret: your-secret-key-here-change-in-production
|
||||
expiration: 86400000 # 24 hours in milliseconds
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.vibing.backend;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
/**
|
||||
* Basic test class for the Vibing Backend application.
|
||||
*/
|
||||
@SpringBootTest
|
||||
class VibingBackendApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
// This test verifies that the Spring application context loads successfully
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user