package com.ga.api.auth; import com.ga.common.auth.JwtTokenProvider; import com.ga.common.exception.BizException; import com.ga.common.exception.ErrorCode; import com.ga.common.system.SystemConfigService; import com.ga.common.system.SystemLogMapper; import com.ga.common.util.SecurityUtil; import com.ga.core.mapper.user.UserMapper; import com.ga.core.vo.user.UserResp; import com.ga.core.vo.user.UserStatus; import com.ga.core.vo.user.UserVO; import io.jsonwebtoken.Claims; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; @Slf4j @Service @RequiredArgsConstructor public class AuthService { private final UserMapper userMapper; private final PasswordEncoder passwordEncoder; private final JwtTokenProvider tokenProvider; private final SystemConfigService configService; private final SystemLogMapper systemLogMapper; @Transactional public LoginResp login(LoginReq req, String ipAddress, String userAgent) { UserVO user = userMapper.selectByLoginId(req.getLoginId()); int failLimit = configService.getInt("LOGIN_FAIL_LIMIT", 5); if (user == null) { saveLoginLog(null, req.getLoginId(), "FAIL", "USER_NOT_FOUND", ipAddress, userAgent); throw new BizException(ErrorCode.LOGIN_FAIL); } if (UserStatus.LOCKED.name().equals(user.getStatus())) { saveLoginLog(user.getUserId(), req.getLoginId(), "FAIL", "ACCOUNT_LOCKED", ipAddress, userAgent); throw new BizException(ErrorCode.ACCOUNT_LOCKED); } if (UserStatus.RETIRED.name().equals(user.getStatus())) { throw new BizException(ErrorCode.ACCOUNT_RETIRED); } if (!passwordEncoder.matches(req.getPassword(), user.getPassword())) { userMapper.incrementFailCount(user.getUserId()); int newFail = (user.getFailCount() == null ? 0 : user.getFailCount()) + 1; if (newFail >= failLimit) { userMapper.updateStatus(user.getUserId(), UserStatus.LOCKED.name()); } saveLoginLog(user.getUserId(), req.getLoginId(), "FAIL", "BAD_PASSWORD", ipAddress, userAgent); throw new BizException(ErrorCode.LOGIN_FAIL); } // 로그인 성공 userMapper.updateLastLogin(user.getUserId()); List roles = userMapper.selectRoleCodes(user.getUserId()); String access = tokenProvider.createAccessToken(user.getUserId(), user.getLoginId(), roles); String refresh = tokenProvider.createRefreshToken(user.getUserId(), user.getLoginId()); saveLoginLog(user.getUserId(), req.getLoginId(), "SUCCESS", null, ipAddress, userAgent); return LoginResp.builder() .accessToken(access) .refreshToken(refresh) .accessTokenExpiresIn(tokenProvider.getAccessExpire()) .userId(user.getUserId()) .loginId(user.getLoginId()) .userName(user.getUserName()) .roles(roles) .build(); } public LoginResp refresh(String refreshToken) { Claims claims = tokenProvider.parse(refreshToken); if (!"REFRESH".equals(claims.get("type"))) { throw new BizException(ErrorCode.TOKEN_INVALID); } Long userId = claims.get("uid", Long.class); UserVO user = userMapper.selectById(userId); if (user == null || !UserStatus.ACTIVE.name().equals(user.getStatus())) { throw new BizException(ErrorCode.UNAUTHORIZED); } List roles = userMapper.selectRoleCodes(userId); String newAccess = tokenProvider.createAccessToken(userId, user.getLoginId(), roles); return LoginResp.builder() .accessToken(newAccess) .accessTokenExpiresIn(tokenProvider.getAccessExpire()) .userId(userId) .loginId(user.getLoginId()) .userName(user.getUserName()) .roles(roles) .build(); } public UserResp me() { Long userId = SecurityUtil.getCurrentUserId(); if (userId == null) throw new BizException(ErrorCode.UNAUTHORIZED); UserResp resp = userMapper.selectDetailById(userId); if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); resp.setRoleCodes(userMapper.selectRoleCodes(userId)); return resp; } @Transactional public void changePassword(String oldPassword, String newPassword) { Long userId = SecurityUtil.getCurrentUserId(); if (userId == null) throw new BizException(ErrorCode.UNAUTHORIZED); UserVO user = userMapper.selectById(userId); if (!passwordEncoder.matches(oldPassword, user.getPassword())) { throw new BizException(ErrorCode.LOGIN_FAIL, "기존 비밀번호가 일치하지 않습니다"); } validatePasswordPolicy(newPassword); userMapper.updatePassword(userId, passwordEncoder.encode(newPassword)); } public void logout() { Long userId = SecurityUtil.getCurrentUserId(); saveLoginLog(userId, SecurityUtil.getCurrentLoginId(), "LOGOUT", null, null, null); } private void saveLoginLog(Long userId, String loginId, String type, String reason, String ip, String ua) { try { systemLogMapper.insertLoginLog(userId, loginId, type, reason, ip, ua); } catch (Exception e) { log.warn("Login log save failed: {}", e.getMessage()); } } private void validatePasswordPolicy(String password) { int min = configService.getInt("PASSWORD_MIN_LEN", 8); if (password == null || password.length() < min) { throw new BizException(ErrorCode.PASSWORD_POLICY, "비밀번호는 " + min + "자 이상이어야 합니다"); } } }