vault backup: 2025-06-02 10:35:28

This commit is contained in:
2025-06-02 10:35:28 +03:30
parent b002a6d740
commit c16110d0b5
4 changed files with 1616 additions and 82 deletions
@@ -0,0 +1,527 @@
# Add a field in Database
- [ ] Add `User` field in `Roles` table
# Branching
 - [ ] Create the feature/authentication branch based on develop
# Adjusting Account and Configurations
- [ ] Add navigation property for `AccountRoles` in Account
```c#
public virtual ICollection<AccountRole> AccountRoles { get; set; }
```
- [ ] Add navigation properties for Account and Role in `AccountRole`
```C#
public virtual Role Role { get; set; }
public virtual Account Account{ get; set; }
```
- [ ] Update the entity configuration to reflect relationship mappings:
```c#
builder.HasOne<Account>(ar => ar.Account)
.WithMany(a => a.AccountRoles)
.HasForeignKey(ar => ar.AccountId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne<Role>(ar => ar.Role)
.WithMany()
.HasForeignKey(ar => ar.RoleId)
.OnDelete(DeleteBehavior.Restrict);
```
# Creating DTOs
📂 Suggested Folder: ApplicationLayer/DTOs/[RelatedFolder]
## AccountDto
- [ ] Create `AccountDto` to expose relevant account information:
```c#
public class AccountDto
{
public long Id { get; set; }
public required string PhoneNumber { get; set; }
public required string Password { set; get; }
public string? Email { get; set; }
public long? PersonId { get; set; }
public List<string> Roles { get; set; }
}
```
## AuthResponseDto
- [ ] Define a DTO for authentication responses:
```c#
public class AuthResponseDto
{
public long Id { get; set; }
public string Token { get; set; } = null!;
public string PhoneNumber { get; set; } = null!;
public List<string> Roles { get; set; }
}
```
- [ ] Define a DTO for login requests:
```c#
public class LoginRequestDto
{
public string PhoneNumber { get; set; } = null!;
public string Password { get; set; } = null!;
}
```
## RegisterRequestDto
- [ ] Define a DTO for registration with validation attributes:
```c#
public class RegisterRequestDto
{
[Required(ErrorMessage = "Phone number is required.")]
[Phone(ErrorMessage = "Phone number format is invalid.")]
public required string PhoneNumber { get; set; }
[Required(ErrorMessage = "Password is required.")]
[MinLength(6, ErrorMessage = "Password must be at least 6 characters long.")]
public required string Password { get; set; }
[Compare("Password", ErrorMessage = "Passwords do not match.")]
public required string ConfirmPassword { get; set; }
}
```
### 🔹 **1. What do the annotations like `[Required]`, `[Phone]`, `[MinLength]`, `[Compare]` on the DTO do?**
These are **Data Annotations** from `System.ComponentModel.DataAnnotations`.
Theyre used by:
- The ASP.NET Core `[ApiController]` attribute
- **Model binding & automatic validation**
**What happens:**
If your controller is marked with `[ApiController]`, ASP.NET Core will **automatically validate** the DTO against these annotations **before entering your action method**.
Example:
```csharp
[ApiController]
public class AuthController : ControllerBase
```
Then this:
```csharp
[HttpPost("register")]
public async Task<IActionResult> Register(RegisterRequestDto dto)
```
If `dto.PhoneNumber` is missing, it **wont even run your logic**, and will return a `400 Bad Request` with validation errors.
> “Why are these here if my frontend is separate?”
✅ **Answer**: They're still useful:
- For **security and safety**: you _must_ validate on the backend — never trust the frontend.
- For **auto validation** before hitting your logic — saving you boilerplate checks.
- You can use them for **Swagger/OpenAPI documentation** as well.
Frontend validation is for **user experience**, not security.
---
### 🔹 **2. Where should password requirements be checked? Frontend or backend?**
✅ **Both.**
- **Frontend**: show real-time UX feedback (“Password must be 6+ characters”).
- **Backend**: enforce security.
**Backend is the source of truth.**
Frontend can be bypassed (e.g., Postman).
In the backend, you can either:
- Use annotations like `[MinLength(6)]`
- Or do manual checks:
```csharp
if (dto.Password.Length < 6)
return BadRequest("Password must be at least 6 characters long.");
```
---
### 🔹 **3. Should confirm password be in the backend?**
✅ **Yes — if you're doing password comparison in backend.**
- `[Compare("Password")]` will validate if `ConfirmPassword` matches.
- Otherwise, youll need to check manually.
You **can skip sending ConfirmPassword to backend** and just validate in frontend if youre confident your frontend handles it.
But again: if someone sends malformed input manually (e.g., via Postman), backend should defend.
💡 **Best practice:**
- Validate `ConfirmPassword` in frontend (UX)
- Do one last check in backend, or use `[Compare]` for auto-validation
---
### 🔹 **4. Is it OK to send plain password in request? Or should we hash it on frontend?**
**✅ YES — it is OK and standard to send raw password in the login/signup request.**
Why?
- Passwords are sent over **HTTPS**, which encrypts the entire request.
- Hashing on frontend is _not_ secure, because:
- Your algorithm/salt would be exposed
- It defeats the purpose of salting and hashing correctly
- You lose control over security management
### 🔹 **5. Error Response from Automatic Model Validation**
If your DTO looks like this:
```csharp
public class RegisterRequestDto
{
[Required(ErrorMessage = "Phone number is required.")]
[Phone(ErrorMessage = "Phone number format is invalid.")]
public string PhoneNumber { get; set; }
[Required(ErrorMessage = "Password is required.")]
[MinLength(6, ErrorMessage = "Password must be at least 6 characters long.")]
public string Password { get; set; }
[Compare("Password", ErrorMessage = "Passwords do not match.")]
public string ConfirmPassword { get; set; }
}
```
And the frontend sends this:
```json
{
"phoneNumber": "",
"password": "123",
"confirmPassword": "abc"
}
```
#### The backend will automatically return:
```json
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"PhoneNumber": [
"Phone number is required."
],
"Password": [
"Password must be at least 6 characters long."
],
"ConfirmPassword": [
"Passwords do not match."
]
}
}
```
This is thanks to `[ApiController]` on your controller class. The framework uses the **ModelState** and returns errors in a structured way.
---
## Adding Mappings
- [ ] Update `MappingProfile` with the following mappings:
```c#
CreateMap<Account, AccountDto>()
.ForMember(dest => dest.Roles, opt => opt.MapFrom(src => src.AccountRoles.Select(x=>x.Role.Title)));
CreateMap<AccountDto, Account>()
.ForMember(dest => dest.AccountRoles, opt => opt.Ignore());
```
# Add Password Hasher Utility
- [ ] Create a password hashing utility class
📂 Suggested Folder: ApplicationLayer/Utils/`PasswordHasher.cs`
```c#
public static class PasswordHasher
{
public static string HashPassword(string password)
{
byte[] salt = RandomNumberGenerator.GetBytes(16);
var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100000, HashAlgorithmName.SHA256);
byte[] hash = pbkdf2.GetBytes(32);
byte[] hashBytes = new byte[48];
Array.Copy(salt, 0, hashBytes, 0, 16);
Array.Copy(hash, 0, hashBytes, 16, 32);
return Convert.ToBase64String(hashBytes);
}
public static bool VerifyPassword(string password, string hashedPassword)
{
byte[] hashBytes = Convert.FromBase64String(hashedPassword);
byte[] salt = new byte[16];
Array.Copy(hashBytes, 0, salt, 0, 16);
var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100000, HashAlgorithmName.SHA256);
byte[] hash = pbkdf2.GetBytes(32);
for (int i = 0; i < 32; i++)
{
if (hashBytes[i + 16] != hash[i])
return false;
}
return true;
}
}
```
# Modifying Account Repository
- [ ] Add the following methods in **`IAccountRepository`**
```c#
Task<Account> GetByPhoneNumberAsync(string phoneNumber);
Task AddAccountRoleAsync(AccountRole accountRole);
```
- [ ] Implement the methods in **`AccountRepository`**
```c#
public async Task AddAccountRoleAsync(AccountRole accountRole)
{
await DbContext.AccountRoles.AddAsync(accountRole);
}
public async Task<Account> GetByPhoneNumberAsync(string phoneNumber)
{
var user = await DbContext.Accounts.Include(x => x.AccountRoles).ThenInclude(x => x.Role).FirstOrDefaultAsync(x => x.PhoneNumber == phoneNumber);
return user;
}
```
# Creating Service
## Fix `Result.cs` Error Method
- [ ] Update the `Error` method to include error messages:
```c#
public static Result<T> Error(T data, string errorMessage) => new() { Status = ResultStatus.Error, Data = data, ErrorMessage = errorMessage };
```
## Creating `IAuthService.cs` and `AuthService.cs`
- [ ] Define the `IAuthService` interface
```c#
public interface IAuthService
{
Task<Result<AuthResponseDto>> RegisterAsync(RegisterRequestDto request);
Task<Result<AuthResponseDto>> LoginAsync(LoginRequestDto request);
}
```
- [ ] Implement the interface in `AuthService.cs`
Use this project as a reference:
https://github.com/MehrdadShirvani/AlibabaClone-Backend/blob/develop/AlibabaClone.Application/Services/AuthService.cs
## Register `IAuthService` in Service in `Program.cs`
- [ ] Add to `Program.cs`
```c#
//...
builder.Services.AddScoped<IAuthService, AuthService>();
//...
```
# Adding JWT
## Installing Required NuGet Packages
Install these packages in the **`WebApi` (Presentation Layer)** project:
```
Microsoft.AspNetCore.Authentication.JwtBearer
Microsoft.IdentityModel.Tokens
System.IdentityModel.Tokens.Jwt
```
## Create JWT Configuration Classes
- [ ] Add JWT section to `appsettings.json`
```xaml
"Jwt": {
"Key": "[supersecretkeyyoustoresecurely]",
"Issuer": "[Issuer]",
"Audience": "MyAppUsers",
"ExpiryMinutes": 60
}
```
- [ ] Create `JwtSettings` and add the following method
📂 Suggested Folder: WebAPI/Authentication
```c#
public class JwtSettings
{
public string Key { get; set; } = null!;
public string Issuer { get; set; } = null!;
public string Audience { get; set; } = null!;
public int ExpiryMinutes { get; set; }
}
```
- [ ] Create `IJwtGenerator` and add the following method
📂 Suggested Folder: WebAPI/Authentication
```c#
string GenerateToken(AuthResponseDto authResponseDto);
```
- [ ] Create `JwtGenerator`, implementing `IJwtGenerator`
📂 Suggested Folder: WebAPI/Authentication
use this project as a reference
https://github.com/MehrdadShirvani/AlibabaClone-Backend/blob/develop/AlibabaClone.Application/Services/AuthService.cs
## Configuring Jwt in Program.cs
- [ ] Register `JwtGenerator` service
```c#
builder.Services.AddScoped<IJwtGenerator, JwtGenerator>();
```
- [ ] Bind `JwtSettings` from configuration
```c#
builder.Services.Configure<JwtSettings>(builder.Configuration.GetSection("Jwt"));
```
- [ ] Configure JWT authentication
```c#
var jwtSettings = builder.Configuration.GetSection("Jwt").Get<JwtSettings>();
builder.Services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = jwtSettings.Issuer,
ValidateAudience = true,
ValidAudience = jwtSettings.Audience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Key)),
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
});
builder.Services.AddAuthorization();
```
- [ ] Add `app.UseAuthentication` before `app.UseAuthorization`
```c#
app.UseAuthentication();
app.UseAuthorization();
```
# Add ApiControllers
- [ ] Create `AuthController`
📂 Suggested Folder: WebApi/Controllers/`AuthController.cs`
- [ ] Add Class and Constructor
```c#
private readonly IAuthService _authService;
private readonly IJwtGenerator _jwtGenerator;
public AuthController(IAuthService authService, IJwtGenerator jwtGenerator)
{
_authService = authService;
_jwtGenerator = jwtGenerator;
}
```
- [ ] Add Register Method
```c#
public async Task<IActionResult> Register(RegisterRequestDto request)
{
var result = await _authService.RegisterAsync(request);
if (!result.IsSuccess)
return BadRequest(result.ErrorMessage);
var token = _jwtGenerator.GenerateToken(result.Data);
var response = new AuthResponseDto
{
PhoneNumber = result.Data.PhoneNumber,
Roles = result.Data.Roles,
Token = token
};
return Ok(response);
}
```
- [ ] Add Login Method
```c#
[HttpPost("login")]
public async Task<IActionResult> Login(LoginRequestDto request)
{
var result = await _authService.LoginAsync(request);
if (!result.IsSuccess)
return Unauthorized(result.ErrorMessage);
var token = _jwtGenerator.GenerateToken(result.Data);
var response = new AuthResponseDto
{
PhoneNumber = result.Data.PhoneNumber,
Roles = result.Data.Roles,
Token = token
};
return Ok(response);
}
```
- [ ] Create AccountController
📂 Suggested Folder: WebApi/Controllers/AccountController.cs
```c#
public class AccountController : ControllerBase
{
[Authorize(Roles = "User")]
[HttpGet("profile")]
public IActionResult GetProfile()
{
return Ok("Hi there, hello");
}
}
```
# Merge
- [ ] Create a PR and merge the current branch with develop