Files
ga-commission-system/ga-api/src/main/java/com/ga/api/auth/AuthService.java
T
GA Pro 7f3c564605 feat: ga-core + ga-api + ga-batch + V13-V15 + Docker + frontend skeleton
ga-core (22 tables):
- user/role: VO + Mapper + XML (login + RBAC)
- org: Grade, Organization (CTE recursive tree), Agent + history
- product: InsuranceCompany, Product, Contract
- rule: CommissionRate, PayoutRule, OverrideRule, ChargebackRule, ExceptionTypeCode
- receive: CompanyProfile, FieldMapping, CodeMapping, RawCommissionData, ParseError
- ledger: Recruit, Maintain, Exception (batch insert + cursor)
- settle: SettleMaster (UPSERT), Override, Chargeback, Payment, Reconciliation, BatchJobLog
- All XMLs use EncryptTypeHandler for PII fields

ga-api:
- AuthController: login (lock on N fails), refresh, me, password, logout
- Domain controllers: Agent, Organization, Contract, Company, Product, Rule,
  Receive, Ledger (recruit/maintain/exception + approve), Settle (confirm/hold/release),
  Payment, User, Role (matrix), BatchTrigger
- All use @RequirePermission + @DataChangeLog

ga-batch:
- MonthlySettlementJob: 8 Steps (receiveData, transformData, reconcile,
  calcRecruit, calcMaintain, chargebackException, calcOverride, aggregate)
- DataReceiver strategy + ManualReceiver, MappingEngine stub
- CommissionCalculator (pct/tax via MoneyUtil), JobLauncherController

DB V13~V15:
- V13: 14 indexes (settle_master, ledgers, contract, chargeback, payment, raw, logs)
- V14: create_monthly_partition() helper function
- V15: v_agent_monthly_summary, v_org_settle_summary, v_chargeback_risk views

Infra:
- docker-compose: postgres + redis + api + batch + frontend
- Multi-stage Dockerfile (gradle build → JRE)

ga-frontend (skeleton):
- Vite + React 18 + TypeScript + AntD 5 + AG Grid + React Query + Zustand
- API request layer (axios + JWT), LoginPage, MainLayout (dynamic menu), Dashboard
- nginx.conf for /api proxying
2026-05-09 22:00:53 +09:00

140 lines
5.8 KiB
Java

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.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 ("LOCKED".equals(user.getStatus())) {
saveLoginLog(user.getUserId(), req.getLoginId(), "FAIL", "ACCOUNT_LOCKED", ipAddress, userAgent);
throw new BizException(ErrorCode.ACCOUNT_LOCKED);
}
if ("RETIRED".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(), "LOCKED");
}
saveLoginLog(user.getUserId(), req.getLoginId(), "FAIL", "BAD_PASSWORD", ipAddress, userAgent);
throw new BizException(ErrorCode.LOGIN_FAIL);
}
// 로그인 성공
userMapper.updateLastLogin(user.getUserId());
List<String> 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 || !"ACTIVE".equals(user.getStatus())) {
throw new BizException(ErrorCode.UNAUTHORIZED);
}
List<String> 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 + "자 이상이어야 합니다");
}
}
}