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
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package team.projectpulse.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class PasswordEncoderConfig {

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
Expand All @@ -31,10 +29,12 @@ public class SecurityConfig {

private final UserDetailsService userDetailsService;
private final JwtAuthFilter jwtAuthFilter;
private final PasswordEncoder passwordEncoder;

public SecurityConfig(UserDetailsService userDetailsService, JwtAuthFilter jwtAuthFilter) {
public SecurityConfig(UserDetailsService userDetailsService, JwtAuthFilter jwtAuthFilter, PasswordEncoder passwordEncoder) {
this.userDetailsService = userDetailsService;
this.jwtAuthFilter = jwtAuthFilter;
this.passwordEncoder = passwordEncoder;
}

@Bean
Expand All @@ -45,6 +45,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.POST, "/api/v1/users/login").permitAll()
.requestMatchers(HttpMethod.POST, "/api/v1/users/register").permitAll()
.requestMatchers(HttpMethod.POST, "/api/v1/users/forget-password/**").permitAll()
.requestMatchers(HttpMethod.PATCH, "/api/v1/users/reset-password").permitAll()
.requestMatchers("/actuator/**").permitAll()
Expand Down Expand Up @@ -73,17 +74,12 @@ public CorsConfigurationSource corsConfigurationSource() {
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
provider.setPasswordEncoder(passwordEncoder);
return provider;
}

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package team.projectpulse.user;

public record RegisterRequest(String firstName, String lastName, String email, String password) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package team.projectpulse.user;

public class UserAlreadyExistsException extends RuntimeException {
public UserAlreadyExistsException(String email) {
super("An account with the email \"" + email + "\" already exists.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ public class UserController {

private final AuthenticationManager authenticationManager;
private final JwtUtils jwtUtils;
private final UserService userService;

public UserController(AuthenticationManager authenticationManager, JwtUtils jwtUtils) {
public UserController(AuthenticationManager authenticationManager, JwtUtils jwtUtils, UserService userService) {
this.authenticationManager = authenticationManager;
this.jwtUtils = jwtUtils;
this.userService = userService;
}

@PostMapping("/login")
Expand All @@ -47,4 +49,14 @@ public Result<Map<String, Object>> login(HttpServletRequest request) {
String token = jwtUtils.generateToken(user);
return Result.success("Login successful.", Map.of("token", token, "userInfo", user));
}

/**
* UC-25: Student sets up a student account via the invitation link.
* POST /api/v1/users/register
*/
@PostMapping("/register")
public Result<Void> register(@RequestBody RegisterRequest request) {
userService.registerStudent(request);
return Result.success("Account created. Please log in.", null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package team.projectpulse.user;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import team.projectpulse.common.Result;

@RestControllerAdvice
public class UserExceptionHandler {

@ExceptionHandler(UserAlreadyExistsException.class)
@ResponseStatus(HttpStatus.CONFLICT)
Result<Void> handleAlreadyExists(UserAlreadyExistsException ex) {
return Result.conflict(ex.getMessage());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@

public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
boolean existsByEmailIgnoreCase(String email);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,41 @@
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class UserService implements UserDetailsService {

private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;

public UserService(UserRepository userRepository) {
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}

@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
return userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("No account found for: " + email));
}

/**
* UC-25: Register a new student account.
* Throws UserAlreadyExistsException if the email is already registered.
*/
public User registerStudent(RegisterRequest req) {
if (userRepository.existsByEmailIgnoreCase(req.email().trim())) {
throw new UserAlreadyExistsException(req.email().trim());
}
User user = new User();
user.setFirstName(req.firstName().trim());
user.setLastName(req.lastName().trim());
user.setEmail(req.email().trim().toLowerCase());
user.setPassword(passwordEncoder.encode(req.password()));
user.setRoles("student");
user.setEnabled(true);
return userRepository.save(user);
}
}
4 changes: 3 additions & 1 deletion src/frontend/src/apis/user/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import request from '@/utils/request'
import type { ChangePasswordRequest, ChangePasswordResponse } from './types'
import type { RegisterRequest, RegisterResponse, ChangePasswordRequest, ChangePasswordResponse } from './types'

enum API { USERS = '/users' }

export const registerUser = (payload: RegisterRequest) =>
request.post<any, RegisterResponse>(`${API.USERS}/register`, payload)
export const changePassword = (payload: ChangePasswordRequest) =>
request.patch<any, ChangePasswordResponse>(`${API.USERS}/change-password`, payload)
13 changes: 13 additions & 0 deletions src/frontend/src/apis/user/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
export interface RegisterRequest {
firstName: string
lastName: string
email: string
password: string
}

export interface RegisterResponse {
flag: boolean
code: number
message: string
}

export interface ChangePasswordRequest {
currentPassword: string
newPassword: string
Expand Down
23 changes: 20 additions & 3 deletions src/frontend/src/pages/UserRegistration.vue
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useNotifyStore } from '@/stores/notify'
import { registerUser } from '@/apis/user'

const route = useRoute()
const router = useRouter()
Expand All @@ -90,19 +91,35 @@ const formData = ref({

onMounted(() => {
const email = route.query.email as string
// Extension 2a: if already registered, redirect to login
const alreadyRegistered = route.query.registered as string
if (email) formData.value.email = email
if (alreadyRegistered === 'true') {
notifyStore.info('You already have an account. Please log in.')
router.push({ name: 'login' })
}
})

async function register() {
const { valid } = await registerForm.value.validate()
if (!valid) return
submitting.value = true
try {
// UC-25 / UC-30: registration API call goes here
await registerUser({
firstName: formData.value.firstName,
lastName: formData.value.lastName,
email: formData.value.email,
password: formData.value.password
})
notifyStore.success('Account created! Please log in.')
router.push({ name: 'login' })
} catch {
// handled by interceptor
} catch (err: any) {
// Extension 2a: email already registered (409)
if (err?.response?.status === 409) {
notifyStore.warning('An account with this email already exists. Redirecting to login.')
router.push({ name: 'login' })
}
// other errors handled by interceptor
} finally {
submitting.value = false
}
Expand Down
Loading