feat: add user management, refs NOISSUE

This commit is contained in:
simon 2025-05-17 22:17:09 +02:00
parent 6b9c383697
commit efbbc915e5
17 changed files with 908 additions and 97 deletions

View File

@ -10,7 +10,8 @@ public class ConfigurationAD
public string LDAPserver { get; set; } = string.Empty;
public string LDAPQueryBase { get; set; } = string.Empty;
public string LDAPUserQueryBase { get; set; } = string.Empty;
public string Crew { get; set; } = string.Empty;
public string Managers { get; set; } = string.Empty;
public string CrewGroup { get; set; } = string.Empty;
public string ManagerGroup { get; set; } = string.Empty;
}

View File

@ -0,0 +1,4 @@
public interface IAuthenticationService
{
bool Authenticate(string userName, string password);
}

View File

@ -0,0 +1,15 @@
public class LdapAuthenticationService : IAuthenticationService
{
private readonly IConfiguration _configuration;
public LdapAuthenticationService(IConfiguration configuration)
{
_configuration = configuration;
}
public bool Authenticate(string username, string password)
{
var ldapInstance = LdapIntegration.Instance;
return ldapInstance.TestLogin(username, password);
}
}

View File

@ -0,0 +1,58 @@
public class PhoneClaimCodeProviderService
{
private Dictionary<string, string> _phoneClaimCodes;
private Dictionary<string, string> _phoneClaimNumbers;
public PhoneClaimCodeProviderService()
{
_phoneClaimCodes = new Dictionary<string, string>();
_phoneClaimNumbers = new Dictionary<string, string>();
}
private static Random random = new Random();
private static string RandomString(int length)
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
return new string(Enumerable.Repeat(chars, length)
.Select(s => s[random.Next(s.Length)]).ToArray());
}
public void GenerateClaimCodeForUserAndNumber(string username, string phoneNumber)
{
var generatedCode = string.Empty;
if (IsCodeGeneratedForUser(username))
{
generatedCode = _phoneClaimCodes[username];
}
else
{
generatedCode = RandomString(6);
_phoneClaimCodes[username] = generatedCode;
_phoneClaimNumbers[username] = phoneNumber;
}
SignalIntegration.Instance.SendMessageToUserAsync("Your phone number validation code is: " + generatedCode, phoneNumber);
}
public string ValidateClaimCodeForUser(string code, string username)
{
var result = false;
result = _phoneClaimCodes[username] == code;
if (result)
{
_phoneClaimCodes.Remove(username);
var number = _phoneClaimNumbers[username];
_phoneClaimNumbers.Remove(username);
return number;
}
return string.Empty;
}
public bool IsCodeGeneratedForUser(string username)
{
return _phoneClaimCodes.ContainsKey(username);
}
}

View File

@ -16,22 +16,21 @@ public class AppConfiguration
this.SignalGroupId = Environment.GetEnvironmentVariable("SIGNAL_GROUP_ID") ?? "group.Wmk1UTVQTnh0Sjd6a0xiOGhnTnMzZlNkc2p2Q3c0SXJiQkU2eDlNU0hyTT0=";
this.WebUIBaseURL = Environment.GetEnvironmentVariable("WEB_BASE_URL") ?? "https://sotd.disi.dev/";
this.UseBotTag = bool.Parse(Environment.GetEnvironmentVariable("USE_BOT_TAG") ?? "true");
this.AverageDaysBetweenRequests = int.Parse(Environment.GetEnvironmentVariable("AVERAGE_DAYS_BETWEEN_REQUESTS") ?? "2");
var managersGroupName = Environment.GetEnvironmentVariable("LDAP_ADMINGROUP") ?? "admins";
var userGroupName = Environment.GetEnvironmentVariable("LDAP_USERGROUP") ?? "everyone";
var userGroupName = Environment.GetEnvironmentVariable("LDAP_USERGROUP") ?? "everybody";
this.LDAPConfig = new ConfigurationAD() {
Username = Environment.GetEnvironmentVariable("LDAP_BIND") ?? "cn=admin.dc=disi,dc=dev",
Username = Environment.GetEnvironmentVariable("LDAP_BIND") ?? "cn=admin,dc=disi,dc=dev",
Password = Environment.GetEnvironmentVariable("LDAP_PASS") ?? "adminPass2022!",
Port = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("LDAP_BIND")) ? int.Parse(Environment.GetEnvironmentVariable("LDAP_BIND")) : 389,
LDAPserver = Environment.GetEnvironmentVariable("LDAP_URL") ?? "192.168.1.108",
LDAPQueryBase = Environment.GetEnvironmentVariable("LDAP_BASE") ?? "dc=disi,dc=dev",
Crew = $"cn={userGroupName},ou=groups,dc=disi,dc=dev",
Managers = $"cn={managersGroupName},ou=groups,dc=disi,dc=dev"
LDAPUserQueryBase = Environment.GetEnvironmentVariable("LDAP_BASE") ?? "ou=people,dc=disi,dc=dev",
CrewGroup = $"cn={userGroupName},ou=groups,dc=disi,dc=dev",
ManagerGroup = $"cn={managersGroupName},ou=groups,dc=disi,dc=dev"
};
}
public string Crew { get; set; } = string.Empty;
public string Managers { get; set; } = string.Empty;
public string SignalAPIEndpointUri
{
get; private set;
@ -87,6 +86,11 @@ public class AppConfiguration
get; private set;
}
public int AverageDaysBetweenRequests
{
get; private set;
}
public ConfigurationAD LDAPConfig {
get; private set;
}

View File

@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication;
public class AuthController : Controller
{
[HttpPost]
public async Task<IActionResult> Login(string username, string password)
{
var ldapService = HttpContext.RequestServices.GetService<LdapAuthenticationService>();
if (ldapService.Authenticate(username, password))
{
var claims = new[] { new Claim(ClaimTypes.Name, username) };
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignInAsync(new ClaimsPrincipal(identity));
return RedirectToSamePageIfPossible();
}
ViewBag.Error = "Invalid credentials";
return RedirectToSamePageIfPossible();
}
[HttpPost]
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync();
return RedirectToSamePageIfPossible();
}
private IActionResult RedirectToSamePageIfPossible()
{
if (Request.Headers.ContainsKey("Referer"))
{
return Redirect(Request.Headers["Referer"].ToString());
}
return RedirectToPage("/");
}
}

View File

@ -0,0 +1,142 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace song_of_the_day.DataMigrations
{
[DbContext(typeof(DataContext))]
[Migration("20250516161831_UpdateUserModel")]
partial class UpdateUserModel
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Song", b =>
{
b.Property<int>("SongId")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("SongId"));
b.Property<string>("Artist")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Url")
.HasColumnType("text");
b.HasKey("SongId");
b.ToTable("Songs");
});
modelBuilder.Entity("SongSuggestion", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone");
b.Property<int?>("SongId")
.HasColumnType("integer");
b.Property<int?>("UserId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("SongId");
b.HasIndex("UserId");
b.ToTable("SongSuggestions");
});
modelBuilder.Entity("SuggestionHelper", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Description")
.HasColumnType("text");
b.Property<string>("Title")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("SuggestionHelpers");
});
modelBuilder.Entity("User", b =>
{
b.Property<int>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("UserId"));
b.Property<bool>("AssociationInProgress")
.HasColumnType("boolean");
b.Property<bool>("IsIntroduced")
.HasColumnType("boolean");
b.Property<string>("LdapUserName")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("NickName")
.HasColumnType("text");
b.Property<string>("SignalMemberId")
.HasColumnType("text");
b.HasKey("UserId");
b.ToTable("Users");
});
modelBuilder.Entity("SongSuggestion", b =>
{
b.HasOne("Song", "Song")
.WithMany()
.HasForeignKey("SongId");
b.HasOne("User", "User")
.WithMany()
.HasForeignKey("UserId");
b.Navigation("Song");
b.Navigation("User");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,265 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace song_of_the_day.DataMigrations
{
/// <inheritdoc />
public partial class UpdateUserModel : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_SongSuggestions_Songs_SongId",
table: "SongSuggestions");
migrationBuilder.DropForeignKey(
name: "FK_SongSuggestions_Users_UserId",
table: "SongSuggestions");
migrationBuilder.AlterColumn<string>(
name: "SignalMemberId",
table: "Users",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "NickName",
table: "Users",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "Users",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AddColumn<bool>(
name: "AssociationInProgress",
table: "Users",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<string>(
name: "LdapUserName",
table: "Users",
type: "text",
nullable: true);
migrationBuilder.AlterColumn<string>(
name: "Title",
table: "SuggestionHelpers",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "Description",
table: "SuggestionHelpers",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<int>(
name: "UserId",
table: "SongSuggestions",
type: "integer",
nullable: true,
oldClrType: typeof(int),
oldType: "integer");
migrationBuilder.AlterColumn<int>(
name: "SongId",
table: "SongSuggestions",
type: "integer",
nullable: true,
oldClrType: typeof(int),
oldType: "integer");
migrationBuilder.AlterColumn<string>(
name: "Url",
table: "Songs",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "Songs",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "Artist",
table: "Songs",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AddForeignKey(
name: "FK_SongSuggestions_Songs_SongId",
table: "SongSuggestions",
column: "SongId",
principalTable: "Songs",
principalColumn: "SongId");
migrationBuilder.AddForeignKey(
name: "FK_SongSuggestions_Users_UserId",
table: "SongSuggestions",
column: "UserId",
principalTable: "Users",
principalColumn: "UserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_SongSuggestions_Songs_SongId",
table: "SongSuggestions");
migrationBuilder.DropForeignKey(
name: "FK_SongSuggestions_Users_UserId",
table: "SongSuggestions");
migrationBuilder.DropColumn(
name: "AssociationInProgress",
table: "Users");
migrationBuilder.DropColumn(
name: "LdapUserName",
table: "Users");
migrationBuilder.AlterColumn<string>(
name: "SignalMemberId",
table: "Users",
type: "text",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "NickName",
table: "Users",
type: "text",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "Users",
type: "text",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Title",
table: "SuggestionHelpers",
type: "text",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Description",
table: "SuggestionHelpers",
type: "text",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.AlterColumn<int>(
name: "UserId",
table: "SongSuggestions",
type: "integer",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "integer",
oldNullable: true);
migrationBuilder.AlterColumn<int>(
name: "SongId",
table: "SongSuggestions",
type: "integer",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "integer",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Url",
table: "Songs",
type: "text",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "Songs",
type: "text",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Artist",
table: "Songs",
type: "text",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.AddForeignKey(
name: "FK_SongSuggestions_Songs_SongId",
table: "SongSuggestions",
column: "SongId",
principalTable: "Songs",
principalColumn: "SongId",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_SongSuggestions_Users_UserId",
table: "SongSuggestions",
column: "UserId",
principalTable: "Users",
principalColumn: "UserId",
onDelete: ReferentialAction.Cascade);
}
}
}

View File

@ -30,15 +30,12 @@ namespace song_of_the_day.DataMigrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("SongId"));
b.Property<string>("Artist")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Url")
.IsRequired()
.HasColumnType("text");
b.HasKey("SongId");
@ -57,10 +54,10 @@ namespace song_of_the_day.DataMigrations
b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone");
b.Property<int>("SongId")
b.Property<int?>("SongId")
.HasColumnType("integer");
b.Property<int>("UserId")
b.Property<int?>("UserId")
.HasColumnType("integer");
b.HasKey("Id");
@ -81,11 +78,9 @@ namespace song_of_the_day.DataMigrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
@ -101,19 +96,22 @@ namespace song_of_the_day.DataMigrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("UserId"));
b.Property<bool>("AssociationInProgress")
.HasColumnType("boolean");
b.Property<bool>("IsIntroduced")
.HasColumnType("boolean");
b.Property<string>("LdapUserName")
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("NickName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SignalMemberId")
.IsRequired()
.HasColumnType("text");
b.HasKey("UserId");
@ -125,15 +123,11 @@ namespace song_of_the_day.DataMigrations
{
b.HasOne("Song", "Song")
.WithMany()
.HasForeignKey("SongId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
.HasForeignKey("SongId");
b.HasOne("User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
.HasForeignKey("UserId");
b.Navigation("Song");

View File

@ -7,4 +7,6 @@ public class User
public string? Name { get; set; }
public string? NickName { get; set; }
public bool IsIntroduced { get; set; }
public bool AssociationInProgress { get; set; }
public string? LdapUserName { get; set; }
}

View File

@ -0,0 +1,7 @@
public class LdapUser
{
public string? UserId { get; set; }
public string? FirstName { get; set; }
public string? LastName { get; set; }
public string? Email { get; set; }
}

View File

@ -0,0 +1,124 @@
using System.Collections;
using System.ComponentModel;
using song_of_the_day;
using System.DirectoryServices.Protocols;
using System.Linq;
public class LdapIntegration
{
public static LdapIntegration? Instance;
private readonly string[] attributesToQuery = new string[]
{
"uid",
"givenName",
"sn",
"mail"
};
public LdapIntegration(string uri, int port, string adminBind, string adminPass)
{
this.Uri = uri;
this.Port = port;
this.AdminBind = adminBind;
this.AdminPass = adminPass;
}
private string Uri { get; set; }
private int Port { get; set; }
private string AdminBind { get; set; }
private string AdminPass { get; set; }
public bool TestLogin(string username, string password)
{
try
{
var userList = this.SearchInADAsUser(
AppConfiguration.Instance.LDAPConfig.LDAPQueryBase,
$"(uid={username})",
SearchScope.Subtree,
username,
password);
}
catch (LdapException ex)
{
if (ex.Message.Contains("credential is invalid"))
{
return false;
}
throw;
}
return true;
}
public List<LdapUser> SearchInAD(
string targetOU,
string query,
SearchScope scope
)
{
// search as admin
return this.SearchInADAsUser(targetOU, query, scope, this.AdminBind, this.AdminPass);
}
public List<LdapUser> SearchInADAsUser(
string targetOU,
string query,
SearchScope scope,
string userName,
string userPass
)
{
// on Windows the authentication type is Negotiate, so there is no need to prepend
// AD user login with domain. On other platforms at the moment only
// Basic authentication is supported
var authType = AuthType.Basic;
var user = userName.StartsWith("cn=") || userName.StartsWith("uid=") ? userName : "uid=" + userName + "," + AppConfiguration.Instance.LDAPConfig.LDAPUserQueryBase;
//var connection = new LdapConnection(ldapServer)
var connection = new LdapConnection(
new LdapDirectoryIdentifier(this.Uri, this.Port)
)
{
AuthType = authType,
Credential = new(user, userPass)
};
// the default one is v2 (at least in that version), and it is unknown if v3
// is actually needed, but at least Synology LDAP works only with v3,
// and since our Exchange doesn't complain, let it be v3
connection.SessionOptions.ProtocolVersion = 3;
// this is for connecting via LDAPS (636 port). It should be working,
// according to https://github.com/dotnet/runtime/issues/43890,
// but it doesn't (at least with Synology DSM LDAP), although perhaps
// for a different reason
//connection.SessionOptions.SecureSocketLayer = true;
connection.Bind();
var request = new SearchRequest(targetOU, query, scope, attributesToQuery);
var response = (System.DirectoryServices.Protocols.SearchResponse)connection.SendRequest(request);
var userList = new List<LdapUser>();
foreach(SearchResultEntry result in response.Entries)
{
userList.Add(new LdapUser() {
UserId = result.Attributes["uid"][0].ToString(),
FirstName = result.Attributes["givenName"][0].ToString(),
LastName = result.Attributes["sn"][0].ToString(),
Email = result.Attributes["mail"][0].ToString(),
});
}
connection.Dispose();
return userList;
}
}

View File

@ -1,4 +1,15 @@
<!DOCTYPE html>
@{
bool DoesUserHaveClaimedPhoneNumber()
{
using (var dci = DataContext.Instance)
{
var user = dci.Users.Where(u => u.LdapUserName == User.Identity.Name);
return user.Any();
}
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
@ -26,9 +37,18 @@
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/SuggestionHelpers">Suggestion Helpers</a>
</li>
@if (this.User.Identity.IsAuthenticated && !DoesUserHaveClaimedPhoneNumber())
{
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/UnclaimedPhoneNumbers">Unclaimed Phone Numbers</a>
</li>
}
</ul>
</div>
</div>
<div class="container" style="min-height: auto; width: 400px;">
<partial name="_LoginView" />
</div>
</nav>
</header>
<div class="container">

View File

@ -0,0 +1,36 @@
@using Microsoft.AspNetCore.Authentication
<div class="loginform">
@if (!this.User.Identity.IsAuthenticated)
{
<form method="post" action="Auth/Login">
<div>
<label for="username">Username:</label>
</div>
<div>
<input name="username" type="text" />
</div>
<div>
<label for="password">Password:</label>
</div>
<div>
<input name="password" type="password" />
</div>
<div>
<input name="submit" type="submit" value="Login" />
</div>
</form>
}
else
{
<form method="post" action="Auth/Logout">
<div>
Welcome, @User.Identity.Name!
</div>
<div>
<input name="submit" type="submit" value="Logout" />
</div>
</form>
}
</div>

View File

@ -0,0 +1,38 @@
@page
@model UnclaimedPhoneNumbersModel
@{
ViewData["Title"] = "Unclaimed Phone Numbers";
var codeService = HttpContext.RequestServices.GetService<PhoneClaimCodeProviderService>();
var codeGenerated = codeService.IsCodeGeneratedForUser(User.Identity.Name);
}
<div class="text-left">
<table>
<tr>
<th>Phone Number</th>
<th>Claim</th>
</tr>
@foreach (var user in @Model.UnclaimedUsers)
{
var phone = user.SignalMemberId; var userId = user.UserId;
<tr>
<td>@phone</td>
<td>
<form method="post">
<input name="userIndex" value="@userId" type="hidden" />
<input type="submit" title="Claim" value="Claim" disabled="@codeGenerated" />
</form>
</td>
</tr>
}
</table>
@if(codeGenerated)
{
<form method="post">
<label for="code">Verification code:</label>
<input type="text" id="code" name="code" />
<input type="submit" title="Verify" value="Verify" asp-page-handler="SubmitCode" />
</form>
}
</div>

View File

@ -0,0 +1,64 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.VisualBasic;
namespace sotd.Pages;
public class UnclaimedPhoneNumbersModel : PageModel
{
private readonly ILogger<UserModel> _logger;
public UnclaimedPhoneNumbersModel(ILogger<UserModel> logger)
{
_logger = logger;
}
public int userId { get; set; }
[BindProperty]
public List<User> UnclaimedUsers { get; set; }
public void OnGet()
{
using (var dci = DataContext.Instance)
{
this.UnclaimedUsers = dci.Users.Where(u => string.IsNullOrEmpty(u.LdapUserName)).ToList();
}
}
public void OnPost(int userIndex)
{
using (var dci = DataContext.Instance)
{
var user = dci.Users.Find(userIndex);
var claimCodeService = HttpContext.RequestServices.GetService<PhoneClaimCodeProviderService>();
claimCodeService.GenerateClaimCodeForUserAndNumber(HttpContext.User.Identity.Name, user.SignalMemberId);
this.UnclaimedUsers = dci.Users.Where(u => string.IsNullOrEmpty(u.LdapUserName)).ToList();
}
}
public IActionResult OnPostSubmitCode(string code)
{
var claimCodeService = HttpContext.RequestServices.GetService<PhoneClaimCodeProviderService>();
var validatedNumber = claimCodeService.ValidateClaimCodeForUser(code, HttpContext.User.Identity.Name);
if (!string.IsNullOrEmpty(validatedNumber))
{
using (var dci = DataContext.Instance)
{
var user = dci.Users.Where(u => u.SignalMemberId == validatedNumber).FirstOrDefault();
if (user == default(User))
{
throw new Exception("User with specified phone number not found!");
}
user.LdapUserName = HttpContext.User.Identity.Name;
dci.SaveChanges();
}
}
else
{
throw new Exception("Invalid code provided!");
}
return RedirectToPage("/");
}
}

View File

@ -5,11 +5,17 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using System.DirectoryServices.Protocols;
using System.Runtime.CompilerServices;
SignalIntegration.Instance = new SignalIntegration(AppConfiguration.Instance.SignalAPIEndpointUri,
int.Parse(AppConfiguration.Instance.SignalAPIEndpointPort),
AppConfiguration.Instance.HostPhoneNumber);
LdapIntegration.Instance = new LdapIntegration(AppConfiguration.Instance.LDAPConfig.LDAPserver,
AppConfiguration.Instance.LDAPConfig.Port,
AppConfiguration.Instance.LDAPConfig.Username,
AppConfiguration.Instance.LDAPConfig.Password);
var builder = WebApplication.CreateBuilder(args);
Console.WriteLine("Setting up user check timer");
@ -34,7 +40,9 @@ userCheckTimer.OnOccurence += async (s, ea) =>
Name = newUserContact.Name,
SignalMemberId = memberId,
NickName = string.Empty,
IsIntroduced = false
IsIntroduced = false,
LdapUserName = string.Empty,
AssociationInProgress = false,
};
dci.Users.Add(newUser);
needsSaving = true;
@ -47,7 +55,7 @@ userCheckTimer.OnOccurence += async (s, ea) =>
}
await dci.DisposeAsync();
};
//userCheckTimer.Start();
userCheckTimer.Start();
Console.WriteLine("Setting up user intro timer");
var userIntroTimer = new CronTimer("*/1 * * * *", "Europe/Vienna", includingSeconds: false);
@ -69,13 +77,23 @@ userIntroTimer.OnOccurence += async (s, ea) =>
}
await dci.DisposeAsync();
};
//userIntroTimer.Start();
userIntroTimer.Start();
Console.WriteLine("Setting up pick of the day timer");
var pickOfTheDayTimer = new CronTimer("0 8 * * *", "Europe/Vienna", includingSeconds: false);
pickOfTheDayTimer.OnOccurence += async (s, ea) =>
{
var rand = new Random();
var num = rand.NextInt64();
var mod = num % AppConfiguration.Instance.AverageDaysBetweenRequests;
if (mod > 0)
{
Console.WriteLine("Skipping pick of the day today!");
return;
}
var dci = DataContext.Instance;
var luckyUser = await dci.Users.ElementAtAsync((new Random()).Next(await dci.Users.CountAsync()));
var userName = string.IsNullOrEmpty(luckyUser.NickName) ? luckyUser.Name : luckyUser.NickName;
@ -85,88 +103,57 @@ pickOfTheDayTimer.OnOccurence += async (s, ea) =>
SignalIntegration.Instance.SendMessageToUserAsync($"Today's (optional) suggestion helper to help you pick a song is:\n\n**{suggestion.Title}**\n\n*{suggestion.Description}*", luckyUser.SignalMemberId);
SignalIntegration.Instance.SendMessageToUserAsync($"For now please just share your suggestion with the group - in the future I might ask you to share directly with me or via the website to help me keep track of past suggestions!", luckyUser.SignalMemberId);
};
//pickOfTheDayTimer.Start();
pickOfTheDayTimer.Start();
var connection = new LdapConnection(AppConfiguration.Instance.LDAPConfig.LDAPserver)
var startUserAssociationProcess = (User userToAssociate) =>
{
Credential = new(
AppConfiguration.Instance.LDAPConfig.Username,
AppConfiguration.Instance.LDAPConfig.Password
)
SignalIntegration.Instance.SendMessageToUserAsync($"Hi, I see you are not associated with any website user yet.", userToAssociate.SignalMemberId);
SignalIntegration.Instance.SendMessageToUserAsync($"If you haven't yet, please navigate to https://users.disi.dev to create a new account.", userToAssociate.SignalMemberId);
SignalIntegration.Instance.SendMessageToUserAsync($"Once you have done so, go to https://sotd.disi.dev, login, navigate to \"Unclaimed Phone Numbers\" and click on the \"Claim\" button to start the claim process.", userToAssociate.SignalMemberId);
SignalIntegration.Instance.SendMessageToUserAsync($"With a future update you will be required to submit songs via your user account - at that point you will be skipped during the selection process if you have not yet claimed your phone number!", userToAssociate.SignalMemberId);
};
var attributesToQuery = new string[]
Console.WriteLine("Setting up LdapAssociation timer");
var ldapAssociationTimer = new CronTimer("*/10 * * * *", "Europe/Vienna", includingSeconds: false);
ldapAssociationTimer.OnOccurence += async (s, ea) =>
{
"objectGUID",
"sAMAccountName",
"displayName",
"mail",
"whenCreated"
};
SearchResponse SearchInAD(
string ldapServer,
int ldapPort,
string domainForAD,
string username,
string password,
string targetOU,
string query,
SearchScope scope,
params string[] attributeList
)
var dci = DataContext.Instance;
var nonAssociatedUsers = dci.Users.Where(u => string.IsNullOrEmpty(u.LdapUserName) && !u.AssociationInProgress);
var needsSaving = false;
foreach (var user in nonAssociatedUsers)
{
// on Windows the authentication type is Negotiate, so there is no need to prepend
// AD user login with domain. On other platforms at the moment only
// Basic authentication is supported
var authType = AuthType.Basic;
user.AssociationInProgress = true;
//var connection = new LdapConnection(ldapServer)
var connection = new LdapConnection(
new LdapDirectoryIdentifier(ldapServer, ldapPort)
)
{
AuthType = authType,
Credential = new(username, password)
};
// the default one is v2 (at least in that version), and it is unknown if v3
// is actually needed, but at least Synology LDAP works only with v3,
// and since our Exchange doesn't complain, let it be v3
connection.SessionOptions.ProtocolVersion = 3;
// this is for connecting via LDAPS (636 port). It should be working,
// according to https://github.com/dotnet/runtime/issues/43890,
// but it doesn't (at least with Synology DSM LDAP), although perhaps
// for a different reason
//connection.SessionOptions.SecureSocketLayer = true;
connection.Bind();
var request = new SearchRequest(targetOU, query, scope, attributeList);
return (SearchResponse)connection.SendRequest(request);
startUserAssociationProcess(user);
user.IsIntroduced = true;
needsSaving = true;
}
var searchResults = SearchInAD(
AppConfiguration.Instance.LDAPConfig.LDAPserver,
AppConfiguration.Instance.LDAPConfig.Port,
AppConfiguration.Instance.LDAPConfig.Username,
AppConfiguration.Instance.LDAPConfig.Password,
if (needsSaving)
{
await dci.SaveChangesAsync();
}
await dci.DisposeAsync();
};
ldapAssociationTimer.Start();
var searchResults = LdapIntegration.Instance.SearchInAD(
AppConfiguration.Instance.LDAPConfig.LDAPQueryBase,
new StringBuilder("(&")
.Append("(objectCategory=person)")
.Append("(objectClass=user)")
.Append($"(memberOf={_configurationAD.Crew})")
.Append("(!(userAccountControl:1.2.840.113556.1.4.803:=2))")
.Append(")")
.ToString(),
SearchScope.Subtree,
attributesToQuery
$"(memberOf={AppConfiguration.Instance.LDAPConfig.CrewGroup})",
SearchScope.Subtree
);
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddOpenApi();
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Auth/Login";
});
builder.Services.AddSingleton<LdapAuthenticationService>();
builder.Services.AddSingleton<PhoneClaimCodeProviderService>();
var app = builder.Build();
@ -188,5 +175,15 @@ app.UseAuthorization();
app.MapStaticAssets();
app.MapRazorPages()
.WithStaticAssets();
app.MapControllerRoute(
name: "login",
pattern: "{controller=Auth}/{action=Login}"
);
app.MapControllerRoute(
name: "logout",
pattern: "{controller=Auth}/{action=Logout}"
);
app.MapGet("/debug/routes", (IEnumerable<EndpointDataSource> endpointSources) =>
string.Join("\n", endpointSources.SelectMany(source => source.Endpoints)));
app.Run();