Yuyuqt commited on
Commit
0acba57
·
1 Parent(s): cf599e0

add: authorization

Browse files
BlazorFrontend/BlazorFrontend.csproj CHANGED
@@ -6,4 +6,8 @@
6
  <ImplicitUsings>enable</ImplicitUsings>
7
  </PropertyGroup>
8
 
 
 
 
 
9
  </Project>
 
6
  <ImplicitUsings>enable</ImplicitUsings>
7
  </PropertyGroup>
8
 
9
+ <ItemGroup>
10
+ <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.17.0" />
11
+ </ItemGroup>
12
+
13
  </Project>
BlazorFrontend/Components/App.razor CHANGED
@@ -55,11 +55,11 @@
55
  </style>
56
 
57
  <link rel="icon" type="image/png" href="favicon.png" />
58
- <HeadOutlet />
59
  </head>
60
 
61
  <body class="h-full">
62
- <Routes />
63
  <script src="_framework/blazor.web.js"></script>
64
 
65
  <!-- Toast Notification Container -->
 
55
  </style>
56
 
57
  <link rel="icon" type="image/png" href="favicon.png" />
58
+ <HeadOutlet @rendermode="new InteractiveServerRenderMode(prerender: false)" />
59
  </head>
60
 
61
  <body class="h-full">
62
+ <Routes @rendermode="new InteractiveServerRenderMode(prerender: false)" />
63
  <script src="_framework/blazor.web.js"></script>
64
 
65
  <!-- Toast Notification Container -->
BlazorFrontend/Components/Layout/Component.razor ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <AuthorizeView>
2
+ <Authorized Context="authContext">
3
+ @if (!authContext.User.IsInRole("Librarian"))
4
+ {
5
+ <NavLink href="Borrowings" class="nav-link" ActiveClass="active">
6
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mr-3"><path d="m21 21-6-6m6 6v-4.8m0 4.8h-4.8"/><path d="M3 16.2V21m0 0h4.8M3 21l6-6"/><path d="M21 7.8V3m0 0h-4.8M21 3l-6 6"/><path d="M3 7.8V3m0 0h4.8M3 3l6 6"/></svg>
7
+ Active Loans
8
+ </NavLink>
9
+ <NavLink href="Membership" class="nav-link" ActiveClass="active">
10
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mr-3"><rect width="20" height="14" x="2" y="5" rx="2"/><line x1="2" x2="22" y1="10" y2="10"/></svg>
11
+ My Membership
12
+ </NavLink>
13
+ <NavLink href="Rewards" class="nav-link" ActiveClass="active">
14
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mr-3"><path d="M20 7h-9"/><path d="M14 17H5"/><circle cx="17" cy="17" r="3"/><circle cx="7" cy="7" r="3"/></svg>
15
+ Rewards
16
+ </NavLink>
17
+ }
18
+ </Authorized>
19
+ </AuthorizeView>
BlazorFrontend/Components/Layout/MainLayout.razor CHANGED
@@ -1,4 +1,7 @@
1
  @inherits LayoutComponentBase
 
 
 
2
 
3
  <div class="flex h-full overflow-hidden">
4
  <!-- Sidebar -->
@@ -20,42 +23,79 @@
20
  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mr-3"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/></svg>
21
  Books Catalog
22
  </NavLink>
23
- <NavLink href="Borrowings" class="nav-link" ActiveClass="active">
24
- <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mr-3"><path d="m21 21-6-6m6 6v-4.8m0 4.8h-4.8"/><path d="M3 16.2V21m0 0h4.8M3 21l6-6"/><path d="M21 7.8V3m0 0h-4.8M21 3l-6 6"/><path d="M3 7.8V3m0 0h4.8M3 3l6 6"/></svg>
25
- Active Loans
26
- </NavLink>
27
- <NavLink href="Membership" class="nav-link" ActiveClass="active">
28
- <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mr-3"><rect width="20" height="14" x="2" y="5" rx="2"/><line x1="2" x2="22" y1="10" y2="10"/></svg>
29
- My Membership
30
- </NavLink>
31
- <NavLink href="Rewards" class="nav-link" ActiveClass="active">
32
- <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mr-3"><path d="M20 7h-9"/><path d="M14 17H5"/><circle cx="17" cy="17" r="3"/><circle cx="7" cy="7" r="3"/></svg>
33
- Rewards
34
- </NavLink>
35
- <NavLink href="Members" class="nav-link" ActiveClass="active">
36
- <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mr-3"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
37
- Members
38
- </NavLink>
39
- <NavLink href="Borrowings/Manage" class="nav-link" ActiveClass="active">
40
- <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mr-3"><path d="M11 15h2a2 2 0 1 0 0-4h-2a2 2 0 1 1 0-4h2"/><path d="M12 5v14"/><path d="M18 19V5l-4 4"/><circle cx="12" cy="12" r="10"/></svg>
41
- Manage Borrowings
42
- </NavLink>
43
- <NavLink href="Rewards/Manage" class="nav-link" ActiveClass="active">
44
- <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mr-3"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
45
- Manage Rewards
46
- </NavLink>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  </nav>
48
  </div>
49
  <div class="px-4 py-4 border-t border-slate-100">
50
- <div class="flex items-center gap-3">
51
- <div class="h-8 w-8 rounded-full bg-slate-900 flex items-center justify-center text-white font-bold text-xs uppercase">
52
- GU
53
- </div>
54
- <div class="flex flex-col">
55
- <span class="text-sm font-medium">Guest User</span>
56
- <span class="text-xs text-slate-500">Sign in to borrow</span>
57
- </div>
58
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  </div>
60
  </div>
61
  </aside>
@@ -85,7 +125,14 @@
85
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
86
  </svg>
87
  </button>
88
- <a href="/login" class="btn-primary py-1.5">Sign In</a>
 
 
 
 
 
 
 
89
  </div>
90
  </div>
91
  </header>
@@ -93,7 +140,16 @@
93
  <!-- Main Content -->
94
  <main class="flex-1 relative overflow-y-auto focus:outline-none">
95
  <div class="py-8 px-4 sm:px-6 md:px-8 max-w-7xl mx-auto">
96
- @Body
 
 
 
 
 
 
 
 
 
97
  </div>
98
  </main>
99
  </div>
@@ -104,3 +160,22 @@
104
  <a href="" class="reload text-blue-500 hover:underline">Reload</a>
105
  <a class="dismiss cursor-pointer">🗙</a>
106
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  @inherits LayoutComponentBase
2
+ @using BlazorFrontend.Providers
3
+ @using Microsoft.AspNetCore.Components.Authorization
4
+ @inject AuthenticationStateProvider AuthStateProvider
5
 
6
  <div class="flex h-full overflow-hidden">
7
  <!-- Sidebar -->
 
23
  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mr-3"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/></svg>
24
  Books Catalog
25
  </NavLink>
26
+
27
+ <AuthorizeView>
28
+ <NotAuthorized>
29
+ <NavLink href="Borrowings" class="nav-link" ActiveClass="active">
30
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mr-3"><path d="m21 21-6-6m6 6v-4.8m0 4.8h-4.8"/><path d="M3 16.2V21m0 0h4.8M3 21l6-6"/><path d="M21 7.8V3m0 0h-4.8M21 3l-6 6"/><path d="M3 7.8V3m0 0h4.8M3 3l6 6"/></svg>
31
+ Active Loans
32
+ </NavLink>
33
+ </NotAuthorized>
34
+ </AuthorizeView>
35
+
36
+ <AuthorizeView>
37
+ <Authorized Context="authContext">
38
+ @if (!authContext.User.IsInRole("Librarian"))
39
+ {
40
+ <NavLink href="Borrowings" class="nav-link" ActiveClass="active">
41
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mr-3"><path d="m21 21-6-6m6 6v-4.8m0 4.8h-4.8"/><path d="M3 16.2V21m0 0h4.8M3 21l6-6"/><path d="M21 7.8V3m0 0h-4.8M21 3l-6 6"/><path d="M3 7.8V3m0 0h4.8M3 3l6 6"/></svg>
42
+ Active Loans
43
+ </NavLink>
44
+ <NavLink href="Membership" class="nav-link" ActiveClass="active">
45
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mr-3"><rect width="20" height="14" x="2" y="5" rx="2"/><line x1="2" x2="22" y1="10" y2="10"/></svg>
46
+ My Membership
47
+ </NavLink>
48
+ <NavLink href="Rewards" class="nav-link" ActiveClass="active">
49
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mr-3"><path d="M20 7h-9"/><path d="M14 17H5"/><circle cx="17" cy="17" r="3"/><circle cx="7" cy="7" r="3"/></svg>
50
+ Rewards
51
+ </NavLink>
52
+ }
53
+ </Authorized>
54
+ </AuthorizeView>
55
+
56
+ <AuthorizeView Roles="Librarian">
57
+ <Authorized>
58
+ <NavLink href="Members" class="nav-link" ActiveClass="active">
59
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mr-3"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
60
+ Members
61
+ </NavLink>
62
+ <NavLink href="Borrowings/Manage" class="nav-link" ActiveClass="active">
63
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mr-3"><path d="M11 15h2a2 2 0 1 0 0-4h-2a2 2 0 1 1 0-4h2"/><path d="M12 5v14"/><path d="M18 19V5l-4 4"/><circle cx="12" cy="12" r="10"/></svg>
64
+ Manage Borrowings
65
+ </NavLink>
66
+ <NavLink href="Rewards/Manage" class="nav-link" ActiveClass="active">
67
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mr-3"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
68
+ Manage Rewards
69
+ </NavLink>
70
+ </Authorized>
71
+ </AuthorizeView>
72
  </nav>
73
  </div>
74
  <div class="px-4 py-4 border-t border-slate-100">
75
+ <AuthorizeView>
76
+ <Authorized Context="authContext">
77
+ <div class="flex items-center gap-3">
78
+ <div class="h-8 w-8 rounded-full bg-slate-900 flex items-center justify-center text-white font-bold text-xs uppercase">
79
+ @(authContext.User.Identity?.Name?.Substring(0, 2) ?? "US")
80
+ </div>
81
+ <div class="flex flex-col">
82
+ <span class="text-sm font-medium">@authContext.User.Identity?.Name</span>
83
+ <span class="text-xs text-slate-500">@authContext.User.Claims.FirstOrDefault(c => c.Type == System.Security.Claims.ClaimTypes.Role)?.Value</span>
84
+ </div>
85
+ </div>
86
+ </Authorized>
87
+ <NotAuthorized>
88
+ <div class="flex items-center gap-3">
89
+ <div class="h-8 w-8 rounded-full bg-slate-900 flex items-center justify-center text-white font-bold text-xs uppercase">
90
+ GU
91
+ </div>
92
+ <div class="flex flex-col">
93
+ <span class="text-sm font-medium">Guest User</span>
94
+ <span class="text-xs text-slate-500">Sign in to borrow</span>
95
+ </div>
96
+ </div>
97
+ </NotAuthorized>
98
+ </AuthorizeView>
99
  </div>
100
  </div>
101
  </aside>
 
125
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
126
  </svg>
127
  </button>
128
+ <AuthorizeView>
129
+ <Authorized>
130
+ <a href="/logout" class="btn-secondary py-1.5">Logout</a>
131
+ </Authorized>
132
+ <NotAuthorized>
133
+ <a href="/login" class="btn-primary py-1.5">Sign In</a>
134
+ </NotAuthorized>
135
+ </AuthorizeView>
136
  </div>
137
  </div>
138
  </header>
 
140
  <!-- Main Content -->
141
  <main class="flex-1 relative overflow-y-auto focus:outline-none">
142
  <div class="py-8 px-4 sm:px-6 md:px-8 max-w-7xl mx-auto">
143
+ @if (_isReady)
144
+ {
145
+ @Body
146
+ }
147
+ else
148
+ {
149
+ <div class="flex items-center justify-center h-64">
150
+ <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-slate-900"></div>
151
+ </div>
152
+ }
153
  </div>
154
  </main>
155
  </div>
 
160
  <a href="" class="reload text-blue-500 hover:underline">Reload</a>
161
  <a class="dismiss cursor-pointer">🗙</a>
162
  </div>
163
+
164
+ @code {
165
+ private bool _isReady;
166
+
167
+ protected override async Task OnAfterRenderAsync(bool firstRender)
168
+ {
169
+ if (firstRender)
170
+ {
171
+ var jwtProvider = AuthStateProvider as JwtAuthenticationStateProvider;
172
+ if (jwtProvider != null)
173
+ {
174
+ await jwtProvider.LoadTokenAsync();
175
+ }
176
+
177
+ _isReady = true;
178
+ StateHasChanged();
179
+ }
180
+ }
181
+ }
BlazorFrontend/Components/Pages/Books.razor CHANGED
@@ -1,4 +1,8 @@
1
  @page "/Books"
 
 
 
 
2
 
3
  <PageTitle>Books Catalog</PageTitle>
4
 
@@ -7,6 +11,78 @@
7
  <p class="text-sm text-slate-500 mt-1">Browse and search our collection of books.</p>
8
  </div>
9
 
10
- <div class="card p-6">
11
- <p class="text-slate-600">Books list will be displayed here.</p>
12
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  @page "/Books"
2
+ @using BlazorFrontend.Services
3
+ @using BlazorFrontend.Models.Dtos
4
+ @inject HttpClient Http
5
+
6
 
7
  <PageTitle>Books Catalog</PageTitle>
8
 
 
11
  <p class="text-sm text-slate-500 mt-1">Browse and search our collection of books.</p>
12
  </div>
13
 
14
+ @if (_books == null)
15
+ {
16
+ <div class="card p-6 text-center">
17
+ <p class="text-slate-600">Loading books...</p>
18
+ </div>
19
+ }
20
+ else if (!_books.Any())
21
+ {
22
+ <div class="card p-6 text-center">
23
+ <p class="text-slate-600">No books found in the catalog.</p>
24
+ </div>
25
+ }
26
+ else
27
+ {
28
+ <div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-6">
29
+ @foreach (var book in _books)
30
+ {
31
+ <div class="card flex flex-col h-full hover:shadow-md transition-shadow">
32
+ <div class="p-5 flex-grow">
33
+ <h3 class="font-semibold text-lg text-slate-900 line-clamp-2">@book.Title</h3>
34
+ <p class="text-sm text-slate-500 mt-1">By @book.Author</p>
35
+ <div class="mt-4 flex flex-wrap gap-2">
36
+ @if (book.Categories != null && book.Categories.Any())
37
+ {
38
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-slate-100 text-slate-800">
39
+ @string.Join(", ", book.Categories.Select(c => c.Name))
40
+ </span>
41
+ }
42
+ @if (book.AvailableCopies > 0)
43
+ {
44
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
45
+ Available (@book.AvailableCopies)
46
+ </span>
47
+ }
48
+ else
49
+ {
50
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
51
+ Checked Out
52
+ </span>
53
+ }
54
+ </div>
55
+ </div>
56
+ <div class="px-5 py-4 border-t border-slate-100 mt-auto">
57
+ <AuthorizeView>
58
+ <Authorized>
59
+ <button class="w-full btn-primary py-2" disabled="@(book.AvailableCopies <= 0)">
60
+ Borrow Book
61
+ </button>
62
+ </Authorized>
63
+ <NotAuthorized>
64
+ <a href="/login" class="w-full btn-secondary py-2 block text-center">Sign In to Borrow</a>
65
+ </NotAuthorized>
66
+ </AuthorizeView>
67
+ </div>
68
+ </div>
69
+ }
70
+ </div>
71
+ }
72
+
73
+ @code {
74
+ private IEnumerable<BookDto>? _books;
75
+
76
+ protected override async Task OnInitializedAsync()
77
+ {
78
+ try
79
+ {
80
+ _books = await Http.GetFromJsonAsync<IEnumerable<BookDto>>("api/books");
81
+ }
82
+ catch (Exception ex)
83
+ {
84
+ Console.WriteLine("Error fetching books: " + ex.Message);
85
+ _books = new List<BookDto>();
86
+ }
87
+ }
88
+ }
BlazorFrontend/Components/Pages/Home.razor CHANGED
@@ -1,4 +1,7 @@
1
  @page "/"
 
 
 
2
 
3
  <PageTitle>Dashboard</PageTitle>
4
 
@@ -10,14 +13,62 @@
10
  <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
11
  <div class="card p-6">
12
  <h3 class="text-lg font-medium text-slate-900">Total Books</h3>
13
- <p class="text-3xl font-bold text-slate-700 mt-2">--</p>
14
  </div>
15
  <div class="card p-6">
16
- <h3 class="text-lg font-medium text-slate-900">Active Loans</h3>
17
- <p class="text-3xl font-bold text-slate-700 mt-2">--</p>
18
- </div>
19
- <div class="card p-6">
20
- <h3 class="text-lg font-medium text-slate-900">Members</h3>
21
- <p class="text-3xl font-bold text-slate-700 mt-2">--</p>
22
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  @page "/"
2
+ @using BlazorFrontend.Services
3
+ @using BlazorFrontend.Models.Dtos
4
+ @inject HttpClient Http
5
 
6
  <PageTitle>Dashboard</PageTitle>
7
 
 
13
  <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
14
  <div class="card p-6">
15
  <h3 class="text-lg font-medium text-slate-900">Total Books</h3>
16
+ <p class="text-3xl font-bold text-slate-700 mt-2">@_totalBooks</p>
17
  </div>
18
  <div class="card p-6">
19
+ <h3 class="text-lg font-medium text-slate-900">Categories</h3>
20
+ <p class="text-3xl font-bold text-slate-700 mt-2">@_totalCategories</p>
 
 
 
 
21
  </div>
22
+ <AuthorizeView Roles="Librarian">
23
+ <Authorized>
24
+ <div class="card p-6">
25
+ <h3 class="text-lg font-medium text-slate-900">Total Users</h3>
26
+ <p class="text-3xl font-bold text-slate-700 mt-2">@_totalUsers</p>
27
+ </div>
28
+ </Authorized>
29
+ <NotAuthorized>
30
+ <div class="card p-6">
31
+ <h3 class="text-lg font-medium text-slate-900">My Active Loans</h3>
32
+ <p class="text-3xl font-bold text-slate-700 mt-2">@_myLoans</p>
33
+ </div>
34
+ </NotAuthorized>
35
+ </AuthorizeView>
36
  </div>
37
+
38
+ @code {
39
+ private string _totalBooks = "--";
40
+ private string _totalCategories = "--";
41
+ private string _totalUsers = "--";
42
+ private string _myLoans = "--";
43
+
44
+ protected override async Task OnInitializedAsync()
45
+ {
46
+ try
47
+ {
48
+ var books = await Http.GetFromJsonAsync<IEnumerable<BookDto>>("api/books");
49
+ _totalBooks = books?.Count().ToString() ?? "0";
50
+
51
+ var categories = await Http.GetFromJsonAsync<IEnumerable<CategoryDto>>("api/categories");
52
+ _totalCategories = categories?.Count().ToString() ?? "0";
53
+
54
+ // These endpoints might require auth, we catch exceptions if the user is unauthenticated or unauthorized
55
+ try
56
+ {
57
+ var users = await Http.GetFromJsonAsync<IEnumerable<UserDto>>("api/users");
58
+ _totalUsers = users?.Count().ToString() ?? "0";
59
+ }
60
+ catch { }
61
+
62
+ try
63
+ {
64
+ var loans = await Http.GetFromJsonAsync<IEnumerable<BorrowingDto>>("api/borrowings/me");
65
+ _myLoans = loans?.Count().ToString() ?? "0";
66
+ }
67
+ catch { }
68
+ }
69
+ catch (Exception ex)
70
+ {
71
+ Console.WriteLine("Error fetching dashboard data: " + ex.Message);
72
+ }
73
+ }
74
+ }
BlazorFrontend/Components/Pages/Login.razor ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @page "/login"
2
+
3
+ @using BlazorFrontend.Services
4
+ @using BlazorFrontend.Providers
5
+ @using BlazorFrontend.Models.Dtos
6
+ @using Microsoft.AspNetCore.Components.Authorization
7
+ @inject HttpClient Http
8
+ @inject AuthenticationStateProvider AuthStateProvider
9
+ @inject NavigationManager Navigation
10
+
11
+ <PageTitle>Sign In</PageTitle>
12
+
13
+ <div class="min-h-[calc(100vh-200px)] flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
14
+ <div class="max-w-md w-full space-y-8 bg-white p-10 rounded-2xl border border-slate-200 shadow-xl">
15
+ <div>
16
+ <div class="mx-auto h-12 w-12 bg-slate-900 rounded-xl flex items-center justify-center text-white mb-4">
17
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" y1="12" x2="3" y2="12"/></svg>
18
+ </div>
19
+ <h2 class="text-center text-3xl font-extrabold text-slate-900 tracking-tight">Welcome back</h2>
20
+ <p class="mt-2 text-center text-sm text-slate-500">
21
+ Continue where you left off.
22
+ </p>
23
+ </div>
24
+
25
+ @if (!string.IsNullOrEmpty(_errorMessage))
26
+ {
27
+ <div class="bg-red-50 text-red-600 p-3 rounded-lg border border-red-200 text-sm mb-4">
28
+ @_errorMessage
29
+ </div>
30
+ }
31
+
32
+ <EditForm Model="@_loginRequest" OnValidSubmit="HandleLogin" FormName="loginForm" class="mt-8 space-y-6">
33
+ <DataAnnotationsValidator />
34
+
35
+ <div class="space-y-4">
36
+ <div>
37
+ <label class="block text-sm font-medium text-slate-700 mb-1">Email Address</label>
38
+ <InputText @bind-Value="_loginRequest.Email" type="email" required class="appearance-none relative block w-full px-3 py-2 border border-slate-300 placeholder-slate-400 text-slate-900 rounded-md focus:outline-none focus:ring-slate-900 focus:border-slate-900 focus:z-10 sm:text-sm" placeholder="name@example.com" />
39
+ <ValidationMessage For="@(() => _loginRequest.Email)" class="text-xs text-red-500 mt-1" />
40
+ </div>
41
+ <div>
42
+ <div class="flex items-center justify-between mb-1">
43
+ <label class="block text-sm font-medium text-slate-700">Password</label>
44
+ </div>
45
+ <InputText type="password" @bind-Value="_loginRequest.Password" required class="appearance-none relative block w-full px-3 py-2 border border-slate-300 placeholder-slate-400 text-slate-900 rounded-md focus:outline-none focus:ring-slate-900 focus:border-slate-900 focus:z-10 sm:text-sm" placeholder="••••••••" />
46
+ <ValidationMessage For="@(() => _loginRequest.Password)" class="text-xs text-red-500 mt-1" />
47
+ </div>
48
+ </div>
49
+
50
+ <div>
51
+ <button type="submit" disabled="@_isLoading" class="group relative w-full flex justify-center py-2.5 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-slate-900 hover:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-900 transition-all disabled:opacity-50">
52
+ @if (_isLoading)
53
+ {
54
+ <span>Signing in...</span>
55
+ }
56
+ else
57
+ {
58
+ <span>Sign In</span>
59
+ }
60
+ </button>
61
+ </div>
62
+
63
+ <div class="text-center">
64
+ <span class="text-sm text-slate-500">Don't have an account?</span>
65
+ <a href="/register" class="text-sm font-medium text-slate-900 hover:underline ml-1">Create one for free</a>
66
+ </div>
67
+ </EditForm>
68
+ </div>
69
+ </div>
70
+
71
+ @code {
72
+ [SupplyParameterFromForm]
73
+ private LoginRequest _loginRequest { get; set; } = new();
74
+
75
+ private string? _errorMessage;
76
+ private bool _isLoading;
77
+
78
+ private async Task HandleLogin()
79
+ {
80
+ try
81
+ {
82
+ _isLoading = true;
83
+ _errorMessage = null;
84
+
85
+ var res = await Http.PostAsJsonAsync("api/auth/login", _loginRequest);
86
+
87
+ if (res.IsSuccessStatusCode)
88
+ {
89
+ var response = await res.Content.ReadFromJsonAsync<AuthResponse>();
90
+ if (response != null && !string.IsNullOrEmpty(response.Token))
91
+ {
92
+ var jwtProvider = (JwtAuthenticationStateProvider)AuthStateProvider;
93
+ await jwtProvider.LoginAsync(response.Token);
94
+
95
+ // ensure storage is ready
96
+ await Task.Delay(100);
97
+
98
+ Navigation.NavigateTo("/");
99
+ return;
100
+ }
101
+ }
102
+
103
+ _errorMessage = "Invalid credentials or server error.";
104
+ }
105
+ catch (Exception ex)
106
+ {
107
+ _errorMessage = "An error occurred: " + ex.Message;
108
+ }
109
+ finally
110
+ {
111
+ _isLoading = false;
112
+ }
113
+ }
114
+ }
BlazorFrontend/Components/Pages/Logout.razor ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @page "/logout"
2
+ @using BlazorFrontend.Providers
3
+ @using Microsoft.AspNetCore.Components.Authorization
4
+ @inject AuthenticationStateProvider AuthStateProvider
5
+ @inject NavigationManager Navigation
6
+
7
+ <PageTitle>Logging out...</PageTitle>
8
+
9
+ <div class="text-center mt-10">
10
+ <p class="text-slate-600">Signing you out...</p>
11
+ </div>
12
+
13
+ @code {
14
+ protected override async Task OnInitializedAsync()
15
+ {
16
+ var jwtProvider = (JwtAuthenticationStateProvider)AuthStateProvider;
17
+ await jwtProvider.LogoutAsync();
18
+ Navigation.NavigateTo("/login");
19
+ }
20
+ }
BlazorFrontend/Components/Pages/Register.razor ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @page "/register"
2
+
3
+ @using BlazorFrontend.Services
4
+ @using BlazorFrontend.Providers
5
+ @using BlazorFrontend.Models.Dtos
6
+ @using Microsoft.AspNetCore.Components.Authorization
7
+ @inject HttpClient Http
8
+ @inject NavigationManager Navigation
9
+
10
+ <PageTitle>Register</PageTitle>
11
+
12
+ <div class="min-h-[calc(100vh-200px)] flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
13
+ <div class="max-w-md w-full space-y-8 bg-white p-10 rounded-2xl border border-slate-200 shadow-xl">
14
+ <div>
15
+ <div class="mx-auto h-12 w-12 bg-slate-900 rounded-xl flex items-center justify-center text-white mb-4">
16
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" y1="8" x2="19" y2="14"/><line x1="22" y1="11" x2="16" y2="11"/></svg>
17
+ </div>
18
+ <h2 class="text-center text-3xl font-extrabold text-slate-900 tracking-tight">Join our Library</h2>
19
+ <p class="mt-2 text-center text-sm text-slate-500">
20
+ Start your reading journey today.
21
+ </p>
22
+ </div>
23
+
24
+ @if (!string.IsNullOrEmpty(_errorMessage))
25
+ {
26
+ <div class="bg-red-50 text-red-600 p-3 rounded-lg border border-red-200 text-sm mb-4">
27
+ @_errorMessage
28
+ </div>
29
+ }
30
+
31
+ <EditForm Model="@_registerRequest" OnValidSubmit="HandleRegister" FormName="registerForm" class="mt-8 space-y-6">
32
+ <DataAnnotationsValidator />
33
+
34
+ <div class="space-y-4">
35
+ <div>
36
+ <label class="block text-sm font-medium text-slate-700 mb-1">Full Name</label>
37
+ <InputText @bind-Value="_registerRequest.FullName" required class="appearance-none relative block w-full px-3 py-2 border border-slate-300 placeholder-slate-400 text-slate-900 rounded-md focus:outline-none focus:ring-slate-900 focus:border-slate-900 focus:z-10 sm:text-sm" placeholder="John Doe" />
38
+ <ValidationMessage For="@(() => _registerRequest.FullName)" class="text-xs text-red-500 mt-1" />
39
+ </div>
40
+ <div>
41
+ <label class="block text-sm font-medium text-slate-700 mb-1">Email Address</label>
42
+ <InputText type="email" @bind-Value="_registerRequest.Email" required class="appearance-none relative block w-full px-3 py-2 border border-slate-300 placeholder-slate-400 text-slate-900 rounded-md focus:outline-none focus:ring-slate-900 focus:border-slate-900 focus:z-10 sm:text-sm" placeholder="name@example.com" />
43
+ <ValidationMessage For="@(() => _registerRequest.Email)" class="text-xs text-red-500 mt-1" />
44
+ </div>
45
+ <div>
46
+ <label class="block text-sm font-medium text-slate-700 mb-1">Phone Number</label>
47
+ <InputText type="tel" @bind-Value="_registerRequest.PhoneNumber" class="appearance-none relative block w-full px-3 py-2 border border-slate-300 placeholder-slate-400 text-slate-900 rounded-md focus:outline-none focus:ring-slate-900 focus:border-slate-900 focus:z-10 sm:text-sm" placeholder="09123456789" />
48
+ <ValidationMessage For="@(() => _registerRequest.PhoneNumber)" class="text-xs text-red-500 mt-1" />
49
+ </div>
50
+ <div>
51
+ <label class="block text-sm font-medium text-slate-700 mb-1">Password</label>
52
+ <InputText type="password" @bind-Value="_registerRequest.Password" required class="appearance-none relative block w-full px-3 py-2 border border-slate-300 placeholder-slate-400 text-slate-900 rounded-md focus:outline-none focus:ring-slate-900 focus:border-slate-900 focus:z-10 sm:text-sm" placeholder="••••••••" />
53
+ <ValidationMessage For="@(() => _registerRequest.Password)" class="text-xs text-red-500 mt-1" />
54
+ </div>
55
+ </div>
56
+
57
+ <div>
58
+ <button type="submit" disabled="@_isLoading" class="group relative w-full flex justify-center py-2.5 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-slate-900 hover:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-900 transition-all disabled:opacity-50">
59
+ @if (_isLoading)
60
+ {
61
+ <span>Creating account...</span>
62
+ }
63
+ else
64
+ {
65
+ <span>Create Account</span>
66
+ }
67
+ </button>
68
+ </div>
69
+
70
+ <div class="text-center">
71
+ <span class="text-sm text-slate-500">Already have an account?</span>
72
+ <a href="/login" class="text-sm font-medium text-slate-900 hover:underline ml-1">Sign in</a>
73
+ </div>
74
+ </EditForm>
75
+ </div>
76
+ </div>
77
+
78
+ @code {
79
+ [SupplyParameterFromForm]
80
+ private RegisterRequest _registerRequest { get; set; } = new();
81
+
82
+ private string? _errorMessage;
83
+ private bool _isLoading;
84
+
85
+ private async Task HandleRegister()
86
+ {
87
+ try
88
+ {
89
+ _isLoading = true;
90
+ _errorMessage = null;
91
+
92
+ var res = await Http.PostAsJsonAsync("api/auth/register", _registerRequest);
93
+
94
+ if (res.IsSuccessStatusCode)
95
+ {
96
+ Navigation.NavigateTo("/login");
97
+ }
98
+ else
99
+ {
100
+ _errorMessage = "Registration failed. This email might already be in use.";
101
+ }
102
+ }
103
+ catch (Exception ex)
104
+ {
105
+ _errorMessage = "An error occurred: " + ex.Message;
106
+ }
107
+ finally
108
+ {
109
+ _isLoading = false;
110
+ }
111
+ }
112
+ }
BlazorFrontend/Components/Routes.razor CHANGED
@@ -1,6 +1,14 @@
1
- <Router AppAssembly="typeof(Program).Assembly">
2
- <Found Context="routeData">
3
- <RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
4
- <FocusOnNavigate RouteData="routeData" Selector="h1" />
5
- </Found>
6
- </Router>
 
 
 
 
 
 
 
 
 
1
+ <CascadingAuthenticationState>
2
+ <Router AppAssembly="typeof(Program).Assembly">
3
+ <Found Context="routeData">
4
+ <AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
5
+ <FocusOnNavigate RouteData="routeData" Selector="h1" />
6
+ </Found>
7
+ <NotFound>
8
+ <PageTitle>Not found</PageTitle>
9
+ <LayoutView Layout="typeof(Layout.MainLayout)">
10
+ <p role="alert">Sorry, there's nothing at this address.</p>
11
+ </LayoutView>
12
+ </NotFound>
13
+ </Router>
14
+ </CascadingAuthenticationState>
BlazorFrontend/Components/_Imports.razor CHANGED
@@ -1,4 +1,4 @@
1
- @using System.Net.Http
2
  @using System.Net.Http.Json
3
  @using Microsoft.AspNetCore.Components.Forms
4
  @using Microsoft.AspNetCore.Components.Routing
@@ -8,3 +8,4 @@
8
  @using Microsoft.JSInterop
9
  @using BlazorFrontend
10
  @using BlazorFrontend.Components
 
 
1
+ @using System.Net.Http
2
  @using System.Net.Http.Json
3
  @using Microsoft.AspNetCore.Components.Forms
4
  @using Microsoft.AspNetCore.Components.Routing
 
8
  @using Microsoft.JSInterop
9
  @using BlazorFrontend
10
  @using BlazorFrontend.Components
11
+ @using Microsoft.AspNetCore.Components.Authorization
BlazorFrontend/Models/Dtos/LibraryDtos.cs ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using System.Text.Json.Serialization;
2
+
3
+ namespace BlazorFrontend.Models.Dtos
4
+ {
5
+ // Auth DTOs
6
+ public class RegisterRequest
7
+ {
8
+ public string FullName { get; set; } = string.Empty;
9
+ public string Email { get; set; } = string.Empty;
10
+ public string Password { get; set; } = string.Empty;
11
+ public string? PhoneNumber { get; set; }
12
+ public string? Address { get; set; }
13
+ public string? StudentId { get; set; }
14
+ }
15
+
16
+ public class LoginRequest
17
+ {
18
+ public string Email { get; set; } = string.Empty;
19
+ public string Password { get; set; } = string.Empty;
20
+ }
21
+
22
+ public class AuthResponse
23
+ {
24
+ public string Token { get; set; } = string.Empty;
25
+ public string FullName { get; set; } = string.Empty;
26
+ public string Role { get; set; } = string.Empty;
27
+ public DateTime Expiry { get; set; }
28
+ }
29
+
30
+ // Book DTOs
31
+ public class BookDto
32
+ {
33
+ public int Id { get; set; }
34
+ public string Title { get; set; } = string.Empty;
35
+ public string Isbn { get; set; } = string.Empty;
36
+ public string Author { get; set; } = string.Empty;
37
+ public string Status { get; set; } = string.Empty;
38
+ public bool IsActive { get; set; }
39
+ public string? Description { get; set; }
40
+ public int TotalCopies { get; set; }
41
+ public int AvailableCopies { get; set; }
42
+ public DateTime CreatedAt { get; set; }
43
+ public DateTime? UpdatedAt { get; set; }
44
+ public List<BookCategoryDto> Categories { get; set; } = new();
45
+ }
46
+
47
+ public class BookCategoryDto
48
+ {
49
+ public int Id { get; set; }
50
+ public string Name { get; set; } = string.Empty;
51
+ }
52
+
53
+ public class BookCreateRequest
54
+ {
55
+ public string Title { get; set; } = string.Empty;
56
+ public string Isbn { get; set; } = string.Empty;
57
+ public string Author { get; set; } = string.Empty;
58
+ public string? Description { get; set; }
59
+ public int TotalCopies { get; set; }
60
+ public List<int>? CategoryIds { get; set; }
61
+ }
62
+
63
+ public class BookUpdateRequest
64
+ {
65
+ public string Title { get; set; } = string.Empty;
66
+ public string Author { get; set; } = string.Empty;
67
+ public string? Description { get; set; }
68
+ public int TotalCopies { get; set; }
69
+ public List<int>? CategoryIds { get; set; }
70
+ }
71
+
72
+ // Borrowing DTOs
73
+ public class BorrowingDto
74
+ {
75
+ public Guid Id { get; set; }
76
+ public Guid UserId { get; set; }
77
+ public string UserEmail { get; set; } = string.Empty;
78
+ public int BookId { get; set; }
79
+ public string BookTitle { get; set; } = string.Empty;
80
+ public DateTime BorrowDate { get; set; }
81
+ public DateTime DueDate { get; set; }
82
+ public DateTime? ReturnDate { get; set; } // This is ActualReturnDate in some contexts
83
+ public string Status { get; set; } = string.Empty;
84
+ public decimal FineAmount { get; set; }
85
+ public bool IsFinePaid { get; set; }
86
+ }
87
+
88
+ public class BorrowRequest
89
+ {
90
+ public int BookId { get; set; }
91
+ }
92
+
93
+ // Category DTOs
94
+ public class CategoryDto
95
+ {
96
+ public int Id { get; set; }
97
+ public string Name { get; set; } = string.Empty;
98
+ }
99
+
100
+ public class CategoryCreateRequest
101
+ {
102
+ public string Name { get; set; } = string.Empty;
103
+ }
104
+
105
+ // Subscription & Membership DTOs
106
+ public class MembershipDto
107
+ {
108
+ public int Id { get; set; }
109
+ public string Type { get; set; } = string.Empty;
110
+ public int MaxBooks { get; set; }
111
+ public int BorrowingDays { get; set; }
112
+ public decimal Price { get; set; }
113
+ public int DurationMonths { get; set; }
114
+ public string? RewardId { get; set; }
115
+ }
116
+
117
+ public class SubscriptionDto
118
+ {
119
+ public Guid Id { get; set; }
120
+ public Guid UserId { get; set; }
121
+ public int MembershipId { get; set; }
122
+ public string MembershipType { get; set; } = string.Empty;
123
+ public string UserName { get; set; } = string.Empty;
124
+ public string UserEmail { get; set; } = string.Empty;
125
+ public DateTime StartDate { get; set; }
126
+ public DateTime ExpiryDate { get; set; }
127
+ public bool IsActive { get; set; }
128
+ public bool IsExpired => DateTime.UtcNow > ExpiryDate;
129
+ }
130
+
131
+ public class SubscribeRequest
132
+ {
133
+ public int MembershipId { get; set; }
134
+ }
135
+
136
+ public class AdminSubscribeRequest
137
+ {
138
+ public Guid UserId { get; set; }
139
+ public int MembershipId { get; set; }
140
+ }
141
+
142
+ // Loyalty DTOs
143
+ public class LoyaltyAccountDto
144
+ {
145
+ [JsonPropertyName("id")]
146
+ public string Id { get; set; } = string.Empty;
147
+
148
+ // Some responses use `accountId` instead of `id`.
149
+ [JsonPropertyName("accountId")]
150
+ public string? AccountId { get; set; }
151
+
152
+ [JsonPropertyName("externalUserId")]
153
+ public string ExternalUserId { get; set; } = string.Empty;
154
+
155
+ [JsonPropertyName("currentBalance")]
156
+ public double CurrentBalance { get; set; }
157
+
158
+ [JsonPropertyName("tier")]
159
+ public string Tier { get; set; } = string.Empty;
160
+
161
+ [JsonPropertyName("lifetimePoints")]
162
+ public double LifetimePoints { get; set; }
163
+ }
164
+
165
+ public class LoyaltyRewardDto
166
+ {
167
+ [JsonPropertyName("id")]
168
+ public string Id { get; set; } = string.Empty;
169
+
170
+ [JsonPropertyName("name")]
171
+ public string Name { get; set; } = string.Empty;
172
+
173
+ [JsonPropertyName("description")]
174
+ public string Description { get; set; } = string.Empty;
175
+
176
+ [JsonPropertyName("pointCost")]
177
+ public double PointCost { get; set; }
178
+
179
+ [JsonPropertyName("stockQuantity")]
180
+ public int StockQuantity { get; set; }
181
+
182
+ [JsonPropertyName("isActive")]
183
+ public bool IsActive { get; set; }
184
+ }
185
+
186
+ public class ClaimRewardRequestDto
187
+ {
188
+ public string RewardId { get; set; } = string.Empty;
189
+ public string? Notes { get; set; }
190
+ }
191
+
192
+ // User DTOs
193
+ public class UserDto
194
+ {
195
+ public Guid Id { get; set; }
196
+ public string FullName { get; set; } = string.Empty;
197
+ public string Email { get; set; } = string.Empty;
198
+ public string? PhoneNumber { get; set; }
199
+ public string Role { get; set; } = string.Empty;
200
+ public bool IsActive { get; set; }
201
+ public string? StudentId { get; set; }
202
+ public string? Address { get; set; }
203
+ public DateTime CreatedAt { get; set; }
204
+ public DateTime? UpdatedAt { get; set; }
205
+ public bool? BanStatus { get; set; }
206
+ public DateTime? SuspensionEndDate { get; set; }
207
+ }
208
+
209
+ public class UserCreateRequest
210
+ {
211
+ public string FullName { get; set; } = string.Empty;
212
+ public string Email { get; set; } = string.Empty;
213
+ public string Password { get; set; } = string.Empty;
214
+ public string? PhoneNumber { get; set; }
215
+ public string? StudentId { get; set; }
216
+ public string? Address { get; set; }
217
+ }
218
+
219
+ public class UserUpdateRequest
220
+ {
221
+ public string FullName { get; set; } = string.Empty;
222
+ public string? PhoneNumber { get; set; }
223
+ public string? StudentId { get; set; }
224
+ public string? Address { get; set; }
225
+ }
226
+
227
+ public class UserRoleUpdateRequest
228
+ {
229
+ public string Role { get; set; } = string.Empty;
230
+ }
231
+
232
+ // Points History DTOs
233
+ public class PointHistoryEntryDto
234
+ {
235
+ [JsonPropertyName("id")]
236
+ public int Id { get; set; }
237
+ [JsonPropertyName("accountId")]
238
+ public string AccountId { get; set; } = string.Empty;
239
+ [JsonPropertyName("externalUserId")]
240
+ public string? ExternalUserId { get; set; }
241
+ [JsonPropertyName("pointDelta")]
242
+ public double PointDelta { get; set; }
243
+ [JsonPropertyName("eventKey")]
244
+ public string EventKey { get; set; } = string.Empty;
245
+ [JsonPropertyName("description")]
246
+ public string Description { get; set; } = string.Empty;
247
+ [JsonPropertyName("referenceId")]
248
+ public string? ReferenceId { get; set; }
249
+ [JsonPropertyName("createdAt")]
250
+ public DateTime CreatedAt { get; set; }
251
+
252
+ [JsonPropertyName("rewardId")]
253
+ public string? RewardId { get; set; }
254
+
255
+ [JsonPropertyName("rewardName")]
256
+ public string? RewardName { get; set; }
257
+
258
+ [JsonPropertyName("redemptionStatus")]
259
+ public string? RedemptionStatus { get; set; }
260
+
261
+ [JsonPropertyName("redeemedAt")]
262
+ public DateTime? RedeemedAt { get; set; }
263
+ }
264
+
265
+ public class UserPointsHistoryDto
266
+ {
267
+ [JsonPropertyName("userId")]
268
+ public Guid UserId { get; set; }
269
+ [JsonPropertyName("userName")]
270
+ public string UserName { get; set; } = string.Empty;
271
+ [JsonPropertyName("userEmail")]
272
+ public string UserEmail { get; set; } = string.Empty;
273
+ [JsonPropertyName("accountId")]
274
+ public string AccountId { get; set; } = string.Empty;
275
+ [JsonPropertyName("currentBalance")]
276
+ public double CurrentBalance { get; set; }
277
+ [JsonPropertyName("tier")]
278
+ public string Tier { get; set; } = string.Empty;
279
+ [JsonPropertyName("history")]
280
+ public IEnumerable<PointHistoryEntryDto> History { get; set; } = Enumerable.Empty<PointHistoryEntryDto>();
281
+ [JsonPropertyName("redemptions")]
282
+ public IEnumerable<LoyaltyRedemptionDto> Redemptions { get; set; } = Enumerable.Empty<LoyaltyRedemptionDto>();
283
+ }
284
+ }
BlazorFrontend/Models/Dtos/LoyaltyRedemptionDto.cs ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using System;
2
+ using System.Text.Json.Serialization;
3
+
4
+ namespace BlazorFrontend.Models.Dtos
5
+ {
6
+ public class LoyaltyRedemptionDto
7
+ {
8
+ [JsonPropertyName("id")]
9
+ public string Id { get; set; } = string.Empty;
10
+
11
+ [JsonPropertyName("systemId")]
12
+ public string SystemId { get; set; } = string.Empty;
13
+
14
+ [JsonPropertyName("externalUserId")]
15
+ public string ExternalUserId { get; set; } = string.Empty;
16
+
17
+ [JsonPropertyName("rewardId")]
18
+ public string? RewardId { get; set; }
19
+
20
+ [JsonPropertyName("rewardName")]
21
+ public string RewardName { get; set; } = string.Empty;
22
+
23
+ [JsonPropertyName("pointCost")]
24
+ public double PointCost { get; set; }
25
+
26
+ [JsonPropertyName("status")]
27
+ public string Status { get; set; } = string.Empty;
28
+
29
+ [JsonPropertyName("redeemedAt")]
30
+ public DateTime RedeemedAt { get; set; }
31
+ }
32
+ }
BlazorFrontend/Models/ErrorViewModel.cs ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ namespace BlazorFrontend.Models
2
+ {
3
+ public class ErrorViewModel
4
+ {
5
+ public string? RequestId { get; set; }
6
+
7
+ public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
8
+ }
9
+ }
BlazorFrontend/Models/MemberDetailsViewModel.cs ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using BlazorFrontend.Models.Dtos;
2
+ using System.Collections.Generic;
3
+
4
+ namespace BlazorFrontend.Models
5
+ {
6
+ public class MemberDetailsViewModel
7
+ {
8
+ public UserDto User { get; set; } = new();
9
+ public SubscriptionDto? CurrentSubscription { get; set; }
10
+ public List<MembershipDto> AvailableMemberships { get; set; } = new();
11
+ }
12
+ }
BlazorFrontend/Models/MembershipViewModel.cs ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using BlazorFrontend.Models.Dtos;
2
+ using System.Collections.Generic;
3
+
4
+ namespace BlazorFrontend.Models
5
+ {
6
+ public class MembershipViewModel
7
+ {
8
+ public SubscriptionDto? CurrentSubscription { get; set; }
9
+ public List<MembershipDto> AvailableMemberships { get; set; } = new List<MembershipDto>();
10
+ public List<SubscriptionDto> QueuedMemberships { get; set; } = new List<SubscriptionDto>();
11
+ public LoyaltyAccountDto? LoyaltyAccount { get; set; }
12
+ public Dictionary<string, double> RewardPointCosts { get; set; } = new Dictionary<string, double>();
13
+ }
14
+ }
BlazorFrontend/Models/PagedResult.cs ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ namespace BlazorFrontend.Models
2
+ {
3
+ public class PagedResult<T>
4
+ {
5
+ public IEnumerable<T> Items { get; set; } = Enumerable.Empty<T>();
6
+ public int CurrentPage { get; set; }
7
+ public int TotalPages { get; set; }
8
+ public int TotalCount { get; set; }
9
+ public int PageSize { get; set; }
10
+
11
+ public bool HasPreviousPage => CurrentPage > 1;
12
+ public bool HasNextPage => CurrentPage < TotalPages;
13
+ }
14
+ }
BlazorFrontend/Models/RewardsViewModel.cs ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using BlazorFrontend.Models.Dtos;
2
+
3
+ namespace BlazorFrontend.Models
4
+ {
5
+ // Member rewards page view model
6
+ public class RewardsViewModel
7
+ {
8
+ public IEnumerable<LoyaltyRewardDto> Rewards { get; set; } = Enumerable.Empty<LoyaltyRewardDto>();
9
+ public LoyaltyAccountDto? Account { get; set; }
10
+ public IEnumerable<LoyaltyRedemptionDto> PendingRedemptions { get; set; } = Enumerable.Empty<LoyaltyRedemptionDto>();
11
+ public IEnumerable<LoyaltyRedemptionDto> RedemptionsHistory { get; set; } = Enumerable.Empty<LoyaltyRedemptionDto>();
12
+ public IEnumerable<PointHistoryEntryDto> PointsHistory { get; set; } = Enumerable.Empty<PointHistoryEntryDto>();
13
+ }
14
+
15
+ // Librarian rewards management view model
16
+ public class LibrarianRewardsViewModel
17
+ {
18
+ public IEnumerable<LoyaltyRedemptionDto> PendingRedemptions { get; set; } = Enumerable.Empty<LoyaltyRedemptionDto>();
19
+ public IEnumerable<UserPointsHistoryDto> AllMembersHistory { get; set; } = Enumerable.Empty<UserPointsHistoryDto>();
20
+ }
21
+ }
BlazorFrontend/Models/SubscriptionsViewModel.cs ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ using BlazorFrontend.Models.Dtos;
2
+
3
+ namespace BlazorFrontend.Models
4
+ {
5
+ public class SubscriptionsViewModel
6
+ {
7
+ public List<MembershipDto> AvailableMemberships { get; set; } = new();
8
+ public List<SubscriptionDto> ActiveSubscriptions { get; set; } = new();
9
+ }
10
+ }
BlazorFrontend/Program.cs CHANGED
@@ -1,4 +1,7 @@
1
  using BlazorFrontend.Components;
 
 
 
2
 
3
  var builder = WebApplication.CreateBuilder(args);
4
 
@@ -6,6 +9,26 @@ var builder = WebApplication.CreateBuilder(args);
6
  builder.Services.AddRazorComponents()
7
  .AddInteractiveServerComponents();
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  var app = builder.Build();
10
 
11
  // Configure the HTTP request pipeline.
 
1
  using BlazorFrontend.Components;
2
+ using Microsoft.AspNetCore.Components.Authorization;
3
+ using BlazorFrontend.Providers;
4
+ using BlazorFrontend.Services;
5
 
6
  var builder = WebApplication.CreateBuilder(args);
7
 
 
9
  builder.Services.AddRazorComponents()
10
  .AddInteractiveServerComponents();
11
 
12
+ builder.Services.AddCascadingAuthenticationState();
13
+ builder.Services.AddScoped<JwtAuthenticationStateProvider>();
14
+ builder.Services.AddScoped<AuthenticationStateProvider>(sp => sp.GetRequiredService<JwtAuthenticationStateProvider>());
15
+ builder.Services.AddAuthorizationCore();
16
+
17
+ builder.Services.AddScoped<AuthHeaderHandler>();
18
+
19
+ builder.Services.AddScoped(sp =>
20
+ {
21
+ var handler = sp.GetRequiredService<AuthHeaderHandler>();
22
+ handler.InnerHandler = new HttpClientHandler();
23
+ return new HttpClient(handler)
24
+ {
25
+ BaseAddress = new Uri("https://localhost:7028/")
26
+ };
27
+ });
28
+
29
+
30
+ builder.Services.AddScoped<LibraryApiClient>();
31
+
32
  var app = builder.Build();
33
 
34
  // Configure the HTTP request pipeline.
BlazorFrontend/Providers/JwtAuthenticationStateProvider.cs ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using Microsoft.AspNetCore.Components.Authorization;
2
+ using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
3
+ using System.IdentityModel.Tokens.Jwt;
4
+ using System.Security.Claims;
5
+
6
+ namespace BlazorFrontend.Providers;
7
+
8
+ public class JwtAuthenticationStateProvider : AuthenticationStateProvider
9
+ {
10
+ private readonly ProtectedLocalStorage _localStorage;
11
+ private readonly ClaimsPrincipal _anonymous = new(new ClaimsIdentity());
12
+
13
+ public string? Token { get; private set; }
14
+
15
+ public JwtAuthenticationStateProvider(ProtectedLocalStorage localStorage)
16
+ {
17
+ _localStorage = localStorage;
18
+ }
19
+
20
+ public override Task<AuthenticationState> GetAuthenticationStateAsync()
21
+ {
22
+ if (string.IsNullOrWhiteSpace(Token))
23
+ return Task.FromResult(new AuthenticationState(_anonymous));
24
+
25
+ try
26
+ {
27
+ var handler = new JwtSecurityTokenHandler();
28
+ var jwt = handler.ReadJwtToken(Token);
29
+
30
+ var identity = new ClaimsIdentity(jwt.Claims, "jwt");
31
+ var user = new ClaimsPrincipal(identity);
32
+
33
+ return Task.FromResult(new AuthenticationState(user));
34
+ }
35
+ catch
36
+ {
37
+ return Task.FromResult(new AuthenticationState(_anonymous));
38
+ }
39
+ }
40
+
41
+ public async Task LoadTokenAsync()
42
+ {
43
+ try
44
+ {
45
+ var tokenResult = await _localStorage.GetAsync<string>("AuthToken");
46
+ if (tokenResult.Success && !string.IsNullOrWhiteSpace(tokenResult.Value))
47
+ {
48
+ Token = tokenResult.Value;
49
+ NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
50
+ }
51
+ }
52
+ catch
53
+ {
54
+ // Ignore JS interop errors if they somehow still happen
55
+ }
56
+ }
57
+
58
+ public async Task LoginAsync(string token)
59
+ {
60
+ Token = token;
61
+ await _localStorage.SetAsync("AuthToken", token);
62
+ NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
63
+ }
64
+
65
+ public async Task LogoutAsync()
66
+ {
67
+ Token = null;
68
+ await _localStorage.DeleteAsync("AuthToken");
69
+ NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(_anonymous)));
70
+ }
71
+ }
BlazorFrontend/Services/AuthHeaderHandler.cs ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using Microsoft.AspNetCore.Components.Authorization;
2
+ using System.Net.Http.Headers;
3
+ using BlazorFrontend.Providers;
4
+
5
+ namespace BlazorFrontend.Services;
6
+
7
+ public class AuthHeaderHandler : DelegatingHandler
8
+ {
9
+ private readonly AuthenticationStateProvider _authStateProvider;
10
+
11
+ public AuthHeaderHandler(AuthenticationStateProvider authStateProvider)
12
+ {
13
+ _authStateProvider = authStateProvider;
14
+ }
15
+
16
+ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
17
+ {
18
+ Console.WriteLine("=== HANDLER RUN ===");
19
+
20
+ try
21
+ {
22
+ var jwtProvider = _authStateProvider as JwtAuthenticationStateProvider;
23
+ var token = jwtProvider?.Token;
24
+
25
+ if (!string.IsNullOrWhiteSpace(token))
26
+ {
27
+ Console.WriteLine("TOKEN FOUND: " + token);
28
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
29
+ }
30
+ else
31
+ {
32
+ Console.WriteLine("NO TOKEN FOUND");
33
+ }
34
+ }
35
+ catch (Exception ex)
36
+ {
37
+ Console.WriteLine("ERROR: " + ex.Message);
38
+ }
39
+
40
+ return await base.SendAsync(request, cancellationToken);
41
+ }
42
+ }
BlazorFrontend/Services/LibraryApiClient.cs ADDED
@@ -0,0 +1,375 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using System.Net.Http.Json;
2
+ using System.Text.Json;
3
+ using BlazorFrontend.Models.Dtos;
4
+
5
+ namespace BlazorFrontend.Services
6
+ {
7
+ public class LibraryApiClient
8
+ {
9
+ private readonly HttpClient _httpClient;
10
+ public LibraryApiClient(HttpClient httpClient)
11
+ {
12
+ _httpClient = httpClient;
13
+ }
14
+
15
+ // Note: You do NOT need to call a SetToken method anymore!
16
+ // Because we registered AuthHeaderHandler in Program.cs as a DelegatingHandler for "LibraryBackend",
17
+ // the JWT token is automatically injected into the headers of EVERY single request made by _httpClient.
18
+
19
+ #region Auth
20
+ public async Task<AuthResponse?> LoginAsync(LoginRequest request)
21
+ {
22
+ var response = await _httpClient.PostAsJsonAsync("api/auth/login", request);
23
+ return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<AuthResponse>() : null;
24
+ }
25
+
26
+ public async Task<AuthResponse?> RegisterAsync(RegisterRequest request)
27
+ {
28
+ var response = await _httpClient.PostAsJsonAsync("api/auth/register", request);
29
+ return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<AuthResponse>() : null;
30
+ }
31
+
32
+ public async Task<object?> GetProfileAsync()
33
+ {
34
+ return await _httpClient.GetFromJsonAsync<object>("api/auth/me");
35
+ }
36
+ #endregion
37
+
38
+ #region Books
39
+ public async Task<IEnumerable<BookDto>> GetBooksAsync(int? categoryId = null)
40
+ {
41
+ var url = categoryId.HasValue ? $"api/books?categoryId={categoryId}" : "api/books";
42
+ return await _httpClient.GetFromJsonAsync<IEnumerable<BookDto>>(url) ?? Enumerable.Empty<BookDto>();
43
+ }
44
+
45
+ public async Task<BookDto?> GetBookAsync(int id)
46
+ {
47
+ return await _httpClient.GetFromJsonAsync<BookDto>($"api/books/{id}");
48
+ }
49
+
50
+ public async Task<BookDto?> CreateBookAsync(BookCreateRequest request)
51
+ {
52
+ var response = await _httpClient.PostAsJsonAsync("api/books", request);
53
+ return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<BookDto>() : null;
54
+ }
55
+
56
+ public async Task<BookDto?> UpdateBookAsync(int id, BookUpdateRequest request)
57
+ {
58
+ var response = await _httpClient.PutAsJsonAsync($"api/books/{id}", request);
59
+ return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<BookDto>() : null;
60
+ }
61
+
62
+ public async Task<bool> DeleteBookAsync(int id)
63
+ {
64
+ var response = await _httpClient.DeleteAsync($"api/books/{id}");
65
+ return response.IsSuccessStatusCode;
66
+ }
67
+ #endregion
68
+
69
+ #region Borrowings
70
+ public async Task<BorrowingDto?> BorrowBookAsync(BorrowRequest request)
71
+ {
72
+ var response = await _httpClient.PostAsJsonAsync("api/borrowings/borrow", request);
73
+ return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<BorrowingDto>() : null;
74
+ }
75
+
76
+ public async Task<BorrowingDto?> ReturnBookAsync(Guid id)
77
+ {
78
+ var response = await _httpClient.PostAsync($"api/borrowings/return/{id}", null);
79
+ return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<BorrowingDto>() : null;
80
+ }
81
+
82
+ public async Task<IEnumerable<BorrowingDto>> GetMyBorrowingsAsync()
83
+ {
84
+ return await _httpClient.GetFromJsonAsync<IEnumerable<BorrowingDto>>("api/borrowings/me") ?? Enumerable.Empty<BorrowingDto>();
85
+ }
86
+
87
+ public async Task<IEnumerable<BorrowingDto>> GetAllBorrowingsAsync()
88
+ {
89
+ return await _httpClient.GetFromJsonAsync<IEnumerable<BorrowingDto>>("api/borrowings") ?? Enumerable.Empty<BorrowingDto>();
90
+ }
91
+ #endregion
92
+
93
+ #region Categories
94
+ public async Task<IEnumerable<CategoryDto>> GetCategoriesAsync()
95
+ {
96
+ return await _httpClient.GetFromJsonAsync<IEnumerable<CategoryDto>>("api/categories") ?? Enumerable.Empty<CategoryDto>();
97
+ }
98
+
99
+ public async Task<IEnumerable<CategoryDto>> GetCategoriesWithBooksAsync()
100
+ {
101
+ return await _httpClient.GetFromJsonAsync<IEnumerable<CategoryDto>>("api/categories/with-books") ?? Enumerable.Empty<CategoryDto>();
102
+ }
103
+
104
+ public async Task<CategoryDto?> CreateCategoryAsync(CategoryCreateRequest request)
105
+ {
106
+ var response = await _httpClient.PostAsJsonAsync("api/categories", request);
107
+ return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<CategoryDto>() : null;
108
+ }
109
+
110
+ public async Task<bool> DeleteCategoryAsync(int id)
111
+ {
112
+ var response = await _httpClient.DeleteAsync($"api/categories/{id}");
113
+ return response.IsSuccessStatusCode;
114
+ }
115
+ #endregion
116
+
117
+ #region Subscriptions
118
+ public async Task<IEnumerable<MembershipDto>> GetMembershipsAsync()
119
+ {
120
+ return await _httpClient.GetFromJsonAsync<IEnumerable<MembershipDto>>("api/memberships") ?? Enumerable.Empty<MembershipDto>();
121
+ }
122
+
123
+ public async Task<SubscriptionDto?> GetMySubscriptionAsync()
124
+ {
125
+ try
126
+ {
127
+ return await _httpClient.GetFromJsonAsync<SubscriptionDto>("api/subscriptions/me");
128
+ }
129
+ catch { return null; }
130
+ }
131
+
132
+ public async Task<IEnumerable<SubscriptionDto>> GetMyAllSubscriptionsAsync()
133
+ {
134
+ try
135
+ {
136
+ return await _httpClient.GetFromJsonAsync<IEnumerable<SubscriptionDto>>("api/subscriptions/me/all") ?? Enumerable.Empty<SubscriptionDto>();
137
+ }
138
+ catch { return Enumerable.Empty<SubscriptionDto>(); }
139
+ }
140
+
141
+ public async Task<SubscriptionDto?> SubscribeAsync(SubscribeRequest request)
142
+ {
143
+ var response = await _httpClient.PostAsJsonAsync("api/subscriptions/subscribe", request);
144
+ return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<SubscriptionDto>() : null;
145
+ }
146
+
147
+ public async Task<SubscriptionDto?> GetUserSubscriptionAsync(Guid userId)
148
+ {
149
+ try
150
+ {
151
+ return await _httpClient.GetFromJsonAsync<SubscriptionDto>($"api/subscriptions/user/{userId}");
152
+ }
153
+ catch { return null; }
154
+ }
155
+
156
+ public async Task<SubscriptionDto?> AdminSubscribeAsync(AdminSubscribeRequest request)
157
+ {
158
+ var response = await _httpClient.PostAsJsonAsync("api/subscriptions/admin-subscribe", request);
159
+ return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<SubscriptionDto>() : null;
160
+ }
161
+
162
+ public async Task<IEnumerable<SubscriptionDto>> GetAllSubscriptionsAsync()
163
+ {
164
+ return await _httpClient.GetFromJsonAsync<IEnumerable<SubscriptionDto>>("api/subscriptions/all") ?? Enumerable.Empty<SubscriptionDto>();
165
+ }
166
+ #endregion
167
+
168
+ public async Task<LoyaltyAccountDto?> GetMyLoyaltyAccountAsync()
169
+ {
170
+ try
171
+ {
172
+ // Using exact casing for the route to be safe
173
+ var response = await _httpClient.GetAsync("api/Loyalty/my-account");
174
+ if (response.IsSuccessStatusCode)
175
+ {
176
+ // JsonPropertyName attributes in LoyaltyAccountDto will handle the mapping
177
+ // and correctly populate CurrentBalance from the backend's currentBalance
178
+ return await response.Content.ReadFromJsonAsync<LoyaltyAccountDto>();
179
+ }
180
+ else
181
+ {
182
+ var error = await response.Content.ReadAsStringAsync();
183
+ System.Diagnostics.Debug.WriteLine($"Loyalty API error: {response.StatusCode} - {error}");
184
+ // Throw custom exception or return null based on error
185
+ }
186
+ return null;
187
+ }
188
+ catch (Exception ex)
189
+ {
190
+ System.Diagnostics.Debug.WriteLine($"Loyalty API Exception: {ex.Message}");
191
+ return null;
192
+ }
193
+ }
194
+
195
+ public async Task<IEnumerable<LoyaltyRedemptionDto>> GetMyRedemptionsAsync()
196
+ {
197
+ try
198
+ {
199
+ return await _httpClient.GetFromJsonAsync<IEnumerable<LoyaltyRedemptionDto>>("api/Loyalty/my-redemptions") ?? Enumerable.Empty<LoyaltyRedemptionDto>();
200
+ }
201
+ catch { return Enumerable.Empty<LoyaltyRedemptionDto>(); }
202
+ }
203
+
204
+ public async Task<IEnumerable<LoyaltyRedemptionDto>> GetMyPendingRedemptionsAsync()
205
+ {
206
+ try
207
+ {
208
+ return await _httpClient.GetFromJsonAsync<IEnumerable<LoyaltyRedemptionDto>>("api/Loyalty/my-pending-redemptions") ?? Enumerable.Empty<LoyaltyRedemptionDto>();
209
+ }
210
+ catch { return Enumerable.Empty<LoyaltyRedemptionDto>(); }
211
+ }
212
+
213
+ public async Task<IEnumerable<PointHistoryEntryDto>> GetPointsHistoryAsync(string accountId)
214
+ {
215
+ try
216
+ {
217
+ var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
218
+ var response = await _httpClient.GetAsync($"api/v1/accounts/{accountId}/history");
219
+ if (response.IsSuccessStatusCode)
220
+ return await response.Content.ReadFromJsonAsync<IEnumerable<PointHistoryEntryDto>>(options) ?? Enumerable.Empty<PointHistoryEntryDto>();
221
+ return Enumerable.Empty<PointHistoryEntryDto>();
222
+ }
223
+ catch { return Enumerable.Empty<PointHistoryEntryDto>(); }
224
+ }
225
+
226
+ public async Task<IEnumerable<UserPointsHistoryDto>> GetAllMembersPointsHistoryAsync()
227
+ {
228
+ try {
229
+ var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
230
+ var response = await _httpClient.GetAsync("api/Loyalty/admin/all-points-history");
231
+ var raw = await response.Content.ReadAsStringAsync();
232
+ if (!response.IsSuccessStatusCode)
233
+ return Enumerable.Empty<UserPointsHistoryDto>();
234
+
235
+ var data = JsonSerializer.Deserialize<List<UserPointsHistoryDto>>(raw, options) ?? new List<UserPointsHistoryDto>();
236
+ foreach (var member in data)
237
+ {
238
+ member.History = member.History?.ToList() ?? new List<PointHistoryEntryDto>();
239
+ member.Redemptions = member.Redemptions?.ToList() ?? new List<LoyaltyRedemptionDto>();
240
+ }
241
+ return data;
242
+ } catch { return Enumerable.Empty<UserPointsHistoryDto>(); }
243
+ }
244
+
245
+ public async Task<bool> RequestReturnAsync(Guid borrowingId)
246
+ {
247
+ var response = await _httpClient.PostAsync($"api/borrowings/return-request/{borrowingId}", null);
248
+ return response.IsSuccessStatusCode;
249
+ }
250
+
251
+ public async Task<(bool Success, string Message)> ClaimRewardAsync(string rewardId, string? notes = null)
252
+ {
253
+ try
254
+ {
255
+ var request = new ClaimRewardRequestDto { RewardId = rewardId, Notes = notes };
256
+ var response = await _httpClient.PostAsJsonAsync("api/Loyalty/claim", request);
257
+
258
+ if (response.IsSuccessStatusCode)
259
+ {
260
+ var result = await response.Content.ReadFromJsonAsync<JsonDocument>();
261
+ string msg = "Reward claimed successfully!";
262
+ if (result != null && result.RootElement.TryGetProperty("message", out var msgElement))
263
+ {
264
+ msg = msgElement.GetString() ?? msg;
265
+ }
266
+ return (true, msg);
267
+ }
268
+ else
269
+ {
270
+ var error = await response.Content.ReadFromJsonAsync<JsonDocument>();
271
+ string msg = "Failed to claim reward.";
272
+ if (error != null && error.RootElement.TryGetProperty("message", out var msgElement))
273
+ {
274
+ msg = msgElement.GetString() ?? msg;
275
+ }
276
+ return (false, msg);
277
+ }
278
+ }
279
+ catch (Exception ex)
280
+ {
281
+ return (false, $"An error occurred: {ex.Message}");
282
+ }
283
+ }
284
+
285
+ public async Task<IEnumerable<LoyaltyRewardDto>> GetActiveRewardsAsync()
286
+ {
287
+ try
288
+ {
289
+
290
+ var client = new HttpClient();
291
+ return await client.GetFromJsonAsync<IEnumerable<LoyaltyRewardDto>>("http://150.95.88.91:4100/api/v1/rewards/active/THS-LMS") ?? Enumerable.Empty<LoyaltyRewardDto>();
292
+ }
293
+ catch { return Enumerable.Empty<LoyaltyRewardDto>(); }
294
+ }
295
+
296
+ #region Users
297
+ public async Task<IEnumerable<UserDto>> GetUsersAsync()
298
+ {
299
+ return await _httpClient.GetFromJsonAsync<IEnumerable<UserDto>>("api/users") ?? Enumerable.Empty<UserDto>();
300
+ }
301
+
302
+ public async Task<UserDto?> GetUserAsync(Guid id)
303
+ {
304
+ return await _httpClient.GetFromJsonAsync<UserDto>($"api/users/{id}");
305
+ }
306
+
307
+ public async Task<UserDto?> CreateUserAsync(UserCreateRequest request)
308
+ {
309
+ var response = await _httpClient.PostAsJsonAsync("api/users", request);
310
+ return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<UserDto>() : null;
311
+ }
312
+
313
+ public async Task<UserDto?> UpdateUserAsync(Guid id, UserUpdateRequest request)
314
+ {
315
+ var response = await _httpClient.PutAsJsonAsync($"api/users/{id}", request);
316
+ return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<UserDto>() : null;
317
+ }
318
+
319
+ public async Task<bool> UpdateUserRoleAsync(Guid id, string role)
320
+ {
321
+ var response = await _httpClient.PutAsJsonAsync($"api/users/role/{id}", new UserRoleUpdateRequest { Role = role });
322
+ return response.IsSuccessStatusCode;
323
+ }
324
+
325
+ public async Task<bool> DeleteUserAsync(Guid id)
326
+ {
327
+ var response = await _httpClient.DeleteAsync($"api/users/{id}");
328
+ return response.IsSuccessStatusCode;
329
+ }
330
+ #endregion
331
+ public async Task<IEnumerable<LoyaltyRedemptionDto>> GetPendingRedemptionsAsync()
332
+ {
333
+ try
334
+ {
335
+ var response = await _httpClient.GetAsync("api/Loyalty/admin/redemptions/pending");
336
+ if (response.IsSuccessStatusCode)
337
+ {
338
+ var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
339
+ return await response.Content.ReadFromJsonAsync<IEnumerable<LoyaltyRedemptionDto>>(options) ?? Enumerable.Empty<LoyaltyRedemptionDto>();
340
+ }
341
+ return Enumerable.Empty<LoyaltyRedemptionDto>();
342
+ }
343
+ catch { return Enumerable.Empty<LoyaltyRedemptionDto>(); }
344
+ }
345
+
346
+ public async Task<(bool Success, string Message)> FulfillRedemptionAsync(string id)
347
+ {
348
+ try
349
+ {
350
+ var response = await _httpClient.PostAsync($"api/Loyalty/admin/redemptions/{id}/fulfill", null);
351
+ if (response.IsSuccessStatusCode)
352
+ {
353
+ var result = await response.Content.ReadFromJsonAsync<JsonDocument>();
354
+ string msg = "Redemption fulfilled successfully!";
355
+ if (result != null && result.RootElement.TryGetProperty("message", out var msgElement))
356
+ {
357
+ msg = msgElement.GetString() ?? msg;
358
+ }
359
+ return (true, msg);
360
+ }
361
+ else
362
+ {
363
+ var error = await response.Content.ReadFromJsonAsync<JsonDocument>();
364
+ string msg = "Failed to fulfill redemption.";
365
+ if (error != null && error.RootElement.TryGetProperty("message", out var msgElement))
366
+ {
367
+ msg = msgElement.GetString() ?? msg;
368
+ }
369
+ return (false, msg);
370
+ }
371
+ }
372
+ catch (Exception ex) { return (false, $"Error: {ex.Message}"); }
373
+ }
374
+ }
375
+ }