Skip to content

Commit 68a8bf8

Browse files
Teams + Auth Fixes + Refactoring (#1)
* refactoring (naming, namespaces, remove unnecessary code ...) * add teams functionality - create team, delete team, list teams * fix authentication, logging out, correct redirecting, ...
1 parent 4d64239 commit 68a8bf8

25 files changed

+554
-96
lines changed

Frontend/Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<ItemGroup>
66
<PackageVersion Include="BitzArt.Blazor.Cookies.Client" Version="1.0.4" />
77
<PackageVersion Include="BitzArt.Blazor.Cookies.Server" Version="1.0.4" />
8+
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.2.2" />
89
<PackageVersion Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
910
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.4" />
1011
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.4" />
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using RailwayResult;
2+
3+
using TeamUp.Contracts.Teams;
4+
5+
namespace TeamUp.ApiLayer;
6+
7+
public sealed partial class ApiClient
8+
{
9+
public Task<Result<List<TeamSlimResponse>>> GetMyTeamsAsync(CancellationToken ct) =>
10+
SendAsync<List<TeamSlimResponse>>(HttpMethod.Get, "/api/v1/teams", ct);
11+
12+
public Task<Result<TeamId>> CreateTeamAsync(CreateTeamRequest request, CancellationToken ct) =>
13+
SendAsync<CreateTeamRequest, TeamId>(HttpMethod.Post, "/api/v1/teams", request, ct);
14+
15+
public Task<Result> DeleteTeamAsync(TeamId teamId, CancellationToken ct) =>
16+
SendAsync(HttpMethod.Delete, $"/api/v1/teams/{teamId.Value}", ct);
17+
18+
public Task<Result<TeamResponse>> GetTeamAsync(TeamId teamId, CancellationToken ct) =>
19+
SendAsync<TeamResponse>(HttpMethod.Get, $"/api/v1/teams/{teamId.Value}", ct);
20+
}
Lines changed: 110 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Net;
2+
using System.Net.Http.Headers;
23
using System.Net.Http.Json;
34
using System.Text;
45
using System.Text.Json;
@@ -13,16 +14,55 @@ namespace TeamUp.ApiLayer;
1314
public sealed partial class ApiClient
1415
{
1516
private readonly HttpClient _client;
17+
private readonly IAuthService _authService;
1618

17-
public ApiClient(HttpClient client)
19+
public ApiClient(HttpClient client, IAuthService authService)
1820
{
1921
_client = client;
22+
_authService = authService;
23+
}
24+
25+
private Task<Result> SendAsync(HttpMethod method, string uri, CancellationToken ct) => SendAsync(method, uri, null, ct);
26+
27+
private Task<Result> SendAsync(HttpMethod method, string uri, Action<HttpRequestMessage>? configure, CancellationToken ct)
28+
{
29+
var request = new HttpRequestMessage
30+
{
31+
Method = method,
32+
RequestUri = new Uri(uri, UriKind.Relative),
33+
};
34+
35+
if (configure is not null)
36+
{
37+
configure(request);
38+
}
39+
40+
return SendRequestAsync(request, ct);
41+
}
42+
43+
private Task<Result<TResponse>> SendAsync<TResponse>(HttpMethod method, string uri, CancellationToken ct) =>
44+
SendAsync<TResponse>(method, uri, null, ct);
45+
46+
private Task<Result<TResponse>> SendAsync<TResponse>(HttpMethod method, string uri, Action<HttpRequestMessage>? configure, CancellationToken ct)
47+
{
48+
var request = new HttpRequestMessage
49+
{
50+
Method = method,
51+
RequestUri = new Uri(uri, UriKind.Relative),
52+
};
53+
54+
if (configure is not null)
55+
{
56+
configure(request);
57+
}
58+
59+
return SendRequestAsync<TResponse>(request, ct);
2060
}
2161

2262
private Task<Result<TResponse>> SendAsync<TRequest, TResponse>(HttpMethod method, string uri, TRequest payload, CancellationToken ct) =>
2363
SendAsync<TRequest, TResponse>(method, uri, payload, null, ct);
2464

25-
private async Task<Result<TResponse>> SendAsync<TRequest, TResponse>(HttpMethod method, string uri, TRequest payload, Action<HttpRequestMessage>? configure, CancellationToken ct)
65+
private async Task<Result<TResponse>> SendAsync<TRequest, TResponse>(HttpMethod method, string uri, TRequest? payload, Action<HttpRequestMessage>? configure, CancellationToken ct)
2666
{
2767
var json = JsonSerializer.Serialize(payload);
2868
var request = new HttpRequestMessage
@@ -37,38 +77,79 @@ private async Task<Result<TResponse>> SendAsync<TRequest, TResponse>(HttpMethod
3777
configure(request);
3878
}
3979

80+
return await SendRequestAsync<TResponse>(request, ct);
81+
}
82+
83+
private async Task<Result> SendRequestAsync(HttpRequestMessage request, CancellationToken ct)
84+
{
85+
await InjectAuthToken(request, ct);
86+
87+
var response = await _client.SendAsync(request, ct);
88+
89+
if (!response.IsSuccessStatusCode)
90+
{
91+
return await ParseErrorResponse(response, ct);
92+
}
93+
94+
return Result.Success;
95+
}
96+
97+
private async Task<Result<TResponse>> SendRequestAsync<TResponse>(HttpRequestMessage request, CancellationToken ct)
98+
{
99+
await InjectAuthToken(request, ct);
100+
40101
var response = await _client.SendAsync(request, ct);
41102

42103
if (!response.IsSuccessStatusCode)
43104
{
44-
if (response.StatusCode == HttpStatusCode.Unauthorized)
45-
{
46-
//logout
47-
}
48-
49-
var contentType = response.Content.Headers.ContentType?.MediaType;
50-
if (contentType != "application/problem+json")
51-
{
52-
return new ApiError("Api.UnexpectedError", $"Unexpected response with content type '{contentType}'.", response.StatusCode);
53-
}
54-
55-
var problemDetails = await response.Content.ReadFromJsonAsync<ValidationProblemDetails>(ct);
56-
if (problemDetails is null)
57-
{
58-
return new ApiError("Api.SerializationError", "Failed to deserialize error response.", response.StatusCode);
59-
}
60-
61-
var title = problemDetails.Title ?? "Undefined Title";
62-
var detail = problemDetails.Detail ?? "Undefined Detail";
63-
64-
if (problemDetails.Errors.Count > 0)
65-
{
66-
return new ApiValidationError("Api.ValidationError", "One or more validation errors occurred.", problemDetails.Errors);
67-
}
68-
69-
return new ApiError("Api.Error", detail, response.StatusCode);
105+
return await ParseErrorResponse(response, ct);
106+
}
107+
108+
var responsePayload = await response.Content.ReadFromJsonAsync<TResponse>(ct);
109+
if (responsePayload is null)
110+
{
111+
return new ApiError("Api.SerializationError", "Failed to deserialize response.", response.StatusCode);
112+
}
113+
114+
return responsePayload;
115+
}
116+
117+
private async Task<Error> ParseErrorResponse(HttpResponseMessage response, CancellationToken ct)
118+
{
119+
if (response.StatusCode == HttpStatusCode.Unauthorized)
120+
{
121+
await _authService.LogoutAsync(ct: ct);
122+
}
123+
124+
var contentType = response.Content.Headers.ContentType?.MediaType;
125+
if (contentType != "application/problem+json")
126+
{
127+
return new ApiError("Api.UnexpectedError", $"Unexpected response with content type '{contentType}'.", response.StatusCode);
70128
}
71129

72-
return await response.Content.ReadFromJsonAsync<TResponse>(ct);
130+
var problemDetails = await response.Content.ReadFromJsonAsync<ValidationProblemDetails>(ct);
131+
if (problemDetails is null)
132+
{
133+
return new ApiError("Api.SerializationError", "Failed to deserialize error response.", response.StatusCode);
134+
}
135+
136+
var title = problemDetails.Title ?? "Undefined Title";
137+
var detail = problemDetails.Detail ?? "Undefined Detail";
138+
139+
if (problemDetails.Errors.Count > 0)
140+
{
141+
return new ApiValidationError("Api.ValidationError", "One or more validation errors occurred.", problemDetails.Errors);
142+
}
143+
144+
return new ApiError("Api.Error", detail, response.StatusCode);
145+
}
146+
147+
private async Task InjectAuthToken(HttpRequestMessage request, CancellationToken ct)
148+
{
149+
var jwt = await _authService.GetTokenAsync(ct);
150+
if (jwt is not null)
151+
{
152+
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
153+
}
73154
}
74155
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace TeamUp.ApiLayer;
2+
3+
public interface IAuthService
4+
{
5+
public Task<string?> GetTokenAsync(CancellationToken ct = default);
6+
7+
public Task LogoutAsync(string url = "/login", CancellationToken ct = default);
8+
}

Frontend/TeamUp.Client/AnonymousPages/Login.razor

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,11 @@
22

33
@rendermode InteractiveAuto
44

5-
@layout EmptyLayout
6-
75
@inherits CancellableComponent
86

97
@using System.ComponentModel.DataAnnotations
108
@using System.Text.Json
119
@using BitzArt.Blazor.Cookies
12-
@using TeamUp.ApiLayer
13-
@using TeamUp.Client.Components
14-
@using TeamUp.Client.Services
15-
@using TeamUp.Contracts
16-
@using TeamUp.Contracts.Users
1710

1811
@inject NavigationManager NavigationManager
1912
@inject ApiClient ApiClient
@@ -25,7 +18,7 @@
2518

2619
<div class="d-flex flex-dir-column h-screen">
2720
<FluentSpacer />
28-
<FluentCard Style="max-width: 550px; margin-bottom: 200px" Class="h-auto margin-auto padding-large">
21+
<FluentCard Style="margin-bottom: 200px" Class="body-small h-auto margin-auto padding-large">
2922
<h1 style="margin-bottom: 16px">Login</h1>
3023
<FluentDivider />
3124
<EditForm Model="@Input" OnValidSubmit="LoginAsync" FormName="LoginForm" Style="margin-top: 16px">
@@ -87,7 +80,7 @@
8780
}
8881

8982
await CookieService.SetAsync("JWT", result.Value, null, CTS.Token);
90-
if (AuthenticationStateProvider is PersistentAuthenticationStateProvider persistAuthProvider)
83+
if (AuthenticationStateProvider is ClientAuthenticationStateProvider persistAuthProvider)
9184
{
9285
persistAuthProvider.Login(result.Value);
9386
Redirect();

Frontend/TeamUp.Client/AnonymousPages/Register.razor

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,7 @@
77
@using System.ComponentModel.DataAnnotations
88
@using System.Text.Json
99
@using BitzArt.Blazor.Cookies
10-
@using TeamUp.ApiLayer
11-
@using TeamUp.Client.Components
12-
@using TeamUp.Contracts
13-
@using TeamUp.Contracts.Users
1410

15-
@inject ILogger<Register> Logger
1611
@inject NavigationManager NavigationManager
1712
@inject ApiClient ApiClient
1813
@inject AuthenticationStateProvider AuthenticationStateProvider
@@ -22,7 +17,7 @@
2217

2318
<div class="d-flex flex-dir-column h-screen">
2419
<FluentSpacer />
25-
<FluentCard Style="max-width: 550px; margin-bottom: 200px" Class="h-auto margin-auto padding-large">
20+
<FluentCard Style="margin-bottom: 200px" Class="body-small h-auto margin-auto padding-large">
2621
<h1 style="margin-bottom: 16px">Register</h1>
2722
<FluentDivider />
2823
<div style="margin-top: 16px"></div>

Frontend/TeamUp.Client/Components/ComponentWrapper.cs

Lines changed: 0 additions & 7 deletions
This file was deleted.
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
@rendermode InteractiveAuto
22

3-
<FluentToastProvider />
3+
<FluentToastProvider RemoveToastsOnNavigation="false" />
44
<FluentDialogProvider />
55
<FluentTooltipProvider />
66
<FluentMessageBarProvider />
7-

0 commit comments

Comments
 (0)