3 Commits

11 changed files with 299 additions and 92 deletions

View File

@@ -20,12 +20,6 @@ public class AccountController : Controller
return View();
}
[HttpGet("Register")]
public IActionResult Register()
{
return View();
}
[HttpGet("Logout")]
public async Task<IActionResult> Logout()
{

View File

@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using OneForMe.Services;
namespace OneForMe.Controllers;
@@ -7,42 +8,78 @@ namespace OneForMe.Controllers;
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly UserManager<IdentityUser> _userManager;
private readonly ILdapService _ldapService;
private readonly SignInManager<IdentityUser> _signInManager;
private readonly UserManager<IdentityUser> _userManager;
private readonly ILogger<AuthController> _logger;
public AuthController(UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager)
public AuthController(
ILdapService ldapService,
SignInManager<IdentityUser> signInManager,
UserManager<IdentityUser> userManager,
ILogger<AuthController> logger)
{
_userManager = userManager;
_ldapService = ldapService;
_signInManager = signInManager;
_userManager = userManager;
_logger = logger;
}
/// <summary>
/// Register a new user
/// </summary>
[HttpPost("register")]
public async Task<IActionResult> Register([FromBody] RegisterRequest request)
{
var user = new IdentityUser { UserName = request.Email, Email = request.Email };
var result = await _userManager.CreateAsync(user, request.Password);
if (result.Succeeded)
return Ok(new { message = "User registered successfully" });
return BadRequest(new { errors = result.Errors.Select(e => e.Description) });
}
/// <summary>
/// Login user
/// Login user with LDAP credentials
/// </summary>
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest request)
{
var result = await _signInManager.PasswordSignInAsync(request.Email, request.Password, false, false);
try
{
// Authenticate with LDAP
var isAuthenticated = await _ldapService.AuthenticateAsync(request.Username, request.Password);
if (!isAuthenticated)
{
_logger.LogWarning($"LDAP authentication failed for user {request.Username}");
return Unauthorized(new { message = "Invalid username or password" });
}
if (result.Succeeded)
// Get user details from LDAP
var ldapUser = await _ldapService.GetUserAsync(request.Username);
if (ldapUser == null)
{
_logger.LogWarning($"Could not retrieve LDAP user details for {request.Username}");
return Unauthorized(new { message = "Could not retrieve user details" });
}
// Check if user exists in database, if not create them
var user = await _userManager.FindByNameAsync(request.Username);
if (user == null)
{
user = new IdentityUser
{
UserName = request.Username,
Email = ldapUser.Email,
EmailConfirmed = true
};
var result = await _userManager.CreateAsync(user);
if (!result.Succeeded)
{
_logger.LogError($"Failed to create user {request.Username} in database");
return BadRequest(new { message = "Failed to create user account" });
}
_logger.LogInformation($"Created new user {request.Username} in database");
}
// Sign in the user
await _signInManager.SignInAsync(user, false);
_logger.LogInformation($"User {request.Username} signed in successfully");
return Ok(new { message = "Login successful" });
return Unauthorized(new { message = "Invalid email or password" });
}
catch (Exception ex)
{
_logger.LogError($"Login error: {ex.Message}");
return StatusCode(500, new { message = "An error occurred during login" });
}
}
/// <summary>
@@ -52,18 +89,13 @@ public class AuthController : ControllerBase
public async Task<IActionResult> Logout()
{
await _signInManager.SignOutAsync();
_logger.LogInformation("User logged out");
return Ok(new { message = "Logout successful" });
}
}
public class RegisterRequest
{
public string Email { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
public class LoginRequest
{
public string Email { get; set; } = string.Empty;
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}

View File

@@ -125,7 +125,7 @@ public class OrderController : Controller
_context.OrderItems.Add(orderItem);
await _context.SaveChangesAsync();
return RedirectToAction("Join", new { code = order.OrderCode });
return RedirectToAction("Details" , new { code = order.OrderCode });
}
[HttpPost]

View File

@@ -17,6 +17,7 @@
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Novell.Directory.Ldap.NETStandard" Version="3.6.0" />
</ItemGroup>
</Project>

View File

@@ -54,6 +54,9 @@ builder.Services.AddIdentity<IdentityUser, IdentityRole>(options =>
// Add LocalizationService
builder.Services.AddScoped<LocalizationService>();
// Add LDAP Service
builder.Services.AddScoped<ILdapService, LdapService>();
var app = builder.Build();
// Create and migrate database on startup

139
Services/LdapService.cs Normal file
View File

@@ -0,0 +1,139 @@
using Novell.Directory.Ldap;
using Microsoft.Extensions.Configuration;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
namespace OneForMe.Services;
public interface ILdapService
{
Task<bool> AuthenticateAsync(string username, string password);
Task<LdapUser> GetUserAsync(string username);
}
public partial class LdapService : ILdapService
{
public Dictionary<string, HashAlgorithm> HashAlgorithms => new() { ["MD5"] = MD5.Create(), ["SHA1"] = SHA1.Create(), ["SHA256"] = SHA256.Create(), ["SHA384"] = SHA384.Create(), ["SHA512"] = SHA512.Create(), ["SSHA"] = SHA1.Create(), ["SSHA256"] = SHA256.Create(), ["SSHA384"] = SHA384.Create(), ["SSHA512"] = SHA512.Create()};
private readonly IConfiguration _configuration;
private readonly ILogger<LdapService> _logger;
public LdapService(IConfiguration configuration, ILogger<LdapService> logger)
{
_configuration = configuration;
_logger = logger;
}
public async Task<bool> AuthenticateAsync(string username, string password)
{
try
{
LdapUser user = await GetUserAsync(username);
bool hashMatches = CompareStringToHashed(password, user.Password);
_logger.LogInformation("User {username} authenticated successfully via LDAP", [username]);
return hashMatches;
}
catch (LdapException ex)
{
_logger.LogWarning("LDAP authentication failed for user {username}: {ex.Message}", [username, ex.Message]);
return false;
}
catch (Exception ex)
{
_logger.LogError("LDAP authentication error for user {username}: {ex.Message}", [username, ex.Message]);
return false;
}
}
public async Task<LdapUser> GetUserAsync(string username)
{
try
{
var connection = new LdapConnection();
var ldapSettings = _configuration.GetSection("LdapSettings");
var serverName = ldapSettings["ServerName"];
var port = int.Parse(ldapSettings["Port"] ?? "389");
var baseDn = ldapSettings["BaseDn"];
var bindDn = ldapSettings["BindDn"];
var bindPassword = ldapSettings["BindPassword"];
connection.Connect(serverName, port);
connection.Bind(bindDn, bindPassword);
var searchFilter = $"(uid={username})";
var results = connection.Search(
baseDn,
LdapConnection.ScopeSub,
searchFilter,
new[] { "uid", "mail", "cn", "sn", "userPassword" },
false
);
LdapUser user = null;
while (results.HasMore())
{
var entry = results.Next();
LdapAttributeSet attributeSet = entry.GetAttributeSet();
attributeSet.TryGetValue("uid", out LdapAttribute? uid);
attributeSet.TryGetValue("mail", out LdapAttribute? mail);
attributeSet.TryGetValue("cn", out LdapAttribute? cn);
attributeSet.TryGetValue("sn", out LdapAttribute? sn);
attributeSet.TryGetValue("userPassword", out LdapAttribute? userPassword);
user = new LdapUser
{
Username = uid?.StringValue ?? username, //entry.GetAttribute("uid")?.StringValue ?? username,
Email = mail?.StringValue ?? "", //entry.GetAttribute("mail")?.StringValue ?? $"{username}@example.com",
FullName = cn?.StringValue ?? "", //entry.GetAttribute("cn")?.StringValue ?? username,
LastName = sn?.StringValue ?? "", //entry.GetAttribute("sn")?.StringValue ?? "",
Password = userPassword?.StringValue ?? "" //entry.GetAttribute("userPassword")?.StringValue ?? ""
};
}
connection.Disconnect();
return user;
}
catch (Exception ex)
{
_logger.LogError($"Error retrieving LDAP user {username}: {ex.Message}");
return null;
}
}
public bool CompareStringToHashed(string sourcePassword, string targetPasswordHashed)
{
string algorithmName = CurlyBracesExtractor().Match(targetPasswordHashed).Groups[1].Value;
HashAlgorithm hashAlgorithm = HashAlgorithms[algorithmName.ToUpperInvariant()];
byte[] sourcePasswordBytes = Encoding.UTF8.GetBytes(sourcePassword);
byte[] targetPasswordHashedBytes = Convert.FromBase64String(CurlyBracesRemover().Replace(targetPasswordHashed, ""));
int hashSize = hashAlgorithm.HashSize / 8;
if (targetPasswordHashedBytes.Length > hashSize) // Has salt
{
int saltLength = targetPasswordHashedBytes.Length - hashSize;
byte[] salt = new byte[saltLength];
Array.Copy(targetPasswordHashedBytes, hashSize, salt, 0, saltLength);
Array.Resize(ref targetPasswordHashedBytes, hashSize);
byte[] newSourcePasswordBytes = new byte[sourcePasswordBytes.Length + salt.Length];
Array.Copy(sourcePasswordBytes, 0, newSourcePasswordBytes, 0, sourcePasswordBytes.Length);
Array.Copy(salt, 0, newSourcePasswordBytes, sourcePasswordBytes.Length, salt.Length);
sourcePasswordBytes = newSourcePasswordBytes;
}
sourcePasswordBytes = hashAlgorithm.ComputeHash(sourcePasswordBytes);
return CryptographicOperations.FixedTimeEquals(sourcePasswordBytes, targetPasswordHashedBytes);
}
[GeneratedRegex(@"\{.*?\}")]
private static partial Regex CurlyBracesRemover();
[GeneratedRegex(@"\{([^}]*)\}")]
private static partial Regex CurlyBracesExtractor();
}
public class LdapUser
{
public string Username { get; set; }
public string Password { get; set; }
public string Email { get; set; }
public string FullName { get; set; }
public string LastName { get; set; }
}

View File

@@ -14,8 +14,8 @@
<form id="loginForm">
<div class="mb-3">
<label for="email" class="form-label">@Localizer["Email"]</label>
<input type="email" class="form-control" id="email" name="email" required>
<label for="username" class="form-label">@Localizer["Username"]</label>
<input type="text" class="form-control" id="username" name="username" required autofocus>
</div>
<div class="mb-3">
@@ -26,10 +26,6 @@
<button type="submit" class="btn btn-primary w-100">@Localizer["Login"]</button>
</form>
<hr>
<p class="text-center">@Localizer["DontHaveAccount"] <a asp-controller="Account" asp-action="Register">@Localizer["RegisterHere"]</a></p>
<div id="message" class="mt-3"></div>
</div>
</div>
@@ -41,7 +37,7 @@
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
const email = document.getElementById('email').value;
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const messageDiv = document.getElementById('message');
@@ -51,7 +47,7 @@ document.getElementById('loginForm').addEventListener('submit', async (e) => {
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ email, password })
body: JSON.stringify({ username, password })
});
if (response.ok) {

View File

@@ -93,7 +93,7 @@
</div>
</div>
<div class="card shadow">
<div class="card shadow mb-4">
<div class="card-header bg-info text-white">
<h5 class="mb-0">@Localizer.Get("Orders") (@Model.OrderItems.Count)</h5>
</div>
@@ -176,6 +176,10 @@
}
</div>
</div>
@if (!Model.IsClosed)
{
<partial name="PlaceOrder" model="Model" />
}
</div>
<div class="col-md-4">

View File

@@ -28,51 +28,7 @@
</div>
</div>
<div class="card shadow mb-4">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">@Localizer["MenuItems"]</h5>
</div>
<div class="card-body">
@if (Model.MenuItems.Any())
{
<form method="post" action="/order/additem">
<input type="hidden" name="orderId" value="@Model.Id">
<div class="mb-3">
<label for="menuItemId" class="form-label">@Localizer["SelectItem"]</label>
<select class="form-select" id="menuItemId" name="menuItemId" required onchange="updatePrice()">
<option value="">-- @Localizer["ChooseAnItem"] --</option>
@foreach (var item in Model.MenuItems)
{
<option value="@item.Id" data-price="@item.Price.ToString("F2")">@item.Name - @Localizer["Currency", item.Price.ToString("F2")]</option>
}
</select>
</div>
<div class="mb-3">
<label for="quantity" class="form-label">@Localizer["Qty"]</label>
<input type="number" class="form-control" id="quantity" name="quantity" value="1" min="1" step="1" required>
</div>
<div class="mb-3">
<label for="comments" class="form-label">@Localizer["Comments"]</label>
<input class="form-control" id="comments" name="comments"></input>
</div>
<div class="alert alert-info">
<strong>@Localizer["Total"]:</strong> @Html.Raw(Localizer["Currency", "<span id=\"totalPrice\">0.00</span>"])
</div>
<button type="submit" class="btn btn-success">@Localizer["AddToOrder"]</button>
<a href="/Home/Dashboard" class="btn btn-secondary">@Localizer["BackToDashboard"]</a>
</form>
}
else
{
<p class="text-muted">@Localizer["NoItemsAvailable"]</p>
}
</div>
</div>
<partial name="PlaceOrder" model="Model" />
</div>
<div class="col-md-4">

View File

@@ -0,0 +1,67 @@
@using OneForMe.Services
@inject LocalizationService Localizer
@model OneForMe.Models.Order
<div class="card shadow mb-4">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">@Localizer["MenuItems"]</h5>
</div>
<div class="card-body">
@if (Model.MenuItems.Any())
{
<form method="post" action="/order/additem">
<input type="hidden" name="orderId" value="@Model.Id">
<div class="mb-3">
<label for="menuItemId" class="form-label">@Localizer["SelectItem"]</label>
<select class="form-select" id="menuItemId" name="menuItemId" required onchange="updatePrice()">
<option value="">-- @Localizer["ChooseAnItem"] --</option>
@foreach (var item in Model.MenuItems)
{
<option value="@item.Id" data-price="@item.Price.ToString("F2")">@item.Name - @Localizer["Currency", item.Price.ToString("F2")]</option>
}
</select>
</div>
<div class="mb-3">
<label for="quantity" class="form-label">@Localizer["Qty"]</label>
<input type="number" class="form-control" id="quantity" name="quantity" value="1" min="1" step="1" required>
</div>
<div class="mb-3">
<label for="comments" class="form-label">@Localizer["Comments"]</label>
<input class="form-control" id="comments" name="comments"></input>
</div>
<div class="alert alert-info">
<strong>@Localizer["Total"]:</strong> @Html.Raw(Localizer["Currency", "<span id=\"totalPrice\">0.00</span>"])
</div>
<button type="submit" class="btn btn-success">@Localizer["AddToOrder"]</button>
<a href="/Home/Dashboard" class="btn btn-secondary">@Localizer["BackToDashboard"]</a>
</form>
}
else
{
<p class="text-muted">@Localizer["NoItemsAvailable"]</p>
}
</div>
</div>
@section Scripts {
<script defer>
function updatePrice() {
const select = document.getElementById('menuItemId');
const quantity = document.getElementById('quantity').value;
const selectedOption = select.options[select.selectedIndex];
const price = parseFloat(selectedOption.dataset.price.replace(",", ".")) || 0;
const total = (price * quantity).toFixed(2);
document.getElementById('totalPrice').textContent = total;
}
document.getElementById('quantity').addEventListener('change', updatePrice);
document.getElementById('quantity').addEventListener('input', updatePrice);
document.getElementById('menuItemId').addEventListener('change', updatePrice);
</script>
}

View File

@@ -9,5 +9,20 @@
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
"AllowedHosts": "*",
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://0.0.0.0:5000"
}
}
},
"LdapSettings": {
"ServerName": "192.168.0.117",
"Port": "389",
"BaseDn": "dc=ham,dc=ld50,dc=dev",
"UserDnTemplate": "uid={0},ou=users,dc=ham,dc=ld50,dc=dev",
"BindDn": "cn=admin,dc=ham,dc=ld50,dc=dev",
"BindPassword": "TestLDAP"
}
}