initial commit

This commit is contained in:
2026-07-21 14:22:06 +07:00
commit 2d7959f202
572 changed files with 45295 additions and 0 deletions
@@ -0,0 +1,131 @@
@page "/account/tenant-selection"
@layout AuthenticationLayout
@using Indotalent.Shared.Consts
@attribute [Authorize]
@using Microsoft.AspNetCore.Authorization
@using Microsoft.JSInterop
@inject NavigationManager Navigation
@inject ISnackbar Snackbar
@inject IJSRuntime JSRuntime
<PageTitle>Select Tenant - @GlobalConsts.AppInitial</PageTitle>
<div class="d-flex flex-column align-center justify-center mb-6 w-100">
<MudText Typo="Typo.h5" Align="Align.Center" Style="font-weight: 800; width: 100%;">Select Tenant</MudText>
<MudText Typo="Typo.body2" Align="Align.Center" Class="text-muted" Style="width: 100%;">Please select a tenant to continue</MudText>
</div>
<div class="w-100 px-2">
@if (isLoading)
{
<div class="d-flex justify-center my-8">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Large" />
</div>
}
else if (!myTenants.Any())
{
<MudAlert Severity="Severity.Error" Variant="Variant.Outlined" Class="my-4" Style="border-radius: 8px;">
You don't have access to any tenant. Please contact your Administrator.
</MudAlert>
}
else
{
<div style="max-height: 320px; overflow-y: auto; padding-right: 4px;">
<MudList T="TenantDto" Clickable="true" SelectionMode="SelectionMode.SingleSelection" Class="pa-0">
@foreach (var item in myTenants)
{
<MudListItem OnClick="() => HandleSelectTenant(item.TenantId)"
Icon="@Icons.Material.Filled.Business"
IconColor="Color.Primary"
Class="py-3 my-2"
Style="border: 1px solid var(--mud-palette-divider); border-radius: 8px; background-color: var(--mud-palette-background-grey); transition: all 0.2s ease-in-out;">
<ChildContent>
<div class="d-flex flex-column">
<MudText Typo="Typo.subtitle1" Style="font-weight: 700; color: var(--mud-palette-text-primary);">
@item.TenantName
</MudText>
@if (!string.IsNullOrEmpty(item.Description))
{
<MudText Typo="Typo.caption" Style="color: var(--mud-palette-text-secondary);">
@item.Description
</MudText>
}
</div>
</ChildContent>
</MudListItem>
}
</MudList>
</div>
}
</div>
<div class="text-center mt-6">
<MudText Typo="Typo.body2">
Want to sign in with another account?
<MudLink Href="/account/logout" Color="Color.Primary" Style="font-weight: 700;">Sign Out</MudLink>
</MudText>
</div>
@code {
private bool isLoading = true;
private List<TenantDto> myTenants = new();
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
myTenants = await JSRuntime.InvokeAsync<List<TenantDto>>("apiAccountFetchMyTenants");
isLoading = false;
StateHasChanged();
}
}
private async Task HandleSelectTenant(string tenantId)
{
var result = await JSRuntime.InvokeAsync<ApiResult>("apiAccountConfirmTenant", tenantId);
if (result.Success)
{
Snackbar.Add("Tenant workspace loaded successfully.", Severity.Success);
Navigation.NavigateTo("/home", forceLoad: true);
}
else
{
Snackbar.Add("Failed to assign tenant workspace.", Severity.Error);
}
}
public class TenantDto { public string TenantId { get; set; } = ""; public string TenantName { get; set; } = ""; public string? Description { get; set; } }
public class ApiResult { public bool Success { get; set; } }
}
<script>
window.apiAccountFetchMyTenants = async () => {
try {
const response = await fetch('/api/account/my-tenants', {
method: 'GET',
credentials: 'include'
});
if (response.status === 200) {
return await response.json();
}
return [];
} catch (error) {
return [];
}
};
window.apiAccountConfirmTenant = async (tenantId) => {
try {
const response = await fetch('/api/account/confirm-tenant', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tenantId }),
credentials: 'include'
});
return await response.json();
} catch (error) {
return { success: false };
}
};
</script>
@@ -0,0 +1,67 @@
using Indotalent.Data.Entities;
using Indotalent.Infrastructure.Database;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace Indotalent.Features.Account.TenantSelection.Cqrs;
public record GetTenantsQuery(string UserId) : IRequest<List<TenantLookupDto>>;
public class TenantLookupDto
{
public string TenantId { get; set; } = string.Empty;
public string TenantName { get; set; } = string.Empty;
public string? Description { get; set; }
}
public class GetTenantsHandler : IRequestHandler<GetTenantsQuery, List<TenantLookupDto>>
{
private readonly AppDbContext _context;
public GetTenantsHandler(AppDbContext context)
{
_context = context;
}
public async Task<List<TenantLookupDto>> Handle(GetTenantsQuery request, CancellationToken cancellationToken)
{
return await _context.Set<TenantUser>()
.IgnoreQueryFilters()
.Where(tu => tu.UserId == request.UserId && tu.IsActive)
.Select(tu => new TenantLookupDto
{
TenantId = tu.TenantId ?? string.Empty,
TenantName = tu.Tenant != null ? (tu.Tenant.Name ?? "Unknown") : "Unknown",
Description = tu.Tenant != null ? (tu.Tenant.Description ?? "") : ""
})
.ToListAsync(cancellationToken);
}
}
public record ConfirmTenantRequest(string TenantId);
public record ConfirmTenantResponse(bool Success, string Message);
public record ConfirmTenantCommand(string UserId, ConfirmTenantRequest Data) : IRequest<ConfirmTenantResponse>;
public class ConfirmTenantHandler : IRequestHandler<ConfirmTenantCommand, ConfirmTenantResponse>
{
private readonly AppDbContext _context;
public ConfirmTenantHandler(AppDbContext context)
{
_context = context;
}
public async Task<ConfirmTenantResponse> Handle(ConfirmTenantCommand request, CancellationToken cancellationToken)
{
var hasAccess = await _context.Set<TenantUser>()
.IgnoreQueryFilters()
.AnyAsync(tu => tu.UserId == request.UserId && tu.TenantId == request.Data.TenantId && tu.IsActive, cancellationToken);
if (!hasAccess)
{
return new ConfirmTenantResponse(false, "Access denied to the selected organization.");
}
return new ConfirmTenantResponse(true, "Tenant authorized.");
}
}