Skip to content
Merged

Dev #15

Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions backend/Controllers/UsersController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,17 @@ public async Task<IActionResult> GetByUsername(string username)
}
return Ok(user);
}

[HttpPut("me")]
public async Task<IActionResult> UpdateProfile([FromBody] UserUpdateDto dto)
{
var uidClaim = User.FindFirst("uid")?.Value;
if (uidClaim == null)
return Unauthorized();

var userId = Guid.Parse(uidClaim);

await _service.UpdateProfileAsync(userId, dto);
return NoContent();
}
}
8 changes: 8 additions & 0 deletions backend/DTOs/User/UserDTO.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,11 @@ public class UserResponseDto
public string Role { get; set; }
}

public class UserUpdateDto
{
public string FirstName { get; set; } = null!;
public string LastName { get; set; } = null!;
public string Email { get; set; } = null!;
public string? Phone { get; set; }
public string? Address { get; set; }
}
114 changes: 93 additions & 21 deletions backend/Middleware/GlobalLogger.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System.IO;
using Npgsql;
using System.Diagnostics;
using System.Security.Claims;
using System.Text;

namespace backend.Middleware;
Expand All @@ -10,37 +12,107 @@ public class GlobalRequestLoggingMiddleware
private readonly RequestDelegate _next;
private readonly ILogger<GlobalRequestLoggingMiddleware> _logger;

public GlobalRequestLoggingMiddleware(RequestDelegate next, ILogger<GlobalRequestLoggingMiddleware> logger)
public GlobalRequestLoggingMiddleware(
RequestDelegate next,
ILogger<GlobalRequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}

public async Task InvokeAsync(HttpContext context)
{
// Log incoming request
_logger.LogInformation("Incoming Request: {Method} {Path}", context.Request.Method, context.Request.Path);
var stopwatch = Stopwatch.StartNew();
var correlationId = context.TraceIdentifier;

// Log request body for POST/PUT
if (context.Request.Method == HttpMethods.Post || context.Request.Method == HttpMethods.Put)
{
context.Request.EnableBuffering();
using var reader = new StreamReader(context.Request.Body, Encoding.UTF8, leaveOpen: true);
var body = await reader.ReadToEndAsync();
context.Request.Body.Position = 0;
if (!string.IsNullOrWhiteSpace(body))
_logger.LogInformation("Request Body: {Body}", body);
}
// Extract user info from JWT (if exists)
var isAuthenticated = context.User?.Identity?.IsAuthenticated ?? false;
var userId = context.User?.Claims?.FirstOrDefault(c => c.Type == "uid")?.Value;
var username = context.User?.Claims?.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value
?? context.User?.Claims?.FirstOrDefault(c => c.Type == "sub")?.Value;

try
using (_logger.BeginScope(new Dictionary<string, object>
{
await _next(context); // call next middleware/controller
_logger.LogInformation("Response Status: {StatusCode}", context.Response.StatusCode);
}
catch (Exception ex)
["CorrelationId"] = correlationId
}))
{
_logger.LogError(ex, "Unhandled exception for request {Method} {Path}", context.Request.Method, context.Request.Path);
throw;
// ---- REQUEST LOGGING ----
_logger.LogInformation(
"Incoming Request | {Method} {Path} | Auth={Auth} | UserId={UserId} | Username={Username}",
context.Request.Method,
context.Request.Path,
isAuthenticated,
userId ?? "anonymous",
username ?? "anonymous"
);

// Log request body (SAFE endpoints only)
if ((context.Request.Method == HttpMethods.Post ||
context.Request.Method == HttpMethods.Put) &&
!context.Request.Path.StartsWithSegments("/auth"))
{
context.Request.EnableBuffering();

using var reader = new StreamReader(
context.Request.Body,
Encoding.UTF8,
leaveOpen: true);

var body = await reader.ReadToEndAsync();
context.Request.Body.Position = 0;

if (!string.IsNullOrWhiteSpace(body))
{
_logger.LogInformation("Request Body: {Body}", body);
}
}

try
{
await _next(context); // Continue pipeline

stopwatch.Stop();

// ---- RESPONSE LOGGING ----
_logger.LogInformation(
"Response | {Method} {Path} | Status={StatusCode} | Time={ElapsedMs}ms",
context.Request.Method,
context.Request.Path,
context.Response.StatusCode,
stopwatch.ElapsedMilliseconds
);
}
catch (PostgresException pgEx)
{
stopwatch.Stop();

// ---- DATABASE ERROR LOGGING ----
_logger.LogError(
pgEx,
"Postgres Error | SqlState={SqlState} | Constraint={Constraint} | Message={Message} | Time={ElapsedMs}ms",
pgEx.SqlState,
pgEx.ConstraintName,
pgEx.MessageText,
stopwatch.ElapsedMilliseconds
);

throw;
}
catch (Exception ex)
{
stopwatch.Stop();

// ---- UNHANDLED ERROR LOGGING ----
_logger.LogError(
ex,
"Unhandled Exception | {Method} {Path} | Time={ElapsedMs}ms",
context.Request.Method,
context.Request.Path,
stopwatch.ElapsedMilliseconds
);

throw;
}
}
}
}
5 changes: 4 additions & 1 deletion backend/Repositories/User/IUserRepository.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using backend.Models;
using backend.DTOs;

namespace backend.Repositories;

Expand All @@ -8,7 +9,9 @@ public interface IUserRepository
Task<User?> GetByEmailAsync(string email);
Task<User?> GetByLoginAsync(string login);
Task<IEnumerable<User>> GetAllAsync();
Task<Guid> CreateAsync(User user);
Task CreateAsync(User user);
Task<User?> GetByIdAsync(Guid id);
Task UpdateProfileAsync(Guid id, UserUpdateDto dto);


}
35 changes: 33 additions & 2 deletions backend/Repositories/User/UserRepository.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Data;
using Dapper;
using backend.Models;
using backend.DTOs;

namespace backend.Repositories;

Expand Down Expand Up @@ -40,15 +41,45 @@ public async Task<IEnumerable<User>> GetAllAsync()

}

public async Task<Guid> CreateAsync(User user)
public async Task CreateAsync(User user)
{
const string sql = @"
INSERT INTO ""user"" (username, password, last_name, first_name, email, phone, address, role)
VALUES (@Username, @Password, @LastName, @FirstName, @Email, @Phone, @Address, @Role)
RETURNING u_id;
";

return await _db.ExecuteScalarAsync<Guid>(sql, user);
user.UId = await _db.ExecuteScalarAsync<Guid>(sql, user);
}

public async Task<User?> GetByIdAsync(Guid id)
{
const string sql = @"SELECT * FROM ""user"" WHERE u_id = @Id;";
return await _db.QuerySingleOrDefaultAsync<User>(sql, new { Id = id });
}

public async Task UpdateProfileAsync(Guid id, UserUpdateDto dto)
{
const string sql = @"
UPDATE ""user""
SET
first_name = @FirstName,
last_name = @LastName,
email = @Email,
phone = @Phone,
address = @Address
WHERE u_id = @Id;
";

await _db.ExecuteAsync(sql, new
{
Id = id,
dto.FirstName,
dto.LastName,
dto.Email,
dto.Phone,
dto.Address
});
}

}
1 change: 1 addition & 0 deletions backend/Services/User/IUserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public interface IUserService
Task<UserResponseDto?> GetUserByUsernameAsync(string username);
Task<IEnumerable<UserResponseDto>> GetAllUsersAsync();
Task<AuthResponseDto> RefreshAsync(string refreshToken);
Task UpdateProfileAsync(Guid userId, UserUpdateDto dto);

}

Expand Down
19 changes: 19 additions & 0 deletions backend/Services/User/UserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,16 @@
FirstName = dto.FirstName,
LastName = dto.LastName,
Email = dto.Email,
Phone = dto.Phone,

Check warning on line 42 in backend/Services/User/UserService.cs

View workflow job for this annotation

GitHub Actions / Backend Unit Tests

Possible null reference assignment.

Check warning on line 42 in backend/Services/User/UserService.cs

View workflow job for this annotation

GitHub Actions / Backend Unit Tests

Possible null reference assignment.
Address = dto.Address,

Check warning on line 43 in backend/Services/User/UserService.cs

View workflow job for this annotation

GitHub Actions / Backend Unit Tests

Possible null reference assignment.

Check warning on line 43 in backend/Services/User/UserService.cs

View workflow job for this annotation

GitHub Actions / Backend Unit Tests

Possible null reference assignment.
Role = "Customer"
};

user.Password = _passwordHasher.HashPassword(user, dto.Password);

await _repo.CreateAsync(user);
if (user.UId == Guid.Empty)
throw new Exception("User ID was not generated");

var accessToken = GenerateJwt(user, false); // short-lived access token
var refreshToken = GenerateJwt(user, true); // long-lived refresh token
Expand Down Expand Up @@ -76,14 +78,16 @@
FirstName = dto.FirstName,
LastName = dto.LastName,
Email = dto.Email,
Phone = dto.Phone,

Check warning on line 81 in backend/Services/User/UserService.cs

View workflow job for this annotation

GitHub Actions / Backend Unit Tests

Possible null reference assignment.

Check warning on line 81 in backend/Services/User/UserService.cs

View workflow job for this annotation

GitHub Actions / Backend Unit Tests

Possible null reference assignment.
Address = dto.Address,

Check warning on line 82 in backend/Services/User/UserService.cs

View workflow job for this annotation

GitHub Actions / Backend Unit Tests

Possible null reference assignment.

Check warning on line 82 in backend/Services/User/UserService.cs

View workflow job for this annotation

GitHub Actions / Backend Unit Tests

Possible null reference assignment.
Role = "Admin"
};

user.Password = _passwordHasher.HashPassword(user, dto.Password);

await _repo.CreateAsync(user);
if (user.UId == Guid.Empty)
throw new Exception("User ID was not generated");

var accessToken = GenerateJwt(user, false); // short-lived access token
var refreshToken = GenerateJwt(user, true); // long-lived refresh token
Expand Down Expand Up @@ -122,7 +126,7 @@
// Helper method to generate JWT
private string GenerateJwt(User user, bool isRefresh = false)
{
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));

Check warning on line 129 in backend/Services/User/UserService.cs

View workflow job for this annotation

GitHub Actions / Backend Unit Tests

Possible null reference argument for parameter 's' in 'byte[] Encoding.GetBytes(string s)'.

Check warning on line 129 in backend/Services/User/UserService.cs

View workflow job for this annotation

GitHub Actions / Backend Unit Tests

Possible null reference argument for parameter 's' in 'byte[] Encoding.GetBytes(string s)'.
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

var claims = new[]
Expand Down Expand Up @@ -150,7 +154,7 @@
public async Task<AuthResponseDto> RefreshAsync(string refreshToken)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.UTF8.GetBytes(_config["Jwt:Key"]);

Check warning on line 157 in backend/Services/User/UserService.cs

View workflow job for this annotation

GitHub Actions / Backend Unit Tests

Possible null reference argument for parameter 's' in 'byte[] Encoding.GetBytes(string s)'.

Check warning on line 157 in backend/Services/User/UserService.cs

View workflow job for this annotation

GitHub Actions / Backend Unit Tests

Possible null reference argument for parameter 's' in 'byte[] Encoding.GetBytes(string s)'.

try
{
Expand Down Expand Up @@ -233,4 +237,19 @@
};
}

public async Task UpdateProfileAsync(Guid userId, UserUpdateDto dto)
{
var existingUser = await _repo.GetByIdAsync(userId);
if (existingUser == null)
throw new Exception("User not found");

// Optional: prevent email duplication
var emailOwner = await _repo.GetByEmailAsync(dto.Email);
if (emailOwner != null && emailOwner.UId != userId)
throw new Exception("Email already in use");

await _repo.UpdateProfileAsync(userId, dto);
}


}
Loading
Loading