sannlynnhtun-coding commited on
Commit
068485b
·
1 Parent(s): 97af519

feat: restructure backend project, add configuration files, implement authentication, user and book management, and enhance borrowing and subscription features

Browse files
Backend/Backend.csproj ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <Project Sdk="Microsoft.NET.Sdk.Web">
2
+
3
+ <PropertyGroup>
4
+ <TargetFramework>net8.0</TargetFramework>
5
+ <Nullable>enable</Nullable>
6
+ <ImplicitUsings>enable</ImplicitUsings>
7
+ </PropertyGroup>
8
+
9
+ <ItemGroup>
10
+ <PackageReference Include="BCrypt.Net-Next" Version="4.1.0" />
11
+ <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.13" />
12
+ <PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
13
+ <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.17.0" />
14
+ </ItemGroup>
15
+
16
+ <ItemGroup>
17
+ <ProjectReference Include="..\Database\Database.csproj" />
18
+ </ItemGroup>
19
+
20
+ </Project>
Backend/Backend.http ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ @Backend_HostAddress = http://localhost:5199
2
+
3
+ GET {{Backend_HostAddress}}/weatherforecast/
4
+ Accept: application/json
5
+
6
+ ###
Backend/Features/Auth/AuthController.cs ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using Microsoft.AspNetCore.Authorization;
2
+ using Microsoft.AspNetCore.Mvc;
3
+ using System.Security.Claims;
4
+
5
+ namespace Backend.Features.Auth
6
+ {
7
+ [ApiController]
8
+ [Route("api/auth")]
9
+ public class AuthController : ControllerBase
10
+ {
11
+ private readonly IAuthService _authService;
12
+
13
+ public AuthController(IAuthService authService)
14
+ {
15
+ _authService = authService;
16
+ }
17
+
18
+ [HttpPost("register")]
19
+ public async Task<ActionResult<AuthResponse>> Register([FromBody] RegisterRequest request)
20
+ {
21
+ try
22
+ {
23
+ var response = await _authService.Register(request);
24
+ return Ok(response);
25
+ }
26
+ catch (Exception ex)
27
+ {
28
+ return BadRequest(new { message = ex.Message });
29
+ }
30
+ }
31
+
32
+ [HttpPost("login")]
33
+ public async Task<ActionResult<AuthResponse>> Login([FromBody] LoginRequest request)
34
+ {
35
+ try
36
+ {
37
+ var response = await _authService.Login(request);
38
+ return Ok(response);
39
+ }
40
+ catch (Exception ex)
41
+ {
42
+ return Unauthorized(new { message = ex.Message });
43
+ }
44
+ }
45
+
46
+ [HttpGet("me")]
47
+ [Authorize]
48
+ public IActionResult GetCurrentUser()
49
+ {
50
+ var claims = User.Claims.Select(c => new { c.Type, c.Value }).ToList();
51
+ return Ok(new
52
+ {
53
+ IsAuthenticated = User.Identity?.IsAuthenticated,
54
+ Name = User.Identity?.Name,
55
+ Claims = claims
56
+ });
57
+ }
58
+ }
59
+ }
Backend/Features/Auth/AuthModels.cs ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ namespace Backend.Features.Auth
2
+ {
3
+ public class RegisterRequest
4
+ {
5
+ public string FullName { get; set; } = string.Empty;
6
+ public string Email { get; set; } = string.Empty;
7
+ public string Password { get; set; } = string.Empty;
8
+ public string? PhoneNumber { get; set; }
9
+ public string? Address { get; set; }
10
+ public string? StudentId { get; set; }
11
+ }
12
+
13
+ public class LoginRequest
14
+ {
15
+ public string Email { get; set; } = string.Empty;
16
+ public string Password { get; set; } = string.Empty;
17
+ }
18
+
19
+ public class AuthResponse
20
+ {
21
+ public string Token { get; set; } = string.Empty;
22
+ public string FullName { get; set; } = string.Empty;
23
+ public string Role { get; set; } = string.Empty;
24
+ public DateTime Expiry { get; set; }
25
+ }
26
+ }
Backend/Features/Auth/AuthService.cs ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using Database.Models;
2
+ using Microsoft.EntityFrameworkCore;
3
+ using Microsoft.IdentityModel.Tokens;
4
+ using System.IdentityModel.Tokens.Jwt;
5
+ using System.Security.Claims;
6
+ using System.Text;
7
+
8
+ using Backend.Features.Subscriptions;
9
+
10
+ namespace Backend.Features.Auth
11
+ {
12
+ public interface IAuthService
13
+ {
14
+ Task<AuthResponse> Register(RegisterRequest request);
15
+ Task<AuthResponse> Login(LoginRequest request);
16
+ }
17
+
18
+ public class AuthService : IAuthService
19
+ {
20
+ private readonly LibraryManagementContext _context;
21
+ private readonly IConfiguration _configuration;
22
+ private readonly ISubscriptionService _subscriptionService;
23
+
24
+ public AuthService(LibraryManagementContext context, IConfiguration configuration, ISubscriptionService subscriptionService)
25
+ {
26
+ _context = context;
27
+ _configuration = configuration;
28
+ _subscriptionService = subscriptionService;
29
+ }
30
+
31
+ public async Task<AuthResponse> Register(RegisterRequest request)
32
+ {
33
+ if (await _context.Users.AnyAsync(u => u.Email == request.Email))
34
+ {
35
+ throw new Exception("User already exists with this email.");
36
+ }
37
+
38
+ var user = new User
39
+ {
40
+ FullName = request.FullName,
41
+ Email = request.Email,
42
+ PhoneNumber = request.PhoneNumber,
43
+ Address = request.Address,
44
+ StudentId = request.StudentId,
45
+ PasswordHash = BCrypt.Net.BCrypt.HashPassword(request.Password),
46
+ Role = "Member", // Default role
47
+ IsActive = true,
48
+ CreatedAt = DateTime.UtcNow
49
+ };
50
+
51
+ _context.Users.Add(user);
52
+ await _context.SaveChangesAsync();
53
+
54
+ if (!string.IsNullOrEmpty(user.StudentId))
55
+ {
56
+ await _subscriptionService.SubscribeUserAsync(user.Id, 3); // 3 is "Basic Yearly"
57
+ }
58
+
59
+ return await AuthenticateUser(user);
60
+ }
61
+
62
+ public async Task<AuthResponse> Login(LoginRequest request)
63
+ {
64
+ var user = await _context.Users.FirstOrDefaultAsync(u => u.Email == request.Email);
65
+
66
+ if (user == null || !BCrypt.Net.BCrypt.Verify(request.Password, user.PasswordHash))
67
+ {
68
+ throw new Exception("Invalid email or password.");
69
+ }
70
+
71
+ if (!user.IsActive)
72
+ {
73
+ throw new Exception("User account is deactivated.");
74
+ }
75
+
76
+ return await AuthenticateUser(user);
77
+ }
78
+
79
+ private async Task<AuthResponse> AuthenticateUser(User user)
80
+ {
81
+ var tokenHandler = new JwtSecurityTokenHandler();
82
+ var key = Encoding.ASCII.GetBytes(_configuration["Jwt:Secret"] ?? "YourSuperSecretKeyForLibraryManagementSystem_AtLeast32CharsLong");
83
+
84
+ var tokenDescriptor = new SecurityTokenDescriptor
85
+ {
86
+ Subject = new ClaimsIdentity(new[]
87
+ {
88
+ new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
89
+ new Claim(ClaimTypes.Email, user.Email),
90
+ new Claim(ClaimTypes.Name, user.FullName),
91
+ new Claim(ClaimTypes.Role, user.Role)
92
+ }),
93
+ Expires = DateTime.UtcNow.AddDays(7),
94
+ SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature),
95
+ Issuer = _configuration["Jwt:Issuer"],
96
+ Audience = _configuration["Jwt:Audience"]
97
+ };
98
+
99
+ var token = tokenHandler.CreateToken(tokenDescriptor);
100
+
101
+ return new AuthResponse
102
+ {
103
+ Token = tokenHandler.WriteToken(token),
104
+ FullName = user.FullName,
105
+ Role = user.Role,
106
+ Expiry = tokenDescriptor.Expires.Value
107
+ };
108
+ }
109
+ }
110
+ }
Backend/Features/Books/BookController.cs ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using Microsoft.AspNetCore.Authorization;
2
+ using Microsoft.AspNetCore.Mvc;
3
+
4
+ namespace Backend.Features.Books
5
+ {
6
+ [ApiController]
7
+ [Route("api/books")]
8
+ public class BookController : ControllerBase
9
+ {
10
+ private readonly IBookService _bookService;
11
+
12
+ public BookController(IBookService bookService)
13
+ {
14
+ _bookService = bookService;
15
+ }
16
+
17
+ [HttpGet]
18
+ //[Authorize]
19
+ public async Task<ActionResult<IEnumerable<BookDto>>> GetBooks()
20
+ {
21
+ var books = await _bookService.GetAllBooksAsync();
22
+ return Ok(books);
23
+ }
24
+
25
+ [HttpGet("{id}")]
26
+ //[Authorize]
27
+ public async Task<ActionResult<BookDto>> GetBook(int id)
28
+ {
29
+ var book = await _bookService.GetBookByIdAsync(id);
30
+ if (book == null) return NotFound();
31
+ return Ok(book);
32
+ }
33
+
34
+ [HttpPost]
35
+ //[Authorize(Roles = "Librarian")]
36
+ public async Task<ActionResult<BookDto>> CreateBook([FromBody] BookCreateRequest request)
37
+ {
38
+ try
39
+ {
40
+ var book = await _bookService.CreateBookAsync(request);
41
+ return CreatedAtAction(nameof(GetBook), new { id = book.Id }, book);
42
+ }
43
+ catch (Exception ex)
44
+ {
45
+ return BadRequest(new { message = ex.Message });
46
+ }
47
+ }
48
+
49
+ [HttpPut("{id}")]
50
+ //[Authorize(Roles = "Librarian")]
51
+ public async Task<ActionResult<BookDto>> UpdateBook(int id, [FromBody] BookUpdateRequest request)
52
+ {
53
+ var updatedBook = await _bookService.UpdateBookAsync(id, request);
54
+ if (updatedBook == null) return NotFound();
55
+ return Ok(updatedBook);
56
+ }
57
+
58
+ [HttpDelete("{id}")]
59
+ //[Authorize(Roles = "Librarian")]
60
+ public async Task<IActionResult> DeleteBook(int id)
61
+ {
62
+ var success = await _bookService.DeleteBookAsync(id);
63
+ if (!success) return NotFound();
64
+ return NoContent();
65
+ }
66
+ }
67
+ }
Backend/Features/Books/BookService.cs ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using Database.Models;
2
+ using Microsoft.EntityFrameworkCore;
3
+
4
+ namespace Backend.Features.Books
5
+ {
6
+ public interface IBookService
7
+ {
8
+ Task<IEnumerable<BookDto>> GetAllBooksAsync();
9
+ Task<BookDto?> GetBookByIdAsync(int id);
10
+ Task<BookDto> CreateBookAsync(BookCreateRequest request);
11
+ Task<BookDto?> UpdateBookAsync(int id, BookUpdateRequest request);
12
+ Task<bool> DeleteBookAsync(int id);
13
+ }
14
+
15
+ public class BookService : IBookService
16
+ {
17
+ private readonly LibraryManagementContext _context;
18
+
19
+ public BookService(LibraryManagementContext context)
20
+ {
21
+ _context = context;
22
+ }
23
+
24
+ public async Task<IEnumerable<BookDto>> GetAllBooksAsync()
25
+ {
26
+ return await _context.Books
27
+ .Where(b => b.IsActive)
28
+ .Select(b => MapToDto(b))
29
+ .ToListAsync();
30
+ }
31
+
32
+ public async Task<BookDto?> GetBookByIdAsync(int id)
33
+ {
34
+ var book = await _context.Books.FindAsync(id);
35
+ if (book == null || !book.IsActive) return null;
36
+ return MapToDto(book);
37
+ }
38
+
39
+ public async Task<BookDto> CreateBookAsync(BookCreateRequest request)
40
+ {
41
+ if (await _context.Books.AnyAsync(b => b.Isbn == request.Isbn))
42
+ {
43
+ throw new Exception("A book with this ISBN already exists.");
44
+ }
45
+
46
+ var book = new Book
47
+ {
48
+ Title = request.Title,
49
+ Isbn = request.Isbn,
50
+ Author = request.Author,
51
+ Description = request.Description,
52
+ TotalCopies = request.TotalCopies,
53
+ AvailableCopies = request.TotalCopies,
54
+ Status = request.TotalCopies > 0 ? "Available" : "Out Of Stock",
55
+ IsActive = true,
56
+ CreatedAt = DateTime.UtcNow
57
+ };
58
+
59
+ _context.Books.Add(book);
60
+ await _context.SaveChangesAsync();
61
+
62
+ return MapToDto(book);
63
+ }
64
+
65
+ public async Task<BookDto?> UpdateBookAsync(int id, BookUpdateRequest request)
66
+ {
67
+ var book = await _context.Books.FindAsync(id);
68
+ if (book == null || !book.IsActive) return null;
69
+
70
+ book.Title = request.Title;
71
+ book.Author = request.Author;
72
+ book.Description = request.Description;
73
+
74
+ // Basic logic to sync availability when total copies change
75
+ int difference = request.TotalCopies - book.TotalCopies;
76
+ book.TotalCopies = request.TotalCopies;
77
+ book.AvailableCopies += difference;
78
+
79
+ if (book.AvailableCopies < 0) book.AvailableCopies = 0;
80
+ book.Status = book.AvailableCopies > 0 ? "Available" : "Out Of Stock";
81
+
82
+ book.UpdatedAt = DateTime.UtcNow;
83
+
84
+ await _context.SaveChangesAsync();
85
+ return MapToDto(book);
86
+ }
87
+
88
+ public async Task<bool> DeleteBookAsync(int id)
89
+ {
90
+ var book = await _context.Books.FindAsync(id);
91
+ if (book == null) return false;
92
+
93
+ book.IsActive = false;
94
+ book.UpdatedAt = DateTime.UtcNow;
95
+
96
+ await _context.SaveChangesAsync();
97
+ return true;
98
+ }
99
+
100
+ private static BookDto MapToDto(Book book)
101
+ {
102
+ return new BookDto
103
+ {
104
+ Id = book.Id,
105
+ Title = book.Title,
106
+ Isbn = book.Isbn,
107
+ Author = book.Author,
108
+ Status = book.Status,
109
+ IsActive = book.IsActive,
110
+ Description = book.Description,
111
+ TotalCopies = book.TotalCopies,
112
+ AvailableCopies = book.AvailableCopies,
113
+ CreatedAt = book.CreatedAt,
114
+ UpdatedAt = book.UpdatedAt
115
+ };
116
+ }
117
+ }
118
+
119
+ public class BookDto
120
+ {
121
+ public int Id { get; set; }
122
+ public string Title { get; set; } = string.Empty;
123
+ public string Isbn { get; set; } = string.Empty;
124
+ public string Author { get; set; } = string.Empty;
125
+ public string Status { get; set; } = string.Empty;
126
+ public bool IsActive { get; set; }
127
+ public string? Description { get; set; }
128
+ public int TotalCopies { get; set; }
129
+ public int AvailableCopies { get; set; }
130
+ public DateTime CreatedAt { get; set; }
131
+ public DateTime? UpdatedAt { get; set; }
132
+ }
133
+
134
+ public class BookCreateRequest
135
+ {
136
+ public string Title { get; set; } = string.Empty;
137
+ public string Isbn { get; set; } = string.Empty;
138
+ public string Author { get; set; } = string.Empty;
139
+ public string? Description { get; set; }
140
+ public int TotalCopies { get; set; }
141
+ }
142
+
143
+ public class BookUpdateRequest
144
+ {
145
+ public string Title { get; set; } = string.Empty;
146
+ public string Author { get; set; } = string.Empty;
147
+ public string? Description { get; set; }
148
+ public int TotalCopies { get; set; }
149
+ }
150
+ }
Backend/Features/Borrowing/BorrowingController.cs ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using Microsoft.AspNetCore.Authorization;
2
+ using Microsoft.AspNetCore.Mvc;
3
+ using System.Security.Claims;
4
+
5
+ namespace Backend.Features.Borrowings
6
+ {
7
+ [ApiController]
8
+ [Route("api/borrowings")]
9
+ public class BorrowingController : ControllerBase
10
+ {
11
+ private readonly IBorrowingService _borrowingService;
12
+
13
+ public BorrowingController(IBorrowingService borrowingService)
14
+ {
15
+ _borrowingService = borrowingService;
16
+ }
17
+
18
+ [HttpPost("borrow")]
19
+ //[Authorize]
20
+ public async Task<ActionResult<BorrowingDto>> BorrowBook([FromBody] BorrowRequest request)
21
+ {
22
+ try
23
+ {
24
+ var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier);
25
+ if (string.IsNullOrEmpty(userIdStr)) return Unauthorized();
26
+
27
+ var userId = int.Parse(userIdStr);
28
+ var borrowing = await _borrowingService.BorrowBookAsync(userId, request.BookId);
29
+ return Ok(borrowing);
30
+ }
31
+ catch (Exception ex)
32
+ {
33
+ return BadRequest(new { message = ex.Message });
34
+ }
35
+ }
36
+
37
+ [HttpPost("return/{id}")]
38
+ //[Authorize(Roles = "Librarian")]
39
+ public async Task<ActionResult<BorrowingDto>> ReturnBook(int id)
40
+ {
41
+ try
42
+ {
43
+ var borrowing = await _borrowingService.ReturnBookAsync(id);
44
+ return Ok(borrowing);
45
+ }
46
+ catch (Exception ex)
47
+ {
48
+ return BadRequest(new { message = ex.Message });
49
+ }
50
+ }
51
+
52
+ [HttpGet("me")]
53
+ //[Authorize]
54
+ public async Task<ActionResult<IEnumerable<BorrowingDto>>> GetMyBorrowings()
55
+ {
56
+ var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier);
57
+ if (string.IsNullOrEmpty(userIdStr)) return Unauthorized();
58
+
59
+ var userId = int.Parse(userIdStr);
60
+ var borrowings = await _borrowingService.GetUserBorrowingsAsync(userId);
61
+ return Ok(borrowings);
62
+ }
63
+
64
+ [HttpGet]
65
+ //[Authorize(Roles = "Librarian")]
66
+ public async Task<ActionResult<IEnumerable<BorrowingDto>>> GetAllBorrowings()
67
+ {
68
+ var borrowings = await _borrowingService.GetAllBorrowingsAsync();
69
+ return Ok(borrowings);
70
+ }
71
+ }
72
+ }
Backend/Features/Borrowing/BorrowingModels.cs ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using System;
2
+
3
+ namespace Backend.Features.Borrowings
4
+ {
5
+ public class BorrowingDto
6
+ {
7
+ public int Id { get; set; }
8
+ public int UserId { get; set; }
9
+ public string UserEmail { get; set; } = string.Empty;
10
+ public int BookId { get; set; }
11
+ public string BookTitle { get; set; } = string.Empty;
12
+ public DateTime BorrowDate { get; set; }
13
+ public DateTime DueDate { get; set; }
14
+ public DateTime? ReturnDate { get; set; }
15
+ public string Status { get; set; } = string.Empty;
16
+ public decimal FineAmount { get; set; }
17
+ public bool IsFinePaid { get; set; }
18
+ }
19
+
20
+ public class BorrowRequest
21
+ {
22
+ public int BookId { get; set; }
23
+ }
24
+ }
Backend/Features/Borrowing/BorrowingService.cs ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using Database.Models;
2
+ using Microsoft.EntityFrameworkCore;
3
+
4
+ namespace Backend.Features.Borrowings
5
+ {
6
+ public interface IBorrowingService
7
+ {
8
+ Task<BorrowingDto> BorrowBookAsync(int userId, int bookId);
9
+ Task<BorrowingDto> ReturnBookAsync(int borrowingId);
10
+ Task<IEnumerable<BorrowingDto>> GetUserBorrowingsAsync(int userId);
11
+ Task<IEnumerable<BorrowingDto>> GetAllBorrowingsAsync();
12
+ }
13
+
14
+ public class BorrowingService : IBorrowingService
15
+ {
16
+ private readonly LibraryManagementContext _context;
17
+ private const decimal FinePerDay = 500;
18
+
19
+ public BorrowingService(LibraryManagementContext context)
20
+ {
21
+ _context = context;
22
+ }
23
+
24
+ public async Task<BorrowingDto> BorrowBookAsync(int userId, int bookId)
25
+ {
26
+ // 1. Check for Active Membership
27
+ var subscription = await _context.UserSubscriptions
28
+ .Include(s => s.Membership)
29
+ .Where(s => s.UserId == userId && s.IsActive && s.ExpiryDate > DateTime.UtcNow)
30
+ .FirstOrDefaultAsync();
31
+
32
+ if (subscription == null)
33
+ {
34
+ throw new Exception("Active membership required to borrow books.");
35
+ }
36
+
37
+ // 2. Check for Overdue Books or Unpaid Fines
38
+ var hasBlockers = await _context.Borrowings
39
+ .AnyAsync(b => b.UserId == userId &&
40
+ ((b.Status == "Borrowed" && b.DueDate < DateTime.UtcNow) || (b.FineAmount > 0 && !b.IsFinePaid)));
41
+
42
+ if (hasBlockers)
43
+ {
44
+ throw new Exception("Borrowing blocked: You have overdue books or unpaid fines.");
45
+ }
46
+
47
+ // 3. Check MaxBooks limit
48
+ var activeBorrowCount = await _context.Borrowings
49
+ .CountAsync(b => b.UserId == userId && b.Status == "Borrowed");
50
+
51
+ if (activeBorrowCount >= subscription.Membership.MaxBooks)
52
+ {
53
+ throw new Exception($"Borrowing limit reached: You can only borrow {subscription.Membership.MaxBooks} books at a time.");
54
+ }
55
+
56
+ // 4. Check Book Availability
57
+ var book = await _context.Books.FindAsync(bookId);
58
+ if (book == null || !book.IsActive || book.AvailableCopies <= 0)
59
+ {
60
+ throw new Exception("Book is currently unavailable.");
61
+ }
62
+
63
+ // 5. Create Borrowing Record
64
+ var borrowing = new Borrowing
65
+ {
66
+ UserId = userId,
67
+ BookId = bookId,
68
+ BorrowDate = DateTime.UtcNow,
69
+ DueDate = DateTime.UtcNow.AddDays(subscription.Membership.BorrowingDays),
70
+ Status = "Borrowed",
71
+ FineAmount = 0,
72
+ IsFinePaid = false
73
+ };
74
+
75
+ // 6. Update Book Stock
76
+ book.AvailableCopies--;
77
+ if (book.AvailableCopies == 0)
78
+ {
79
+ book.Status = "Out Of Stock";
80
+ }
81
+
82
+ _context.Borrowings.Add(borrowing);
83
+ await _context.SaveChangesAsync();
84
+
85
+ // Load relations for mapping
86
+ await _context.Entry(borrowing).Reference(b => b.Book).LoadAsync();
87
+ await _context.Entry(borrowing).Reference(b => b.User).LoadAsync();
88
+
89
+ return MapToDto(borrowing);
90
+ }
91
+
92
+ public async Task<BorrowingDto> ReturnBookAsync(int borrowingId)
93
+ {
94
+ var borrowing = await _context.Borrowings
95
+ .Include(b => b.Book)
96
+ .Include(b => b.User)
97
+ .FirstOrDefaultAsync(b => b.Id == borrowingId);
98
+
99
+ if (borrowing == null) throw new Exception("Borrowing record not found.");
100
+ if (borrowing.Status == "Returned") throw new Exception("Book has already been returned.");
101
+
102
+ var now = DateTime.UtcNow;
103
+ borrowing.ReturnDate = now;
104
+ borrowing.Status = "Returned";
105
+
106
+ // Calculate Fine
107
+ if (now > borrowing.DueDate)
108
+ {
109
+ var overdueDays = (now.Date - borrowing.DueDate.Date).Days;
110
+ if (overdueDays > 0)
111
+ {
112
+ borrowing.FineAmount = overdueDays * FinePerDay;
113
+ }
114
+ }
115
+
116
+ // Update Book Stock
117
+ var book = borrowing.Book;
118
+ book.AvailableCopies++;
119
+ book.Status = "Available";
120
+
121
+ await _context.SaveChangesAsync();
122
+ return MapToDto(borrowing);
123
+ }
124
+
125
+ public async Task<IEnumerable<BorrowingDto>> GetUserBorrowingsAsync(int userId)
126
+ {
127
+ var borrowings = await _context.Borrowings
128
+ .Include(b => b.Book)
129
+ .Include(b => b.User)
130
+ .Where(b => b.UserId == userId)
131
+ .OrderByDescending(b => b.BorrowDate)
132
+ .ToListAsync();
133
+
134
+ return borrowings.Select(MapToDto);
135
+ }
136
+
137
+ public async Task<IEnumerable<BorrowingDto>> GetAllBorrowingsAsync()
138
+ {
139
+ var borrowings = await _context.Borrowings
140
+ .Include(b => b.Book)
141
+ .Include(b => b.User)
142
+ .OrderByDescending(b => b.BorrowDate)
143
+ .ToListAsync();
144
+
145
+ return borrowings.Select(MapToDto);
146
+ }
147
+
148
+ private static BorrowingDto MapToDto(Borrowing b)
149
+ {
150
+ return new BorrowingDto
151
+ {
152
+ Id = b.Id,
153
+ UserId = b.UserId,
154
+ UserEmail = b.User?.Email ?? "Unknown",
155
+ BookId = b.BookId,
156
+ BookTitle = b.Book?.Title ?? "Unknown",
157
+ BorrowDate = b.BorrowDate,
158
+ DueDate = b.DueDate,
159
+ ReturnDate = b.ReturnDate,
160
+ Status = b.Status,
161
+ FineAmount = b.FineAmount,
162
+ IsFinePaid = b.IsFinePaid
163
+ };
164
+ }
165
+ }
166
+ }
Backend/Features/Categories/CategoryController.cs ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using Microsoft.AspNetCore.Authorization;
2
+ using Microsoft.AspNetCore.Mvc;
3
+
4
+ namespace Backend.Features.Categories
5
+ {
6
+ [ApiController]
7
+ [Route("api/categories")]
8
+ public class CategoryController : ControllerBase
9
+ {
10
+ private readonly ICategoryService _categoryService;
11
+
12
+ public CategoryController(ICategoryService categoryService)
13
+ {
14
+ _categoryService = categoryService;
15
+ }
16
+
17
+ [HttpGet]
18
+ //[Authorize] // Accessible by both Librarian and Member
19
+ public async Task<ActionResult<IEnumerable<CategoryDto>>> GetCategories()
20
+ {
21
+ var categories = await _categoryService.GetAllCategoriesAsync();
22
+ return Ok(categories);
23
+ }
24
+
25
+ [HttpPost]
26
+ //[Authorize(Roles = "Librarian")]
27
+ public async Task<ActionResult<CategoryDto>> CreateCategory([FromBody] CategoryCreateRequest request)
28
+ {
29
+ try
30
+ {
31
+ var category = await _categoryService.CreateCategoryAsync(request);
32
+ return CreatedAtAction(nameof(GetCategories), category);
33
+ }
34
+ catch (Exception ex)
35
+ {
36
+ return BadRequest(new { message = ex.Message });
37
+ }
38
+ }
39
+
40
+ [HttpDelete("{id}")]
41
+ //[Authorize(Roles = "Librarian")]
42
+ public async Task<IActionResult> DeleteCategory(int id)
43
+ {
44
+ var success = await _categoryService.DeleteCategoryAsync(id);
45
+ if (!success) return NotFound();
46
+ return NoContent();
47
+ }
48
+ }
49
+ }
Backend/Features/Categories/CategoryService.cs ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using Database.Models;
2
+ using Microsoft.EntityFrameworkCore;
3
+
4
+ namespace Backend.Features.Categories
5
+ {
6
+ public interface ICategoryService
7
+ {
8
+ Task<IEnumerable<CategoryDto>> GetAllCategoriesAsync();
9
+ Task<CategoryDto> CreateCategoryAsync(CategoryCreateRequest request);
10
+ Task<bool> DeleteCategoryAsync(int id);
11
+ }
12
+
13
+ public class CategoryService : ICategoryService
14
+ {
15
+ private readonly LibraryManagementContext _context;
16
+
17
+ public CategoryService(LibraryManagementContext context)
18
+ {
19
+ _context = context;
20
+ }
21
+
22
+ public async Task<IEnumerable<CategoryDto>> GetAllCategoriesAsync()
23
+ {
24
+ return await _context.Categories
25
+ .Select(c => new CategoryDto { Id = c.Id, Name = c.Name })
26
+ .ToListAsync();
27
+ }
28
+
29
+ public async Task<CategoryDto> CreateCategoryAsync(CategoryCreateRequest request)
30
+ {
31
+ if (await _context.Categories.AnyAsync(c => c.Name == request.Name))
32
+ {
33
+ throw new Exception("Category with this name already exists.");
34
+ }
35
+
36
+ var category = new Category
37
+ {
38
+ Name = request.Name
39
+ };
40
+
41
+ _context.Categories.Add(category);
42
+ await _context.SaveChangesAsync();
43
+
44
+ return new CategoryDto { Id = category.Id, Name = category.Name };
45
+ }
46
+
47
+ public async Task<bool> DeleteCategoryAsync(int id)
48
+ {
49
+ var category = await _context.Categories.FindAsync(id);
50
+ if (category == null) return false;
51
+
52
+ // Note: EF automatically handles removing associations in many-to-many join tables
53
+ _context.Categories.Remove(category);
54
+ await _context.SaveChangesAsync();
55
+ return true;
56
+ }
57
+ }
58
+
59
+ public class CategoryDto
60
+ {
61
+ public int Id { get; set; }
62
+ public string Name { get; set; } = string.Empty;
63
+ }
64
+
65
+ public class CategoryCreateRequest
66
+ {
67
+ public string Name { get; set; } = string.Empty;
68
+ }
69
+ }
Backend/Features/Subscriptions/SubscriptionController.cs ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using Microsoft.AspNetCore.Authorization;
2
+ using Microsoft.AspNetCore.Mvc;
3
+ using System.Security.Claims;
4
+
5
+ namespace Backend.Features.Subscriptions
6
+ {
7
+ [ApiController]
8
+ [Route("api")]
9
+ public class SubscriptionController : ControllerBase
10
+ {
11
+ private readonly ISubscriptionService _subscriptionService;
12
+
13
+ public SubscriptionController(ISubscriptionService subscriptionService)
14
+ {
15
+ _subscriptionService = subscriptionService;
16
+ }
17
+
18
+ [HttpGet("memberships")]
19
+ [AllowAnonymous]
20
+ public async Task<ActionResult<IEnumerable<MembershipDto>>> GetMemberships()
21
+ {
22
+ return Ok(await _subscriptionService.GetMembershipsAsync());
23
+ }
24
+
25
+ [HttpGet("subscriptions/me")]
26
+ [Authorize]
27
+ public async Task<ActionResult<SubscriptionDto>> GetMySubscription()
28
+ {
29
+ var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier);
30
+ if (string.IsNullOrEmpty(userIdStr)) return Unauthorized();
31
+
32
+ var userId = int.Parse(userIdStr);
33
+ var subscription = await _subscriptionService.GetUserSubscriptionAsync(userId);
34
+
35
+ if (subscription == null) return NotFound(new { message = "No active subscription found." });
36
+
37
+ return Ok(subscription);
38
+ }
39
+
40
+ [HttpPost("subscriptions/subscribe")]
41
+ [Authorize]
42
+ public async Task<ActionResult<SubscriptionDto>> Subscribe([FromBody] SubscribeRequest request)
43
+ {
44
+ try
45
+ {
46
+ var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier);
47
+ if (string.IsNullOrEmpty(userIdStr)) return Unauthorized();
48
+
49
+ var userId = int.Parse(userIdStr);
50
+ var subscription = await _subscriptionService.SubscribeUserAsync(userId, request.MembershipId);
51
+ return Ok(subscription);
52
+ }
53
+ catch (Exception ex)
54
+ {
55
+ return BadRequest(new { message = ex.Message });
56
+ }
57
+ }
58
+
59
+ [HttpGet("subscriptions/user/{userId}")]
60
+ [Authorize(Roles = "Admin")]
61
+ public async Task<ActionResult<SubscriptionDto>> GetUserSubscription(int userId)
62
+ {
63
+ var subscription = await _subscriptionService.GetUserSubscriptionAsync(userId);
64
+ if (subscription == null) return NotFound(new { message = "No active subscription found." });
65
+
66
+ return Ok(subscription);
67
+ }
68
+
69
+ [HttpPost("subscriptions/admin-subscribe")]
70
+ [Authorize(Roles = "Admin")]
71
+ public async Task<ActionResult<SubscriptionDto>> AdminSubscribe([FromBody] AdminSubscribeRequest request)
72
+ {
73
+ try
74
+ {
75
+ var subscription = await _subscriptionService.SubscribeUserAsync(request.UserId, request.MembershipId);
76
+ return Ok(subscription);
77
+ }
78
+ catch (Exception ex)
79
+ {
80
+ return BadRequest(new { message = ex.Message });
81
+ }
82
+ }
83
+ }
84
+ }
Backend/Features/Subscriptions/SubscriptionModels.cs ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using System;
2
+
3
+ namespace Backend.Features.Subscriptions
4
+ {
5
+ public class MembershipDto
6
+ {
7
+ public int Id { get; set; }
8
+ public string Type { get; set; } = string.Empty;
9
+ public int MaxBooks { get; set; }
10
+ public int BorrowingDays { get; set; }
11
+ public decimal Price { get; set; }
12
+ public string Currency { get; set; } = "MMK";
13
+ public int DurationMonths { get; set; }
14
+ }
15
+
16
+ public class SubscriptionDto
17
+ {
18
+ public int Id { get; set; }
19
+ public int UserId { get; set; }
20
+ public int MembershipId { get; set; }
21
+ public string MembershipType { get; set; } = string.Empty;
22
+ public DateTime StartDate { get; set; }
23
+ public DateTime ExpiryDate { get; set; }
24
+ public bool IsActive { get; set; }
25
+ public bool IsExpired => DateTime.UtcNow > ExpiryDate;
26
+ }
27
+
28
+ public class SubscribeRequest
29
+ {
30
+ public int MembershipId { get; set; }
31
+ }
32
+
33
+ public class AdminSubscribeRequest
34
+ {
35
+ public int UserId { get; set; }
36
+ public int MembershipId { get; set; }
37
+ }
38
+ }
Backend/Features/Subscriptions/SubscriptionService.cs ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using Database.Models;
2
+ using Microsoft.EntityFrameworkCore;
3
+
4
+ namespace Backend.Features.Subscriptions
5
+ {
6
+ public interface ISubscriptionService
7
+ {
8
+ Task<IEnumerable<MembershipDto>> GetMembershipsAsync();
9
+ Task<SubscriptionDto?> GetUserSubscriptionAsync(int userId);
10
+ Task<SubscriptionDto> SubscribeUserAsync(int userId, int membershipId);
11
+ }
12
+
13
+ public class SubscriptionService : ISubscriptionService
14
+ {
15
+ private readonly LibraryManagementContext _context;
16
+
17
+ public SubscriptionService(LibraryManagementContext context)
18
+ {
19
+ _context = context;
20
+ }
21
+
22
+ public async Task<IEnumerable<MembershipDto>> GetMembershipsAsync()
23
+ {
24
+ return await _context.Memberships
25
+ .Select(m => new MembershipDto
26
+ {
27
+ Id = m.Id,
28
+ Type = m.Type,
29
+ MaxBooks = m.MaxBooks,
30
+ BorrowingDays = m.BorrowingDays,
31
+ Price = m.Price,
32
+ DurationMonths = m.DurationMonths
33
+ }).ToListAsync();
34
+ }
35
+
36
+ public async Task<SubscriptionDto?> GetUserSubscriptionAsync(int userId)
37
+ {
38
+ var subscription = await _context.UserSubscriptions
39
+ .Include(s => s.Membership)
40
+ .Where(s => s.UserId == userId && s.IsActive)
41
+ .OrderByDescending(s => s.StartDate)
42
+ .FirstOrDefaultAsync();
43
+
44
+ if (subscription == null) return null;
45
+
46
+ return MapToDto(subscription);
47
+ }
48
+
49
+ public async Task<SubscriptionDto> SubscribeUserAsync(int userId, int membershipId)
50
+ {
51
+ var membership = await _context.Memberships.FindAsync(membershipId);
52
+ if (membership == null) throw new Exception("Membership plan not found.");
53
+
54
+ // Deactivate any existing active subscriptions for this user
55
+ var activeSubscriptions = await _context.UserSubscriptions
56
+ .Where(s => s.UserId == userId && s.IsActive)
57
+ .ToListAsync();
58
+
59
+ foreach (var s in activeSubscriptions)
60
+ {
61
+ s.IsActive = false;
62
+ }
63
+
64
+ var subscription = new UserSubscription
65
+ {
66
+ UserId = userId,
67
+ MembershipId = membershipId,
68
+ StartDate = DateTime.UtcNow,
69
+ ExpiryDate = DateTime.UtcNow.AddMonths(membership.DurationMonths),
70
+ IsActive = true
71
+ };
72
+
73
+ _context.UserSubscriptions.Add(subscription);
74
+ await _context.SaveChangesAsync();
75
+
76
+ // Explicitly load the membership data for the DTO mapping
77
+ await _context.Entry(subscription).Reference(s => s.Membership).LoadAsync();
78
+
79
+ return MapToDto(subscription);
80
+ }
81
+
82
+ private static SubscriptionDto MapToDto(UserSubscription subscription)
83
+ {
84
+ return new SubscriptionDto
85
+ {
86
+ Id = subscription.Id,
87
+ UserId = subscription.UserId,
88
+ MembershipId = subscription.MembershipId,
89
+ MembershipType = subscription.Membership.Type,
90
+ StartDate = subscription.StartDate,
91
+ ExpiryDate = subscription.ExpiryDate,
92
+ IsActive = subscription.IsActive
93
+ };
94
+ }
95
+ }
96
+ }
Backend/Features/Users/UserController.cs ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using Microsoft.AspNetCore.Authorization;
2
+ using Microsoft.AspNetCore.Mvc;
3
+
4
+ namespace Backend.Features.Users
5
+ {
6
+ [ApiController]
7
+ [Route("api/users")]
8
+ public class UserController : ControllerBase
9
+ {
10
+ private readonly IUserService _userService;
11
+
12
+ public UserController(IUserService userService)
13
+ {
14
+ _userService = userService;
15
+ }
16
+
17
+ [HttpGet]
18
+ //[Authorize(Roles = "Librarian")]
19
+ public async Task<ActionResult<IEnumerable<UserDto>>> GetUsers()
20
+ {
21
+ var users = await _userService.GetAllUsersAsync();
22
+ return Ok(users);
23
+ }
24
+
25
+ [HttpGet("{id}")]
26
+ //[Authorize(Roles = "Librarian")]
27
+ public async Task<ActionResult<UserDto>> GetUser(int id)
28
+ {
29
+ var user = await _userService.GetUserByIdAsync(id);
30
+ if (user == null) return NotFound();
31
+ return Ok(user);
32
+ }
33
+
34
+ [HttpPost]
35
+ [AllowAnonymous]
36
+ public async Task<ActionResult<UserDto>> CreateUser([FromBody] UserCreateRequest request)
37
+ {
38
+ try
39
+ {
40
+ var response = await _userService.CreateUserAsync(request);
41
+ return CreatedAtAction(nameof(GetUser), new { id = response.Id }, response);
42
+ }
43
+ catch (Exception ex)
44
+ {
45
+ return BadRequest(new { message = ex.Message });
46
+ }
47
+ }
48
+
49
+ [HttpPut("{id}")]
50
+ //[Authorize(Roles = "Librarian")]
51
+ public async Task<ActionResult<UserDto>> UpdateUser(int id, [FromBody] UserUpdateRequest request)
52
+ {
53
+ var updatedUser = await _userService.UpdateUserAsync(id, request);
54
+ if (updatedUser == null) return NotFound();
55
+ return Ok(updatedUser);
56
+ }
57
+
58
+ [HttpPatch("{id}/role")]
59
+ //[Authorize(Roles = "Librarian")]
60
+ public async Task<IActionResult> UpdateUserRole(int id, [FromBody] UserRoleUpdateRequest request)
61
+ {
62
+ var success = await _userService.UpdateUserRoleAsync(id, request.Role);
63
+ if (!success) return NotFound();
64
+ return NoContent();
65
+ }
66
+
67
+ [HttpDelete("{id}")]
68
+ //[Authorize(Roles = "Librarian")]
69
+ public async Task<IActionResult> DeleteUser(int id)
70
+ {
71
+ var success = await _userService.DeleteUserAsync(id);
72
+ if (!success) return NotFound();
73
+ return NoContent();
74
+ }
75
+ }
76
+ }
77
+
Backend/Features/Users/UserService.cs ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using Database.Models;
2
+ using Microsoft.EntityFrameworkCore;
3
+ using Backend.Features.Subscriptions;
4
+ using System;
5
+ using System.Collections.Generic;
6
+ using System.Linq;
7
+ using System.Threading.Tasks;
8
+
9
+ namespace Backend.Features.Users
10
+ {
11
+ public interface IUserService
12
+ {
13
+ Task<IEnumerable<UserDto>> GetAllUsersAsync();
14
+ Task<UserDto?> GetUserByIdAsync(int id);
15
+ Task<UserDto> CreateUserAsync(UserCreateRequest request);
16
+ Task<UserDto?> UpdateUserAsync(int id, UserUpdateRequest request);
17
+ Task<bool> UpdateUserRoleAsync(int id, string role);
18
+ Task<bool> DeleteUserAsync(int id);
19
+ }
20
+
21
+ public class UserService : IUserService
22
+ {
23
+ private readonly LibraryManagementContext _context;
24
+ private readonly ISubscriptionService _subscriptionService;
25
+
26
+ public UserService(LibraryManagementContext context, ISubscriptionService subscriptionService)
27
+ {
28
+ _context = context;
29
+ _subscriptionService = subscriptionService;
30
+ }
31
+
32
+ public async Task<IEnumerable<UserDto>> GetAllUsersAsync()
33
+ {
34
+ var users = await _context.Users.ToListAsync();
35
+ return users.Select(MapToDto);
36
+ }
37
+
38
+ public async Task<UserDto?> GetUserByIdAsync(int id)
39
+ {
40
+ var user = await _context.Users.FindAsync(id);
41
+ return user == null ? null : MapToDto(user);
42
+ }
43
+
44
+ public async Task<UserDto> CreateUserAsync(UserCreateRequest request)
45
+ {
46
+ if (await _context.Users.AnyAsync(u => u.Email == request.Email))
47
+ {
48
+ throw new Exception("Email already registered.");
49
+ }
50
+
51
+ var user = new User
52
+ {
53
+ FullName = request.FullName,
54
+ Email = request.Email,
55
+ PasswordHash = BCrypt.Net.BCrypt.HashPassword(request.Password),
56
+ PhoneNumber = request.PhoneNumber,
57
+ Address = request.Address,
58
+ StudentId = request.StudentId,
59
+ Role = "Member", // Default role
60
+ IsActive = true,
61
+ CreatedAt = DateTime.UtcNow
62
+ };
63
+
64
+ _context.Users.Add(user);
65
+ await _context.SaveChangesAsync();
66
+
67
+ if (!string.IsNullOrEmpty(user.StudentId))
68
+ {
69
+ await _subscriptionService.SubscribeUserAsync(user.Id, 3); // 3 is "Basic Yearly"
70
+ }
71
+
72
+ return MapToDto(user);
73
+ }
74
+
75
+ public async Task<UserDto?> UpdateUserAsync(int id, UserUpdateRequest request)
76
+ {
77
+ var user = await _context.Users.FindAsync(id);
78
+ if (user == null) return null;
79
+
80
+ bool isNewlyStudent = string.IsNullOrEmpty(user.StudentId) && !string.IsNullOrEmpty(request.StudentId);
81
+
82
+ user.FullName = request.FullName;
83
+ user.PhoneNumber = request.PhoneNumber;
84
+ user.Address = request.Address;
85
+ user.StudentId = request.StudentId;
86
+ user.UpdatedAt = DateTime.UtcNow;
87
+
88
+ await _context.SaveChangesAsync();
89
+
90
+ if (isNewlyStudent)
91
+ {
92
+ await _subscriptionService.SubscribeUserAsync(user.Id, 3);
93
+ }
94
+
95
+ return MapToDto(user);
96
+ }
97
+
98
+ public async Task<bool> UpdateUserRoleAsync(int id, string role)
99
+ {
100
+ var user = await _context.Users.FindAsync(id);
101
+ if (user == null) return false;
102
+
103
+ user.Role = role;
104
+ user.UpdatedAt = DateTime.UtcNow;
105
+
106
+ await _context.SaveChangesAsync();
107
+ return true;
108
+ }
109
+
110
+ public async Task<bool> DeleteUserAsync(int id)
111
+ {
112
+ var user = await _context.Users.FindAsync(id);
113
+ if (user == null) return false;
114
+
115
+ // Soft delete
116
+ user.IsActive = false;
117
+ user.UpdatedAt = DateTime.UtcNow;
118
+
119
+ await _context.SaveChangesAsync();
120
+ return true;
121
+ }
122
+
123
+ private static UserDto MapToDto(User user)
124
+ {
125
+ return new UserDto
126
+ {
127
+ Id = user.Id,
128
+ FullName = user.FullName,
129
+ Email = user.Email,
130
+ PhoneNumber = user.PhoneNumber,
131
+ Role = user.Role,
132
+ IsActive = user.IsActive,
133
+ StudentId = user.StudentId,
134
+ Address = user.Address,
135
+ CreatedAt = user.CreatedAt,
136
+ UpdatedAt = user.UpdatedAt,
137
+ BanStatus = user.BanStatus,
138
+ SuspensionEndDate = user.SuspensionEndDate
139
+ };
140
+ }
141
+ }
142
+
143
+ public class UserDto
144
+ {
145
+ public int Id { get; set; }
146
+ public string Name => FullName;
147
+ public string FullName { get; set; } = string.Empty;
148
+ public string Email { get; set; } = string.Empty;
149
+ public string? PhoneNumber { get; set; }
150
+ public string Role { get; set; } = string.Empty;
151
+ public bool IsActive { get; set; }
152
+ public string? StudentId { get; set; }
153
+ public string? Address { get; set; }
154
+ public DateTime CreatedAt { get; set; }
155
+ public DateTime? UpdatedAt { get; set; }
156
+ public bool? BanStatus { get; set; }
157
+ public DateTime? SuspensionEndDate { get; set; }
158
+ }
159
+
160
+ public class UserCreateRequest
161
+ {
162
+ public string FullName { get; set; } = string.Empty;
163
+ public string Email { get; set; } = string.Empty;
164
+ public string Password { get; set; } = string.Empty;
165
+ public string? PhoneNumber { get; set; }
166
+ public string? StudentId { get; set; }
167
+ public string? Address { get; set; }
168
+ }
169
+
170
+ public class UserUpdateRequest
171
+ {
172
+ public string FullName { get; set; } = string.Empty;
173
+ public string? PhoneNumber { get; set; }
174
+ public string? StudentId { get; set; }
175
+ public string? Address { get; set; }
176
+ }
177
+
178
+ public class UserRoleUpdateRequest
179
+ {
180
+ public string Role { get; set; } = string.Empty;
181
+ }
182
+ }
Backend/Program.cs ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using System.Text;
2
+ using Database.Models;
3
+ using Backend.Features.Auth;
4
+ using Backend.Features.Users;
5
+ using Backend.Features.Books;
6
+ using Microsoft.AspNetCore.Authentication.JwtBearer;
7
+ using Microsoft.EntityFrameworkCore;
8
+ using Microsoft.IdentityModel.Tokens;
9
+ using Microsoft.OpenApi.Models;
10
+ using Backend.Features.Subscriptions;
11
+ using Backend.Features.Categories;
12
+ using Backend.Features.Borrowings;
13
+
14
+ var builder = WebApplication.CreateBuilder(args);
15
+
16
+ builder.Services.AddControllers();
17
+ builder.Services.AddAuthorization();
18
+ builder.Services.AddEndpointsApiExplorer();
19
+ builder.Services.AddSwaggerGen(c =>
20
+ {
21
+ c.SwaggerDoc("v1", new OpenApiInfo { Title = "Library Management API", Version = "v1" });
22
+ c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
23
+ {
24
+ Description = "JWT Authorization header using the Bearer scheme.",
25
+ Name = "Authorization",
26
+ In = ParameterLocation.Header,
27
+ Type = SecuritySchemeType.Http,
28
+ Scheme = "bearer",
29
+ BearerFormat = "JWT"
30
+ });
31
+ c.AddSecurityRequirement(new OpenApiSecurityRequirement
32
+ {
33
+ {
34
+ new OpenApiSecurityScheme
35
+ {
36
+ Reference = new OpenApiReference
37
+ {
38
+ Type = ReferenceType.SecurityScheme,
39
+ Id = "Bearer"
40
+ }
41
+ },
42
+ new string[] {}
43
+ }
44
+ });
45
+ });
46
+
47
+ builder.Services.AddDbContext<LibraryManagementContext>(options =>
48
+ options.UseSqlServer(builder.Configuration.GetConnectionString("MssqlConnection")));
49
+
50
+ // Register Services
51
+ builder.Services.AddScoped<IAuthService, AuthService>();
52
+ builder.Services.AddScoped<IUserService, UserService>();
53
+ builder.Services.AddScoped<IBookService, BookService>();
54
+ builder.Services.AddScoped<ISubscriptionService, SubscriptionService>();
55
+ builder.Services.AddScoped<ICategoryService, CategoryService>();
56
+ builder.Services.AddScoped<IBorrowingService, BorrowingService>();
57
+
58
+ // Configure JWT Authentication
59
+ var key = Encoding.ASCII.GetBytes(builder.Configuration["Jwt:Secret"] ?? "YourSuperSecretKeyForLibraryManagementSystem_AtLeast32CharsLong");
60
+ builder.Services.AddAuthentication(x =>
61
+ {
62
+ x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
63
+ x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
64
+ })
65
+ .AddJwtBearer(x =>
66
+ {
67
+ x.RequireHttpsMetadata = false;
68
+ x.SaveToken = true;
69
+ x.TokenValidationParameters = new TokenValidationParameters
70
+ {
71
+ ValidateIssuerSigningKey = true,
72
+ IssuerSigningKey = new SymmetricSecurityKey(key),
73
+ ValidateIssuer = true,
74
+ ValidateAudience = true,
75
+ ValidIssuer = builder.Configuration["Jwt:Issuer"],
76
+ ValidAudience = builder.Configuration["Jwt:Audience"],
77
+ ClockSkew = TimeSpan.Zero
78
+ };
79
+ });
80
+
81
+ var app = builder.Build();
82
+
83
+ app.UseSwagger();
84
+ app.UseSwaggerUI();
85
+ app.UseAuthentication();
86
+ app.UseAuthorization();
87
+
88
+ app.MapControllers();
89
+
90
+ app.Run();
91
+
Backend/Properties/launchSettings.json ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "http://json.schemastore.org/launchsettings.json",
3
+ "iisSettings": {
4
+ "windowsAuthentication": false,
5
+ "anonymousAuthentication": true,
6
+ "iisExpress": {
7
+ "applicationUrl": "http://localhost:13110",
8
+ "sslPort": 44374
9
+ }
10
+ },
11
+ "profiles": {
12
+ "http": {
13
+ "commandName": "Project",
14
+ "dotnetRunMessages": true,
15
+ "launchBrowser": true,
16
+ "launchUrl": "swagger",
17
+ "applicationUrl": "http://localhost:5001",
18
+ "environmentVariables": {
19
+ "ASPNETCORE_ENVIRONMENT": "Development"
20
+ }
21
+ },
22
+ "https": {
23
+ "commandName": "Project",
24
+ "dotnetRunMessages": true,
25
+ "launchBrowser": true,
26
+ "launchUrl": "swagger",
27
+ "applicationUrl": "https://localhost:7028;http://localhost:5199",
28
+ "environmentVariables": {
29
+ "ASPNETCORE_ENVIRONMENT": "Development"
30
+ }
31
+ },
32
+ "IIS Express": {
33
+ "commandName": "IISExpress",
34
+ "launchBrowser": true,
35
+ "launchUrl": "swagger",
36
+ "environmentVariables": {
37
+ "ASPNETCORE_ENVIRONMENT": "Development"
38
+ }
39
+ }
40
+ }
41
+ }
Backend/WeatherForecast.cs ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ namespace Backend
2
+ {
3
+ public class WeatherForecast
4
+ {
5
+ public DateOnly Date { get; set; }
6
+
7
+ public int TemperatureC { get; set; }
8
+
9
+ public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
10
+
11
+ public string? Summary { get; set; }
12
+ }
13
+ }
Backend/appsettings.Development.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "Logging": {
3
+ "LogLevel": {
4
+ "Default": "Information",
5
+ "Microsoft.AspNetCore": "Warning"
6
+ }
7
+ }
8
+ }
Backend/appsettings.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "Logging": {
3
+ "LogLevel": {
4
+ "Default": "Information",
5
+ "Microsoft.AspNetCore": "Warning"
6
+ }
7
+ },
8
+ "AllowedHosts": "*",
9
+ "ConnectionStrings": {
10
+ //"MssqlConnection": "Server=DESKTOP-BP9A061;Database=LibraryManagement;User Id=sa;Password=sasa@123;TrustServerCertificate=True;"
11
+ "MssqlConnection": "Server=.;Database=LibraryManagement;User Id=sa;Password=sasa@123;TrustServerCertificate=True;"
12
+ },
13
+ "Jwt": {
14
+ "Secret": "YourSuperSecretKeyForLibraryManagementSystem_AtLeast32CharsLong",
15
+ "Issuer": "LibraryManagementSystem",
16
+ "Audience": "LibraryManagementUsers"
17
+ }
18
+ }
LibraryManagement.slnx CHANGED
@@ -1,5 +1,5 @@
1
  <Solution>
2
  <Project Path="Database/Database.csproj" Id="1ec23dc7-355d-4456-a503-cfd0fb81cd5c" />
3
  <Project Path="Frontend/Frontend.csproj" Id="824d7fed-4fd3-41c5-8c5e-acd458ccbcdb" />
4
- <Project Path="LibraryManagement.Backend/LibraryManagement.Backend.csproj" Id="82e311b1-ce76-4235-9d5f-0d06a942d038" />
5
  </Solution>
 
1
  <Solution>
2
  <Project Path="Database/Database.csproj" Id="1ec23dc7-355d-4456-a503-cfd0fb81cd5c" />
3
  <Project Path="Frontend/Frontend.csproj" Id="824d7fed-4fd3-41c5-8c5e-acd458ccbcdb" />
4
+ <Project Path="Backend/Backend.csproj" Id="82e311b1-ce76-4235-9d5f-0d06a942d038" />
5
  </Solution>