AccountController.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. using Microsoft.AspNetCore.Mvc;
  2. using Microsoft.AspNetCore.Authorization;
  3. using Microsoft.AspNetCore.Authentication.JwtBearer;
  4. using Microsoft.EntityFrameworkCore;
  5. using bitforum.DTOs.Request;
  6. using bitforum.DTOs.Response;
  7. using bitforum.Repository;
  8. using bitforum.Services;
  9. using bitforum.Helpers;
  10. using bitforum.Constants;
  11. using bitforum.Models.Log;
  12. using bitforum.Extensions;
  13. namespace bitforum.Controllers.API
  14. {
  15. [ApiController]
  16. [Route("api/[controller]")]
  17. [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] // JWT 인증 적용
  18. public class AccountController: ControllerBase
  19. {
  20. private readonly ILogger<AccountController> _logger;
  21. private readonly DefaultDbContext _db;
  22. private readonly IMemberRepository _memberRepository;
  23. private readonly IMailService _mailService;
  24. private readonly IConfigService _configService;
  25. private ResultDto _result = new ResultDto();
  26. public AccountController(ILogger<AccountController> logger, DefaultDbContext db, IMemberRepository memberRepository, IMailService mailService, IConfigService configService)
  27. {
  28. _logger = logger;
  29. _db = db;
  30. _memberRepository = memberRepository;
  31. _mailService = mailService;
  32. _configService = configService;
  33. }
  34. // 회원 조회
  35. [HttpGet("Info/{email}")]
  36. public async Task<ActionResult<ResultDto>> Info([FromRoute] string email)
  37. {
  38. try
  39. {
  40. if (string.IsNullOrEmpty(email))
  41. {
  42. throw new Exception("이메일을 입력해주세요.");
  43. }
  44. var member = await _memberRepository.FindMemberByEmail(email);
  45. if (member is null)
  46. {
  47. throw new Exception("회원 정보를 찾을 수 없습니다.");
  48. }
  49. _result.Data = member;
  50. }
  51. catch (Exception e)
  52. {
  53. _logger.LogError(e, e.Message);
  54. _result.Ok = false;
  55. _result.Status = StatusCodes.Status400BadRequest;
  56. _result.Message = e.Message;
  57. }
  58. return _result;
  59. }
  60. // 별명 수정
  61. [HttpPost("change-nickname")]
  62. public async Task<ActionResult<ResultDto>> ChangeNickname([FromBody] ChangeNicknameDto request)
  63. {
  64. try
  65. {
  66. if (!ModelState.IsValid)
  67. {
  68. _result.Errors = ModelState.GetErrors();
  69. throw new Exception("유효성 검사에 실패했습니다.");
  70. }
  71. // 중복 여부 확인
  72. var isExist = await _memberRepository.IsExistNickname(request.Name);
  73. if (isExist)
  74. {
  75. throw new Exception("이미 사용중인 별명입니다.");
  76. }
  77. // 회원 조회
  78. var member = await _memberRepository.FindMemberByID(request.ID);
  79. if (member is null)
  80. {
  81. throw new Exception("회원 정보를 찾을 수 없습니다.");
  82. }
  83. // 별명 변경 가능 기간 계산 (현재 날짜 - 설정된 변경 기간)
  84. var changeNicknameDay = Config.Register.ChangeNicknameDay ?? 0;
  85. if (changeNicknameDay > 0)
  86. {
  87. // 사용자의 마지막 별명 변경 날짜가 설정된 제한일 이내인지 확인
  88. if (member.LastNameChangedAt.HasValue)
  89. {
  90. int daysSinceLastChange = (int)(DateTime.UtcNow - member.LastNameChangedAt.Value).TotalDays; // 남은 일수 계산
  91. if (daysSinceLastChange < changeNicknameDay)
  92. {
  93. throw new Exception($"별명 변경은 {changeNicknameDay - daysSinceLastChange}일 후에 가능합니다.");
  94. }
  95. }
  96. }
  97. await _db.NameChangeLog.AddAsync(new NameChangeLog
  98. {
  99. MemberID = member.ID,
  100. BeforeName = member.Name,
  101. AfterName = request.Name,
  102. CreatedAt = DateTime.UtcNow
  103. });
  104. member.Name = request.Name;
  105. member.LastNameChangedAt = DateTime.UtcNow;
  106. await _db.SaveChangesAsync();
  107. _result.Message = "별명이 변경되었습니다.";
  108. }
  109. catch (Exception e)
  110. {
  111. _logger.LogError(e, e.Message);
  112. _result.Ok = false;
  113. _result.Status = StatusCodes.Status400BadRequest;
  114. _result.Message = e.Message;
  115. }
  116. return _result;
  117. }
  118. // 이메일 변경
  119. [HttpPost("change-email")]
  120. public async Task<ActionResult<ResultDto>> ChangeEmail([FromBody] ChangeEmailDto request)
  121. {
  122. try
  123. {
  124. if (!ModelState.IsValid)
  125. {
  126. _result.Errors = ModelState.GetErrors();
  127. throw new Exception("유효성 검사에 실패했습니다.");
  128. }
  129. // 중복 여부 확인
  130. var isExist = await _memberRepository.IsExistEmail(request.Email);
  131. if (isExist)
  132. {
  133. throw new Exception("이미 사용중인 이메일입니다.");
  134. }
  135. // 회원 조회
  136. var member = await _memberRepository.FindMemberByID(request.ID);
  137. if (member is null)
  138. {
  139. throw new Exception("회원 정보를 찾을 수 없습니다.");
  140. }
  141. // 이메일 변경 가능 기간 계산 (현재 날짜 - 설정된 변경 기간)
  142. var changeEmailDay = Config.Register.ChangeEmailDay ?? 0;
  143. if (changeEmailDay > 0)
  144. {
  145. // 사용자의 마지막 이메일 변경 날짜가 설정된 제한일 이내인지 확인
  146. if (member.LastEmailChangedAt.HasValue)
  147. {
  148. int daysSinceLastChange = (int)(DateTime.UtcNow - member.LastEmailChangedAt.Value).TotalDays; // 남은 일수 계산
  149. if (daysSinceLastChange < changeEmailDay)
  150. {
  151. throw new Exception($"별명 변경은 {changeEmailDay - daysSinceLastChange}일 후에 가능합니다.");
  152. }
  153. }
  154. }
  155. // 이메일 인증 메일 발송
  156. await _mailService.SendEmailVerifyAsync(request.Email, member, new AdditionalData
  157. {
  158. Email = member.Email
  159. });
  160. _result.Message = "이메일 확인을 위해 인증 메일이 전송되었습니다.";
  161. }
  162. catch (Exception e)
  163. {
  164. _logger.LogError(e, e.Message);
  165. _result.Ok = false;
  166. _result.Status = StatusCodes.Status400BadRequest;
  167. _result.Message = e.Message;
  168. }
  169. return _result;
  170. }
  171. // 비밀번호 변경
  172. [HttpPost("change-password")]
  173. public async Task<ActionResult<ResultDto>> ChangePassword([FromBody] ChangePasswordDto request)
  174. {
  175. try
  176. {
  177. if (!ModelState.IsValid)
  178. {
  179. _result.Errors = ModelState.GetErrors();
  180. throw new Exception("유효성 검사에 실패하였습니다.");
  181. }
  182. var memberID = User.GetID();
  183. // 회원 확인
  184. var isExist = await _memberRepository.IsExistID(memberID);
  185. if (!isExist)
  186. {
  187. throw new Exception("존재하지 않는 회원입니다.");
  188. }
  189. // 회원 조회
  190. var member = await _memberRepository.FindMemberByID(memberID);
  191. if (member is null)
  192. {
  193. throw new Exception("회원 정보를 찾을 수 없습니다.");
  194. }
  195. // 현재 비밀번호가 맞는지 확인
  196. var isValidPassword = BCrypt.Net.BCrypt.Verify(request.CurrentPassword, member.Password);
  197. if (!isValidPassword)
  198. {
  199. throw new Exception("현재 비밀번호가 일치하지 않습니다.");
  200. }
  201. var newPassword = BCrypt.Net.BCrypt.HashPassword(request.NewPassword);
  202. if (member.Password == newPassword)
  203. {
  204. throw new Exception("기존 비밀번호와 동일합니다.");
  205. }
  206. var isValidPolicy = _configService.IsPasswordPolicyValid(request.ConfirmPassword);
  207. if (!isValidPolicy)
  208. {
  209. string message = "";
  210. int minLengthConfig = Config.Register.PasswordMinLength ?? 6;
  211. int uppercaseLengthConfig = Config.Register.PasswordUppercaseLength ?? 0;
  212. int numbersLengthConfig = Config.Register.PasswordNumbersLength ?? 0;
  213. int specialCharsLengthConfig = Config.Register.PasswordSpecialcharsLength ?? 0;
  214. // 위에 데이터로 안내 문구 만들어줘
  215. if (minLengthConfig > 0)
  216. {
  217. message += $"최소 {minLengthConfig}자 이상, ";
  218. }
  219. if (uppercaseLengthConfig > 0)
  220. {
  221. message += $"대문자 {uppercaseLengthConfig}자 이상, ";
  222. }
  223. if (numbersLengthConfig > 0)
  224. {
  225. message += $"숫자 {numbersLengthConfig}자 이상, ";
  226. }
  227. if (specialCharsLengthConfig > 0)
  228. {
  229. message += $"특수문자 {specialCharsLengthConfig}자 이상, ";
  230. }
  231. message = message.TrimEnd(',', ' ');
  232. throw new Exception($"입력하신 비밀번호는 사용하실 수 없습니다. 다음 조건을 충족해주세요.\n\n{message}");
  233. }
  234. member.Password = newPassword;
  235. member.PasswordUpdatedAt = DateTime.UtcNow;
  236. await _db.SaveChangesAsync();
  237. await _mailService.SendChangedPasswordEmailAsync(member);
  238. _result.Message = "비밀번호가 변경되었습니다.";
  239. }
  240. catch (Exception e)
  241. {
  242. _logger.LogError(e, e.Message);
  243. _result.Ok = false;
  244. _result.Status = StatusCodes.Status400BadRequest;
  245. _result.Message = e.Message;
  246. }
  247. return _result;
  248. }
  249. // 회원탈퇴
  250. [HttpPost("withdraw")]
  251. public async Task<ActionResult<ResultDto>> Withdraw()
  252. {
  253. try
  254. {
  255. var memberID = User.GetID();
  256. // 회원 확인
  257. var isExist = await _memberRepository.IsExistID(memberID);
  258. if (!isExist)
  259. {
  260. throw new Exception("존재하지 않는 회원입니다.");
  261. }
  262. // 회원 조회
  263. var member = await _memberRepository.FindMemberByID(memberID);
  264. if (member is null)
  265. {
  266. throw new Exception("회원 정보를 찾을 수 없습니다.");
  267. }
  268. if (member.IsWithdraw)
  269. {
  270. throw new Exception("이미 탈퇴한 회원입니다.");
  271. }
  272. member.IsWithdraw = true;
  273. member.DeletedAt = DateTime.UtcNow;
  274. // 탈퇴 완료 메일 발송
  275. await _mailService.SendWithdrawEmailAsync(member);
  276. // 로그인 강제 로그아웃(쿠키 삭제)
  277. Response.Cookies.Delete("accessToken");
  278. Response.Cookies.Delete("refreshToken");
  279. _result.Message = "정상적으로 탈퇴되었습니다. 이용해 주셔서 감사합니다.";
  280. }
  281. catch (Exception e)
  282. {
  283. _logger.LogError(e, e.Message);
  284. _result.Ok = false;
  285. _result.Status = StatusCodes.Status400BadRequest;
  286. _result.Message = e.Message;
  287. }
  288. return _result;
  289. }
  290. // 로그인 기록
  291. [HttpGet("login-log")]
  292. public async Task<ActionResult<ResultDto>> LoginLog([FromQuery] int page = 1, [FromQuery] string type = LoginLogType.Today)
  293. {
  294. try
  295. {
  296. var memberID = User.GetID();
  297. // 회원 확인
  298. var isExist = await _memberRepository.IsExistID(memberID);
  299. if (!isExist)
  300. {
  301. throw new Exception("존재하지 않는 회원입니다.");
  302. }
  303. if (!LoginLogType.ValidTypes.Contains(type))
  304. {
  305. throw new ArgumentException("유효하지 않은 타입입니다.");
  306. }
  307. DateTime now = DateTime.UtcNow;
  308. DateTime startDate = now.Date;
  309. DateTime endOfDate = type switch
  310. {
  311. LoginLogType.Today => now.Date.AddDays(1).AddTicks(-1),
  312. LoginLogType.Week => now.AddDays(-7),
  313. LoginLogType.Month => now.AddMonths(-1),
  314. LoginLogType.HalfYear => now.AddMonths(-6),
  315. _ => DateTime.MinValue // 전체 조회
  316. };
  317. var query = _db.LoginLog.Where(c => c.MemberID == memberID && c.CreatedAt >= startDate && c.CreatedAt <= endOfDate)
  318. .Select(c => new
  319. {
  320. c.ID,
  321. c.MemberID,
  322. c.Success,
  323. c.IpAddress,
  324. c.UserAgent,
  325. CreatedAt = c.CreatedAt.ToLocalTime()
  326. })
  327. .OrderByDescending(c => c.ID)
  328. .Skip(page * 10)
  329. .Take(10);
  330. var logs = await query.ToListAsync();
  331. if (logs.Count == 0)
  332. {
  333. throw new Exception("로그인 기록이 없습니다.");
  334. }
  335. _result.Data = logs;
  336. }
  337. catch (Exception e)
  338. {
  339. _logger.LogError(e, e.Message);
  340. _result.Ok = false;
  341. _result.Status = StatusCodes.Status400BadRequest;
  342. _result.Message = e.Message;
  343. }
  344. return _result;
  345. }
  346. }
  347. }