diff --git a/Program.cs b/Program.cs index a48805d..ddddd9f 100644 --- a/Program.cs +++ b/Program.cs @@ -65,7 +65,8 @@ builder.Services.AddAuthentication(options => ValidateIssuerSigningKey = true, ValidIssuer = jwtSettings.Issuer, ValidAudience = jwtSettings.Audience, - IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.SecretKey)) + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.SecretKey)), + ClockSkew = TimeSpan.FromMinutes(jwtSettings.ClockSkewMinutes) }; }); // JWT 설정부 끝 @@ -96,6 +97,19 @@ builder.Services.AddCors(option => }); }); +// 로그 설정 부분 +builder.Logging.ClearProviders(); +builder.Logging.AddConsole(); +if (builder.Environment.IsDevelopment()) { + builder.Logging.SetMinimumLevel(LogLevel.Trace); +} +else +{ + builder.Logging.SetMinimumLevel(LogLevel.Warning); +} + + + // 로컬 테스트 위한 부분 builder.WebHost.UseUrls("http://0.0.0.0:5144"); ///// ===== builder 설정 부 ===== ///// diff --git a/Program/Common/Data/AppDbContext.cs b/Program/Common/Data/AppDbContext.cs index f03f11c..3fddb4f 100644 --- a/Program/Common/Data/AppDbContext.cs +++ b/Program/Common/Data/AppDbContext.cs @@ -1,3 +1,4 @@ +using AcaMate.Common.Models; using Microsoft.EntityFrameworkCore; using AcaMate.V1.Models; using Version = AcaMate.V1.Models.Version; @@ -13,11 +14,18 @@ public class AppDbContext: DbContext //MARK: Program public DbSet Version { get; set; } public DbSet Academy { get; set; } + public DbSet RefreshTokens { get; set; } //MARK: USER public DbSet Login { get; set; } public DbSet UserAcademy { get; set; } public DbSet User { get; set; } + public DbSet Permission { get; set; } + // public DbSet Token { get; set; } + public DbSet Location { get; set; } + public DbSet Contact { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/Program/Common/JWTToken/JwtTokenService.cs b/Program/Common/JWTToken/JwtTokenService.cs new file mode 100644 index 0000000..0995a5d --- /dev/null +++ b/Program/Common/JWTToken/JwtTokenService.cs @@ -0,0 +1,72 @@ +using System; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using System.Collections.Generic; +using AcaMate.Common.Models; +using Microsoft.IdentityModel.Tokens; +using Microsoft.Extensions.Options; + +using System.Security.Cryptography; + +namespace AcaMate.Common.Token; + +public class JwtTokenService +{ + private readonly JwtSettings _jwtSettings; + + public JwtTokenService(IOptions jwtSettings) + { + _jwtSettings = jwtSettings.Value; + } + + public string GenerateJwtToken(string uid, string role) + { + // 1. 클레임(Claim) 설정 - 필요에 따라 추가 정보도 포함 + var claims = new List + { + // 토큰 주체(sub) 생성을 위해 값으로 uid를 사용함 : 토큰이 대표하는 고유 식별자 + new Claim(JwtRegisteredClaimNames.Sub, uid), + // Jti 는 토큰 식별자로 토큰의 고유 ID 이다. + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + // jwt 토큰이 가지는 권한 + new Claim(ClaimTypes.Role, role) + // 추가 클레임 예: new Claim(ClaimTypes.Role, "Admin") + }; + + // 2. 비밀 키와 SigningCredentials 생성 + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.SecretKey)); + var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + // 3. 토큰 생성 (Issuer, Audience, 만료 시간, 클레임, 서명 정보 포함) + var token = new JwtSecurityToken( + issuer: _jwtSettings.Issuer, + audience: _jwtSettings.Audience, + claims: claims, + expires: DateTime.UtcNow.AddMinutes(_jwtSettings.ExpiryMinutes), + signingCredentials: credentials + ); + + // 4. 토큰 객체를 문자열로 변환하여 반환 + return new JwtSecurityTokenHandler().WriteToken(token); + } + + public RefreshToken GenerateRefreshToken(string uid) + { + var randomNumber = new byte[32]; // 256비트 + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(randomNumber); + } + + // return Convert.ToBase64String(randomNumber); + return new RefreshToken() + { + uid = uid, + token = Convert.ToBase64String(randomNumber), + create_Date = DateTime.UtcNow, + expire_date = DateTime.UtcNow.AddDays(_jwtSettings.RefreshTokenExpiryDays) + + }; + } +} \ No newline at end of file diff --git a/Program/Common/Model/JwtSettings.cs b/Program/Common/Model/JwtSettings.cs index 7f8d826..8f949fe 100644 --- a/Program/Common/Model/JwtSettings.cs +++ b/Program/Common/Model/JwtSettings.cs @@ -1,3 +1,6 @@ +using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; + namespace AcaMate.Common.Models; public class JwtSettings @@ -6,4 +9,19 @@ public class JwtSettings public string Issuer { get; set; } public string Audience { get; set; } public int ExpiryMinutes { get; set; } + public int ClockSkewMinutes { get; set; } + public int RefreshTokenExpiryDays { get; set; } +} +[Table(("refresh_token"))] +public class RefreshToken +{ + [Key] + [Required(ErrorMessage = "필수 항목 누락")] + public string uid { get; set; } + public string token { get; set; } + public DateTime create_Date { get; set; } + public DateTime expire_date { get; set; } + + // 이건 로그아웃시에 폐기 시킬예정이니 그떄 변경하는걸로 합시다. + public DateTime? revoke_Date { get; set; } } \ No newline at end of file diff --git a/Program/V1/Controllers/UserController.cs b/Program/V1/Controllers/UserController.cs index d2c8c99..df702c7 100644 --- a/Program/V1/Controllers/UserController.cs +++ b/Program/V1/Controllers/UserController.cs @@ -1,33 +1,38 @@ -using Microsoft.AspNetCore.Mvc; -using System.Text.Json; using AcaMate.Common.Data; using AcaMate.Common.Models; using AcaMate.V1.Models; -using Microsoft.AspNetCore.Http.HttpResults; -using Microsoft.IdentityModel.Tokens; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System.Linq.Expressions; +using AcaMate.V1.Services; +using AcaMate.Common.Token; +using Microsoft.Extensions.Logging; +using Microsoft.EntityFrameworkCore.Query; +using MySqlConnector; namespace AcaMate.V1.Controllers; - [ApiController] [Route("/api/v1/in/user")] [ApiExplorerSettings(GroupName = "사용자")] -public class UserController: ControllerBase +public class UserController : ControllerBase { private readonly AppDbContext _dbContext; - public UserController(AppDbContext dbContext) + private readonly ILogger _logger; + private readonly JwtTokenService _jwtTokenService; + + public UserController(AppDbContext dbContext, ILogger logger, JwtTokenService jwtTokenService) { _dbContext = dbContext; + _logger = logger; + _jwtTokenService = jwtTokenService; } - [HttpGet()] + [HttpGet] [CustomOperation("회원 정보 조회", "회원 정보 조회", "사용자")] public IActionResult GetUserData(string uid) { - if (string.IsNullOrEmpty(uid)) - { - return BadRequest(DefaultResponse.InvalidInputError); - } + if (string.IsNullOrEmpty(uid)) return BadRequest(DefaultResponse.InvalidInputError); try { @@ -41,13 +46,13 @@ public class UserController: ControllerBase birth = u.birth, device_id = u.device_id, login_date = u.login_date, - type = u.type, + type = u.type }) .FirstOrDefault(); - + var response = new APIResponseStatus { - status = new Status() + status = new Status { code = "000", message = "정상" @@ -56,7 +61,7 @@ public class UserController: ControllerBase }; return Ok(response.JsonToString()); } - catch(Exception ex) + catch (Exception ex) { return StatusCode(500, DefaultResponse.UnknownError); } @@ -66,22 +71,21 @@ public class UserController: ControllerBase [CustomOperation("SNS 로그인", "로그인 후 회원이 있는지 확인", "사용자")] public IActionResult Login(string acctype, string sns_id) { - if (string.IsNullOrEmpty(acctype) && string.IsNullOrEmpty(sns_id)) - { return BadRequest(DefaultResponse.InvalidInputError); - } try { var login = _dbContext.Login.FirstOrDefault(l => l.sns_type == acctype && l.sns_id == sns_id); - string uid = ""; + var uid = ""; + List bids = new List(); if (login != null) { uid = login.uid; + var user = _dbContext.User.FirstOrDefault(user => user.uid == uid); if (user != null) { @@ -89,16 +93,22 @@ public class UserController: ControllerBase _dbContext.SaveChanges(); } + // 토큰 생성은 로그인 부분에서 하는거지 + var accessToken = _jwtTokenService.GenerateJwtToken(uid, "Normal"); + var refreshToken = _jwtTokenService.GenerateRefreshToken(uid); + _dbContext.RefreshTokens.Add(refreshToken); + _dbContext.SaveChanges(); + var userAcademy = _dbContext.UserAcademy.Where(u => u.uid == uid).ToList(); - foreach(User_Academy userData in userAcademy) + foreach (var userData in userAcademy) { - Console.WriteLine($"uid: {userData.uid} || bid: {userData.bid}"); + _logger.LogInformation($"uid: {userData.uid} || bid: {userData.bid}"); bids.Add(userData.bid); } - + var response = new APIResponseStatus { - status = new Status() + status = new Status { code = "000", message = "정상" @@ -109,7 +119,7 @@ public class UserController: ControllerBase bid = bids } }; - + return Ok(response.JsonToString()); } else @@ -117,7 +127,7 @@ public class UserController: ControllerBase // 계정이 없다는 거 var response = new APIResponseStatus { - status = new Status() + status = new Status { code = "010", message = "정상" @@ -125,7 +135,7 @@ public class UserController: ControllerBase data = new { uid = "", - bid = new string[]{} + bid = new string[] { } } }; return Ok(response.JsonToString()); @@ -136,9 +146,9 @@ public class UserController: ControllerBase return StatusCode(500, DefaultResponse.UnknownError); } } - + [HttpPost("academy")] - [CustomOperation("학원 리스트 확인","등록된 학원 리스트 확인", "사용자")] + [CustomOperation("학원 리스트 확인", "등록된 학원 리스트 확인", "사용자")] public IActionResult ReadAcademyInfo([FromBody] RequestAcademy request) { if (!request.bids.Any()) @@ -156,10 +166,10 @@ public class UserController: ControllerBase name = a.business_name }) .ToList(); - + var response = new APIResponseStatus> { - status = new Status() + status = new Status { code = "000", message = "정상" @@ -171,17 +181,111 @@ public class UserController: ControllerBase [HttpPost("register")] [CustomOperation("회원 가입", "사용자 회원 가입", "사용자")] - public IActionResult UserRegister([FromBody] User request) + public async Task UserRegister([FromBody] UserAll request) { - if (request.uid.IsNullOrEmpty()) + if (!ModelState.IsValid) return BadRequest(DefaultResponse.InvalidInputError); + + var atIndext = request.email.IndexOf('@'); + var localPart_email = request.email.Substring(0, atIndext); + var uid = $"AM{localPart_email}{DateTime.Now:yyyyMMdd}"; + + var user = new User { - var error = DefaultResponse.InvalidInputError; - return Ok(error); + uid = uid, + name = request.name, + birth = request.birth, + type = request.type, + device_id = request.device_id, + auto_login_yn = request.auto_login_yn, + login_date = request.login_date + }; + var login = new Login + { + uid = uid, + sns_id = request.sns_id, + sns_type = request.sns_type + }; + + var permission = new Permission + { + uid = uid, + location_yn = request.location_yn, + camera_yn = request.camera_yn, + photo_yn = request.photo_yn, + push_yn = request.push_yn, + market_app_yn = request.market_app_yn, + market_sms_yn = request.market_sms_yn, + market_email_yn = request.market_email_yn + }; + + var contact = new Contact + { + uid = uid, + email = request.email, + phone = request.phone, + address = request.address + }; + + + if (await SaveData(user, u => u.uid)) + { + await SaveData(login, l => l.sns_id); + await SaveData(permission, p => p.uid); + await SaveData(contact, c => c.uid); } - else + + // TO-DO: jwt 토큰 만들어서 여기서 보내는 작업을 해야 함 + + return Ok($"회원가입 : {uid}"); + } + private async Task SaveData (T entity, Expression> key) where T : class + { + try { - return Ok("회원가입"); + var value = key.Compile()(entity); + + // x 라 함은 Expression 으로 생성되는 트리에서 T 타입으의 매개변수를 지칭함 + var parameter = Expression.Parameter(typeof(T), "x"); + + var invokedExpr = Expression.Invoke(key, parameter); + var constantExpr = Expression.Constant(value, key.Body.Type); + var equalsExpr = Expression.Equal(invokedExpr, constantExpr); + + var predicate = Expression.Lambda>(equalsExpr, parameter); + + var dbSet = _dbContext.Set(); + var entityData = await dbSet.FirstOrDefaultAsync(predicate); + + if (entityData != null) + { + _logger.LogInformation($"[{typeof(T)}] 해당 PK 존재 [{value}]: 계속"); + var entry = _dbContext.Entry(entityData); + entry.CurrentValues.SetValues(entity); + if (entry.Properties.Any(p => p.IsModified)) + { + _logger.LogInformation($"[{typeof(T)}] 변경사항 존재: 계속"); + } + else + { + _logger.LogInformation($"[{typeof(T)}] 변경사항 없음: 종료"); + return true; + } + } + else + { + dbSet.Add(entity); + } + + await _dbContext.SaveChangesAsync(); + _logger.LogInformation($"[{typeof(T)}] DB 저장 완료: 종료"); + return true; + } + catch (Exception ex) + { + _logger.LogInformation($"[{typeof(T)}] 알 수 없는 오류: 종료\n{ex}"); + return false; } } - + + } \ No newline at end of file diff --git a/Program/V1/Models/User.cs b/Program/V1/Models/User.cs index c1c0871..4aecbef 100644 --- a/Program/V1/Models/User.cs +++ b/Program/V1/Models/User.cs @@ -8,10 +8,15 @@ namespace AcaMate.V1.Models; public class Login { [Key] + [Required(ErrorMessage = "필수 항목 누락")] [MaxLength(100)] public string sns_id {get; set;} + + [Required(ErrorMessage = "필수 항목 누락")] [MaxLength(70)] public string uid {get; set;} + + [Required(ErrorMessage = "필수 항목 누락")] [MaxLength(4)] public string sns_type {get; set;} } @@ -20,9 +25,16 @@ public class Login public class User_Academy { [Key] + [Required(ErrorMessage = "필수 항목 누락")] public string uid { get; set; } + + [Required(ErrorMessage = "필수 항목 누락")] public string bid { get; set; } + + [Required(ErrorMessage = "필수 항목 누락")] public DateTime register_date { get; set; } + + [Required(ErrorMessage = "필수 항목 누락")] public bool status { get; set; } } @@ -30,12 +42,22 @@ public class User_Academy public class User { [Key] + [Required(ErrorMessage = "필수 항목 누락")] public string uid { get; set; } + + [Required(ErrorMessage = "필수 항목 누락")] public string name { get; set; } - public DateTime birth { get; set; } + public DateTime? birth { get; set; } + + [Required(ErrorMessage = "필수 항목 누락")] public string type { get; set; } - public string device_id { get; set; } - public int auto_login_yn { get; set; } + + public string? device_id { get; set; } + + [Required(ErrorMessage = "필수 항목 누락")] + public bool auto_login_yn { get; set; } + + [Required(ErrorMessage = "필수 항목 누락")] public DateTime login_date { get; set; } } @@ -44,6 +66,8 @@ public class User public class Permission { [Key] + + [Required(ErrorMessage = "필수 항목 누락")] public string uid { get; set; } public bool location_yn {get; set;} @@ -59,10 +83,20 @@ public class Permission public class Token { [Key] + + [Required(ErrorMessage = "필수 항목 누락")] public string uid { get; set; } + + [Required(ErrorMessage = "필수 항목 누락")] public string refresh_token { get; set; } + + [Required(ErrorMessage = "필수 항목 누락")] public DateTime create_date { get; set; } + + [Required(ErrorMessage = "필수 항목 누락")] public DateTime expires_date { get; set; } + + [Required(ErrorMessage = "필수 항목 누락")] public DateTime revoke_date { get; set; } } @@ -70,8 +104,14 @@ public class Token public class Location { [Key] + + [Required(ErrorMessage = "필수 항목 누락")] public string uid { get; set; } + + [Required(ErrorMessage = "필수 항목 누락")] public string lat { get; set; } + + [Required(ErrorMessage = "필수 항목 누락")] public string lng { get; set; } } @@ -79,25 +119,40 @@ public class Location public class Contact { [Key] + + [Required(ErrorMessage = "필수 항목 누락")] public string uid { get; set; } + + [Required(ErrorMessage = "필수 항목 누락")] public string email { get; set; } - public string phone { get; set; } + public string? phone { get; set; } + public string? address { get; set; } } // -- -- -- -- -- DB 테이블 -- -- -- -- -- // public class UserAll { - public string uid { get; set; } - public string name { get; set; } - public DateTime birth { get; set; } - public string type { get; set; } - public string device_id { get; set; } - public int auto_login_yn { get; set; } - public DateTime login_date { get; set; } + // [Required(ErrorMessage = "필수 항목 누락")] + // public string uid { get; set; } + [Required(ErrorMessage = "필수 항목 누락")] + public string name { get; set; } + + public DateTime? birth { get; set; } + + [Required(ErrorMessage = "필수 항목 누락")] + public string type { get; set; } + + public string? device_id { get; set; } + + public bool auto_login_yn { get; set; } + public DateTime login_date { get; set; } + + [Required(ErrorMessage = "필수 항목 누락")] public string email { get; set; } - public string phone { get; set; } + public string? phone { get; set; } + public string? address { get; set; } public bool location_yn {get; set;} public bool camera_yn {get; set;} @@ -107,7 +162,9 @@ public class UserAll public bool market_sms_yn {get; set;} public bool market_email_yn {get; set;} + [Required(ErrorMessage = "필수 항목 누락")] public string sns_id {get; set;} + + [Required(ErrorMessage = "필수 항목 누락")] public string sns_type {get; set;} - public string sns_email {get; set;} } \ No newline at end of file diff --git a/appsettings.json b/appsettings.json index ec04bc1..d66442c 100644 --- a/appsettings.json +++ b/appsettings.json @@ -1,7 +1,7 @@ { "Logging": { "LogLevel": { - "Default": "Information", + "Default": "Warning", "Microsoft.AspNetCore": "Warning" } }, diff --git a/private/jwtSetting.Development.json b/private/jwtSetting.Development.json index 3af0d78..acae7f3 100755 --- a/private/jwtSetting.Development.json +++ b/private/jwtSetting.Development.json @@ -1,6 +1,6 @@ { "JwtSettings": { - "SecretKey": "YourSuperSecretKeyThatIsLongEnough123!", + "SecretKey": "QWNhTWF0ZS1TZWNyZXRLZXlfTWFkZUJ5J1RlYW0uU3RlaW5BbmRPd25lclNlYW5BbmRJbXBvcnRhbnRfTnVtYmVyLTk0MDUwOSE=", "Issuer": "AcaMate", "Audience": "https:/devacamate.ipstein.myds.me", "ExpiryMinutes": 10, diff --git a/private/jwtSetting.json b/private/jwtSetting.json index ebc7ca1..3550ba2 100755 --- a/private/jwtSetting.json +++ b/private/jwtSetting.json @@ -1,11 +1,15 @@ { "JwtSettings": { - "SecretKey": "YourSuperSecretKeyThatIsLongEnough123!", + "SecretKey": "QWNhTWF0ZS1TZWNyZXRLZXlfTWFkZUJ5J1RlYW0uU3RlaW5BbmRPd25lclNlYW5BbmRJbXBvcnRhbnRfTnVtYmVyLTk0MDUwOSE=", "Issuer": "AcaMate", "Audience": "https://acamate.ipstein.myds.me", + // 엑세스 토큰 유효기간 (분) "ExpiryMinutes": 10, + // 서버와 클라이언트 간 시간차이 보정 (분) "ClockSkewMinutes": 5, + // 리프레시 토큰의 유효기간 (일) "RefreshTokenExpiryDays": 7 } } -// SecretKey 의 길이는 최소 256bit(32byte)는 넘어야 함 \ No newline at end of file +// SecretKey 의 길이는 최소 256bit(32byte)는 넘어야 함 +// "SecretKey": "AcaMate-SecretKey_MadeBy'Team.SteinAndOwnerSeanAndImportant_Number-940509!",