Ajoute une zone d'administration des utilisateurs
This commit is contained in:
@@ -3,6 +3,7 @@ using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using ChessCubing.Server.Admin;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace ChessCubing.Server.Auth;
|
||||
@@ -14,6 +15,11 @@ public sealed class KeycloakAuthService(HttpClient httpClient, IOptions<Keycloak
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
};
|
||||
|
||||
private static readonly JsonSerializerOptions UpdateJsonOptions = new(JsonOptions)
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
|
||||
};
|
||||
|
||||
private readonly HttpClient _httpClient = httpClient;
|
||||
private readonly KeycloakAuthOptions _options = options.Value;
|
||||
|
||||
@@ -32,6 +38,87 @@ public sealed class KeycloakAuthService(HttpClient httpClient, IOptions<Keycloak
|
||||
return await LoginAsync(request.Username, request.Password, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<AdminIdentityUser>> GetAdminUsersAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var adminToken = await RequestAdminTokenAsync(cancellationToken);
|
||||
var users = new List<AdminIdentityUser>();
|
||||
const int pageSize = 100;
|
||||
|
||||
for (var first = 0; ; first += pageSize)
|
||||
{
|
||||
using var request = new HttpRequestMessage(
|
||||
HttpMethod.Get,
|
||||
$"{GetAdminBaseUrl()}/users?first={first}&max={pageSize}&briefRepresentation=false");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", adminToken);
|
||||
|
||||
using var response = await _httpClient.SendAsync(request, cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new KeycloakAuthException("Impossible de recuperer la liste des utilisateurs Keycloak.", (int)response.StatusCode);
|
||||
}
|
||||
|
||||
var page = await ReadJsonAsync<List<AdminUserRepresentation>>(response, cancellationToken) ?? [];
|
||||
users.AddRange(page
|
||||
.Where(user => !string.IsNullOrWhiteSpace(user.Id) && !string.IsNullOrWhiteSpace(user.Username))
|
||||
.Select(MapAdminIdentityUser));
|
||||
|
||||
if (page.Count < pageSize)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
public async Task<AdminIdentityUser> GetAdminUserAsync(string userId, CancellationToken cancellationToken)
|
||||
{
|
||||
var adminToken = await RequestAdminTokenAsync(cancellationToken);
|
||||
return await GetAdminUserAsync(adminToken, userId, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<AdminIdentityUser> UpdateAdminUserAsync(
|
||||
string userId,
|
||||
AdminIdentityUserUpdateRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var adminToken = await RequestAdminTokenAsync(cancellationToken);
|
||||
|
||||
using (var httpRequest = new HttpRequestMessage(HttpMethod.Put, $"{GetAdminBaseUrl()}/users/{Uri.EscapeDataString(userId)}")
|
||||
{
|
||||
Content = JsonContent.Create(new
|
||||
{
|
||||
username = request.Username,
|
||||
email = request.Email,
|
||||
enabled = request.IsEnabled,
|
||||
emailVerified = request.IsEmailVerified,
|
||||
firstName = request.FirstName,
|
||||
lastName = request.LastName,
|
||||
}, options: UpdateJsonOptions)
|
||||
})
|
||||
{
|
||||
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", adminToken);
|
||||
|
||||
using var response = await _httpClient.SendAsync(httpRequest, cancellationToken);
|
||||
if (response.StatusCode == HttpStatusCode.Conflict)
|
||||
{
|
||||
throw new KeycloakAuthException("Ce nom d'utilisateur ou cet email existe deja.", StatusCodes.Status409Conflict);
|
||||
}
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
throw new KeycloakAuthException("Utilisateur introuvable dans Keycloak.", StatusCodes.Status404NotFound);
|
||||
}
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new KeycloakAuthException("La mise a jour du compte Keycloak a echoue.", (int)response.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
return await GetAdminUserAsync(adminToken, userId, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<TokenSuccessResponse> RequestPasswordTokenAsync(string username, string password, CancellationToken cancellationToken)
|
||||
{
|
||||
var formData = new Dictionary<string, string>
|
||||
@@ -235,6 +322,44 @@ public sealed class KeycloakAuthService(HttpClient httpClient, IOptions<Keycloak
|
||||
return users?.FirstOrDefault()?.Id;
|
||||
}
|
||||
|
||||
private async Task<AdminIdentityUser> GetAdminUserAsync(string adminToken, string userId, CancellationToken cancellationToken)
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, $"{GetAdminBaseUrl()}/users/{Uri.EscapeDataString(userId)}");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", adminToken);
|
||||
|
||||
using var response = await _httpClient.SendAsync(request, cancellationToken);
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
throw new KeycloakAuthException("Utilisateur introuvable dans Keycloak.", StatusCodes.Status404NotFound);
|
||||
}
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new KeycloakAuthException("Impossible de recuperer le compte Keycloak.", (int)response.StatusCode);
|
||||
}
|
||||
|
||||
var user = await ReadJsonAsync<AdminUserRepresentation>(response, cancellationToken);
|
||||
if (user is null || string.IsNullOrWhiteSpace(user.Id) || string.IsNullOrWhiteSpace(user.Username))
|
||||
{
|
||||
throw new KeycloakAuthException("Le compte Keycloak est invalide.");
|
||||
}
|
||||
|
||||
return MapAdminIdentityUser(user);
|
||||
}
|
||||
|
||||
private static AdminIdentityUser MapAdminIdentityUser(AdminUserRepresentation user)
|
||||
=> new(
|
||||
user.Id!,
|
||||
user.Username!,
|
||||
user.Email,
|
||||
user.FirstName,
|
||||
user.LastName,
|
||||
user.Enabled ?? true,
|
||||
user.EmailVerified ?? false,
|
||||
user.CreatedTimestamp is > 0
|
||||
? DateTimeOffset.FromUnixTimeMilliseconds(user.CreatedTimestamp.Value).UtcDateTime
|
||||
: null);
|
||||
|
||||
private string[] ExtractRealmRoles(string accessToken)
|
||||
{
|
||||
try
|
||||
@@ -326,6 +451,33 @@ public sealed class KeycloakAuthService(HttpClient httpClient, IOptions<Keycloak
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; init; }
|
||||
}
|
||||
|
||||
private sealed class AdminUserRepresentation
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public string? Id { get; init; }
|
||||
|
||||
[JsonPropertyName("username")]
|
||||
public string? Username { get; init; }
|
||||
|
||||
[JsonPropertyName("email")]
|
||||
public string? Email { get; init; }
|
||||
|
||||
[JsonPropertyName("firstName")]
|
||||
public string? FirstName { get; init; }
|
||||
|
||||
[JsonPropertyName("lastName")]
|
||||
public string? LastName { get; init; }
|
||||
|
||||
[JsonPropertyName("enabled")]
|
||||
public bool? Enabled { get; init; }
|
||||
|
||||
[JsonPropertyName("emailVerified")]
|
||||
public bool? EmailVerified { get; init; }
|
||||
|
||||
[JsonPropertyName("createdTimestamp")]
|
||||
public long? CreatedTimestamp { get; init; }
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class KeycloakAuthException(string message, int statusCode = StatusCodes.Status400BadRequest) : Exception(message)
|
||||
|
||||
Reference in New Issue
Block a user