Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e7dc7b1
feat: oauth2를 redis를 통한 로그인 방식으로 수정
ImGdevel Aug 31, 2025
f2a0326
fix: db context 연결 문제 해결
ImGdevel Aug 31, 2025
c1da9dd
fix: 서버내 메시지 타입 수정
ImGdevel Sep 1, 2025
b7a683d
refactory: 메시지 타입 개선
ImGdevel Sep 1, 2025
055f2e0
feat: 메시지 전송 Build 패턴 적용
ImGdevel Sep 1, 2025
5eaaa99
refactory: 출력 포맷 변경
ImGdevel Sep 1, 2025
4e83965
fix: 단답형 질문에 대해 막히는 문제 수정
ImGdevel Sep 1, 2025
327fa2c
feat: 캐릭터 설정
ImGdevel Sep 1, 2025
f2d8a32
feat: 캐릭터 소유권 지정 및 추가
ImGdevel Sep 2, 2025
8aa4c24
feat: 캐릭터 공개 여부 기능 추가
ImGdevel Sep 2, 2025
0cb7930
feat: 캐릭터 클라이언트
ImGdevel Sep 2, 2025
0986638
feat: 대화 기록 기능 고도화
ImGdevel Sep 3, 2025
105857f
feat: 더미 클라이언트에 대화 기록 조회 기능 추가
ImGdevel Sep 3, 2025
c82bd41
test: 테스트 코드 수정
ImGdevel Sep 3, 2025
e29a9fb
feat: 토큰 기능 개발
ImGdevel Sep 3, 2025
c280132
feat: 토큰 지급과 비용 추적
ImGdevel Sep 3, 2025
d4048d4
test: 토큰 지금 관련 테스트 코드 추가
ImGdevel Sep 3, 2025
e6ea4a2
refactory: 명칭 변경 Token-> Credit
ImGdevel Sep 3, 2025
9ad211b
로그인 로직 수정
ImGdevel Sep 3, 2025
41cdc92
refactory: 로그인로직 리팩토링
ImGdevel Sep 3, 2025
f48c908
refactor: Domain 엔티티 Data Annotation 도입 및 주석 최소화
ImGdevel Sep 3, 2025
cb84fbb
refactor: Repository 인터페이스를 Domain 모듈로 이동
ImGdevel Sep 3, 2025
511bf25
refactor(auth, character): 사용자 인증 및 캐릭터 관리 개선
ImGdevel Sep 4, 2025
3d399a4
TTS 인프라 개선
ImGdevel Sep 4, 2025
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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@ Thumbs.db
.DS_Store
Desktop.ini

# Shell configuration files
.zshrc
.bashrc
.bash_profile
.profile

# Crash dumps
*.dmp

Expand Down
2 changes: 1 addition & 1 deletion ProjectVG.Api/ApiServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public static IServiceCollection AddDevelopmentCors(this IServiceCollection serv
services.AddCors(options => {
options.AddPolicy("AllowAll",
policy => policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()
.WithExposedHeaders("X-Access-Token", "X-Refresh-Token", "X-Expires-In", "X-UID"));
.WithExposedHeaders("X-Access-Credit", "X-Refresh-Credit", "X-Expires-In", "X-UID"));
});

return services;
Expand Down
33 changes: 17 additions & 16 deletions ProjectVG.Api/Controllers/AuthController.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using Microsoft.AspNetCore.Mvc;
using ProjectVG.Application.Services.Auth;
using ProjectVG.Common.Constants;
using ProjectVG.Common.Exceptions;

namespace ProjectVG.Api.Controllers
{
[ApiController]
[Route("api/v1/[controller]")]
[Route("api/v1/auth")]
public class AuthController : ControllerBase
{
private readonly IAuthService _authService;
Expand All @@ -14,14 +16,16 @@ public AuthController(IAuthService authService)
_authService = authService;
}

/// <summary>
/// Access Token 갱신
/// </summary>
[HttpPost("refresh")]
public async Task<IActionResult> RefreshToken()
{
var refreshToken = GetRefreshTokenFromHeader();
var result = await _authService.RefreshTokenAsync(refreshToken);

return Ok(new
{
var result = await _authService.RefreshAccessTokenAsync(refreshToken);

return Ok(new {
success = true,
tokens = result.Tokens,
user = result.User
Expand All @@ -33,9 +37,8 @@ public async Task<IActionResult> Logout()
{
var refreshToken = GetRefreshTokenFromHeader();
var success = await _authService.LogoutAsync(refreshToken);

return Ok(new
{

return Ok(new {
success = success,
message = success ? "Logout successful" : "Logout failed"
});
Expand All @@ -44,15 +47,13 @@ public async Task<IActionResult> Logout()
[HttpPost("guest-login")]
public async Task<IActionResult> GuestLogin([FromBody] string guestId)
{
if (string.IsNullOrEmpty(guestId))
{
if (string.IsNullOrEmpty(guestId)) {
throw new ValidationException(ErrorCode.GUEST_ID_INVALID);
}

var result = await _authService.LoginWithOAuthAsync("guest", guestId);

return Ok(new
{
var result = await _authService.GuestLoginAsync(guestId);

return Ok(new {
success = true,
tokens = result.Tokens,
user = result.User
Expand All @@ -61,7 +62,7 @@ public async Task<IActionResult> GuestLogin([FromBody] string guestId)

private string GetRefreshTokenFromHeader()
{
return Request.Headers["X-Refresh-Token"].FirstOrDefault() ?? string.Empty;
return Request.Headers["X-Refresh-Credit"].FirstOrDefault() ?? string.Empty;
}
}
}
}
82 changes: 73 additions & 9 deletions ProjectVG.Api/Controllers/CharacterController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
using ProjectVG.Application.Models.Character;
using ProjectVG.Api.Models.Character.Request;
using ProjectVG.Api.Models.Character.Response;
using ProjectVG.Api.Filters;
using System.Security.Claims;

namespace ProjectVG.Api.Controllers
{
Expand All @@ -19,6 +21,16 @@ public CharacterController(ICharacterService characterService, ILogger<Character
_logger = logger;
}

private Guid? GetCurrentUserId()
{
var userIdClaim = User.FindFirst("user_id")?.Value;
if (Guid.TryParse(userIdClaim, out var userId))
{
return userId;
}
return null;
}
Comment on lines +24 to +32
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

클레임 키 불일치로 사용자 ID를 읽지 못함 (user_id vs NameIdentifier).

테스트/다른 컨트롤러는 ClaimTypes.NameIdentifier를 사용합니다. 현재 구현은 user_id만 읽어 인증이 항상 실패할 수 있습니다. 호환 검색으로 보완하세요.

- private Guid? GetCurrentUserId()
- {
-     var userIdClaim = User.FindFirst("user_id")?.Value;
-     if (Guid.TryParse(userIdClaim, out var userId))
-     {
-         return userId;
-     }
-     return null;
- }
+ private Guid? GetCurrentUserId()
+ {
+     var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value
+                        ?? User.FindFirst("user_id")?.Value; // 호환
+     return Guid.TryParse(userIdClaim, out var userId) ? userId : null;
+ }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private Guid? GetCurrentUserId()
{
var userIdClaim = User.FindFirst("user_id")?.Value;
if (Guid.TryParse(userIdClaim, out var userId))
{
return userId;
}
return null;
}
private Guid? GetCurrentUserId()
{
var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value
?? User.FindFirst("user_id")?.Value; // 호환
return Guid.TryParse(userIdClaim, out var userId) ? userId : null;
}
🤖 Prompt for AI Agents
In ProjectVG.Api/Controllers/CharacterController.cs around lines 24 to 32,
GetCurrentUserId only reads a claim named "user_id" which mismatches other parts
of the app that use ClaimTypes.NameIdentifier; update the method to attempt
reading ClaimTypes.NameIdentifier first, then fall back to "user_id" (or vice
versa depending on preferred precedence), safely handle null/empty values, parse
the claim value with Guid.TryParse and return the parsed Guid or null; ensure
you reference System.Security.Claims.ClaimTypes for the NameIdentifier constant
and keep the method behavior identical otherwise.


[HttpGet]
public async Task<ActionResult<IEnumerable<CharacterResponse>>> GetAllCharacters()
{
Expand All @@ -35,29 +47,81 @@ public async Task<ActionResult<CharacterResponse>> GetCharacterById(Guid id)
return Ok(response);
}

[HttpPost]
public async Task<ActionResult<CharacterResponse>> CreateCharacter([FromBody] CreateCharacterRequest request)
[HttpPost("individual")]
[JwtAuthentication]
public async Task<ActionResult<CharacterResponse>> CreateCharacterWithFields([FromBody] CreateCharacterWithFieldsRequest request)
{
var command = request.ToCreateCharacterCommand();
var characterDto = await _characterService.CreateCharacterAsync(command);
var userId = GetCurrentUserId();
var command = request.ToCommand(userId);
var characterDto = await _characterService.CreateCharacterWithFieldsAsync(command);
var response = CharacterResponse.ToResponseDto(characterDto);
return CreatedAtAction(nameof(GetCharacterById), new { id = response.Id }, response);
}

[HttpPut("{id}")]
public async Task<ActionResult<CharacterResponse>> UpdateCharacter(Guid id, [FromBody] UpdateCharacterRequest request)
[HttpPost("systemprompt")]
[JwtAuthentication]
public async Task<ActionResult<CharacterResponse>> CreateCharacterWithSystemPrompt([FromBody] CreateCharacterWithSystemPromptRequest request)
{
var userId = GetCurrentUserId();
var command = request.ToCommand(userId);
var characterDto = await _characterService.CreateCharacterWithSystemPromptAsync(command);
var response = CharacterResponse.ToResponseDto(characterDto);
return CreatedAtAction(nameof(GetCharacterById), new { id = response.Id }, response);
}

[HttpPut("{id}/individual")]
public async Task<ActionResult<CharacterResponse>> UpdateCharacterToIndividual(Guid id, [FromBody] UpdateCharacterToIndividualRequest request)
{
var command = request.ToCommand(id);
var characterDto = await _characterService.UpdateCharacterToIndividualAsync(command);
var response = CharacterResponse.ToResponseDto(characterDto);
return Ok(response);
}
Comment on lines +72 to +79
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

업데이트 API에 인증/권한 검증 누락

두 엔드포인트 모두 인증 특성과 소유자 검증이 없습니다. 최소한 컨트롤러에서 차단하고, 궁극적으로 서비스 계층에서 강제하세요.

-[HttpPut("{id}/individual")]
-public async Task<ActionResult<CharacterResponse>> UpdateCharacterToIndividual(Guid id, [FromBody] UpdateCharacterToIndividualRequest request)
+[HttpPut("{id}/individual")]
+[JwtAuthentication]
+public async Task<ActionResult<CharacterResponse>> UpdateCharacterToIndividual(Guid id, [FromBody] UpdateCharacterToIndividualRequest request)
 {
-    var command = request.ToCommand(id);
-    var characterDto = await _characterService.UpdateCharacterToIndividualAsync(command);
+    var userId = GetCurrentUserId();
+    if (!userId.HasValue) return Unauthorized();
+    var current = await _characterService.GetCharacterByIdAsync(id);
+    if (current.CreatedByUserId != userId.Value) return Forbid();
+    var command = request.ToCommand(id);
+    var characterDto = await _characterService.UpdateCharacterToIndividualAsync(command/* , userId.Value */);
     ...
 }
 
-[HttpPut("{id}/systemprompt")]
-public async Task<ActionResult<CharacterResponse>> UpdateCharacterToSystemPrompt(Guid id, [FromBody] UpdateCharacterToSystemPromptRequest request)
+[HttpPut("{id}/systemprompt")]
+[JwtAuthentication]
+public async Task<ActionResult<CharacterResponse>> UpdateCharacterToSystemPrompt(Guid id, [FromBody] UpdateCharacterToSystemPromptRequest request)
 {
-    var command = request.ToCommand(id);
-    var characterDto = await _characterService.UpdateCharacterToSystemPromptAsync(command);
+    var userId = GetCurrentUserId();
+    if (!userId.HasValue) return Unauthorized();
+    var current = await _characterService.GetCharacterByIdAsync(id);
+    if (current.CreatedByUserId != userId.Value) return Forbid();
+    var command = request.ToCommand(id);
+    var characterDto = await _characterService.UpdateCharacterToSystemPromptAsync(command/* , userId.Value */);
     ...
 }

추가로 TOCTOU 방지를 위해 서비스 메서드에 userId를 전달하고 내부에서 IsOwnedBy를 검증하도록 시그니처 변경을 권장합니다(별도 코멘트 참조).

Also applies to: 81-88

🤖 Prompt for AI Agents
In ProjectVG.Api/Controllers/CharacterController.cs around lines 72-79 (and
similarly 81-88) the update endpoints lack authentication and owner
authorization checks; add the [Authorize] attribute to the controller or these
actions, extract the current user's id from the JWT/ClaimsPrincipal in the
controller, pass that userId into the service call (change
UpdateCharacterToIndividualAsync signature to accept userId), and have the
service verify ownership (IsOwnedBy) and throw a 403/Forbidden if not owned to
prevent TOCTOU; ensure controller returns appropriate Forbidden/Unauthorized
responses when checks fail.


[HttpPut("{id}/systemprompt")]
public async Task<ActionResult<CharacterResponse>> UpdateCharacterToSystemPrompt(Guid id, [FromBody] UpdateCharacterToSystemPromptRequest request)
{
var command = request.ToUpdateCharacterCommand();
var characterDto = await _characterService.UpdateCharacterAsync(id, command);
var command = request.ToCommand(id);
var characterDto = await _characterService.UpdateCharacterToSystemPromptAsync(command);
var response = CharacterResponse.ToResponseDto(characterDto);
return Ok(response);
}
Comment on lines +81 to 88
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

SystemPrompt 수정 API에도 인증/권한 검증 추가.

-[HttpPut("{id}/systemprompt")]
+[HttpPut("{id}/systemprompt")]
+[JwtAuthentication]
 public async Task<ActionResult<CharacterResponse>> UpdateCharacterToSystemPrompt(Guid id, [FromBody] UpdateCharacterToSystemPromptRequest request)
 {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
[HttpPut("{id}/systemprompt")]
public async Task<ActionResult<CharacterResponse>> UpdateCharacterToSystemPrompt(Guid id, [FromBody] UpdateCharacterToSystemPromptRequest request)
{
var command = request.ToUpdateCharacterCommand();
var characterDto = await _characterService.UpdateCharacterAsync(id, command);
var command = request.ToCommand(id);
var characterDto = await _characterService.UpdateCharacterToSystemPromptAsync(command);
var response = CharacterResponse.ToResponseDto(characterDto);
return Ok(response);
}
[HttpPut("{id}/systemprompt")]
[JwtAuthentication]
public async Task<ActionResult<CharacterResponse>> UpdateCharacterToSystemPrompt(Guid id, [FromBody] UpdateCharacterToSystemPromptRequest request)
{
var command = request.ToCommand(id);
var characterDto = await _characterService.UpdateCharacterToSystemPromptAsync(command);
var response = CharacterResponse.ToResponseDto(characterDto);
return Ok(response);
}
🤖 Prompt for AI Agents
In ProjectVG.Api/Controllers/CharacterController.cs around lines 81 to 88, the
UpdateCharacterToSystemPrompt endpoint lacks authentication and authorization
checks; add the [Authorize] attribute to the action (or controller) and enforce
ownership/permission before updating: extract the caller user id from the
HttpContext/User claims, call or add a service method to verify the user is
owner or has the required role/permission for the character id (return
Unauthorized() if unauthenticated or Forbid() if not permitted), and only then
call _characterService.UpdateCharacterToSystemPromptAsync and return the
response.


[HttpDelete("{id}")]
[JwtAuthentication]
public async Task<ActionResult> DeleteCharacter(Guid id)
{
await _characterService.DeleteCharacterAsync(id);
var userId = GetCurrentUserId();
if (!userId.HasValue)
{
return Unauthorized();
}

await _characterService.DeleteCharacterAsync(id, userId.Value);
return NoContent();
}

[HttpGet("my")]
[JwtAuthentication]
public async Task<ActionResult<IEnumerable<CharacterResponse>>> GetMyCharacters([FromQuery] string orderBy = "latest")
{
var userId = GetCurrentUserId();
if (!userId.HasValue)
{
return Unauthorized();
}

var characterDtos = await _characterService.GetMyCharactersAsync(userId.Value, orderBy);
var responses = characterDtos.Select(CharacterResponse.ToResponseDto);
return Ok(responses);
}

[HttpGet("public")]
public async Task<ActionResult<IEnumerable<CharacterResponse>>> GetPublicCharacters([FromQuery] string orderBy = "latest")
{
var characterDtos = await _characterService.GetPublicCharactersAsync(orderBy);
var responses = characterDtos.Select(CharacterResponse.ToResponseDto);
return Ok(responses);
}
}
}
2 changes: 1 addition & 1 deletion ProjectVG.Api/Controllers/ChatController.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Mvc;
using ProjectVG.Application.Models.Chat;
using ProjectVG.Application.Models.API.Request;
using ProjectVG.Api.Models.Chat.Request;
using ProjectVG.Application.Services.Chat;
using System.Security.Claims;

Expand Down
Loading