Skip to content

Commit

Permalink
Merge pull request #95 from dotnetcameroon/develop
Browse files Browse the repository at this point in the history
Develop - Projects section
  • Loading branch information
djoufson authored Jan 3, 2025
2 parents 7c5094c + ec62263 commit 88d9c7c
Show file tree
Hide file tree
Showing 47 changed files with 2,215 additions and 249 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
# secret files
secrets/
hangfire.db*
projects.db*

# dotenv files
.env
Expand Down
11 changes: 11 additions & 0 deletions app.business/Services/IExternalAppService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using app.domain.Models.ExternalAppAggregate;
using app.shared.Utilities;
using ErrorOr;
using Microsoft.AspNetCore.Identity;

namespace app.business.Services;
public interface IExternalAppService
{
Task<ErrorOr<PagedList<Application>>> GetAllAsync(int page = 1, int size = 10, CancellationToken cancellationToken = default);
Task<ErrorOr<(Application application, string secret)>> RegisterAsync(string applicationName, IPasswordHasher<Application> passwordHasher, CancellationToken cancellationToken = default);
}
1 change: 1 addition & 0 deletions app.business/Services/IIdentityService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ namespace app.business.Services;
public interface IIdentityService
{
Task<ErrorOr<User>> LoginAsync(string email, string password);
Task<ErrorOr<string>> LoginAppAsync(Guid applicationId, string applicationSecret);
Task LogoutAsync();
}
8 changes: 8 additions & 0 deletions app.business/Services/IProjectService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using app.domain.Models.ProjectsAggregate;

namespace app.business.Services;
public interface IProjectService
{
Task<Project[]> GetAllAsync();
Task RefreshAsync(IEnumerable<Project> projects);
}
7 changes: 7 additions & 0 deletions app.business/Services/ITokenProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using System.Security.Claims;

namespace app.business.Services;
public interface ITokenProvider
{
string Generate(IEnumerable<Claim> claims);
}
8 changes: 7 additions & 1 deletion app.client/Components/AdminSideBar.razor
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@
<NavLink href="/admin/partners" class="sidebar-link text-white hover:bg-white hover:bg-opacity-15 p-4 rounded-lg transition-all">
<i class="fa-solid fa-handshake-angle"></i>
<span class="hidden md:inline nav-text text-inherit">
Patrners
Partners
</span>
</NavLink>
<NavLink href="/admin/apps" class="sidebar-link text-white hover:bg-white hover:bg-opacity-15 p-4 rounded-lg transition-all">
<i class="fa-solid fa-gears"></i>
<span class="hidden md:inline nav-text text-inherit">
Applications
</span>
</NavLink>
</div>
Expand Down
2 changes: 1 addition & 1 deletion app.client/Pages/Admin/Events/Delete.razor
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@page "/admin/events/delete/{Id:guid}"
@layout AdminLayout
@attribute [Authorize]
@attribute [Authorize(Policy = Policies.AdminOnly)]
@inject NavigationManager NavigationManager
@inject IEventService EventService

Expand Down
63 changes: 34 additions & 29 deletions app.client/Pages/Admin/Events/Events.razor
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@page "/admin"
@attribute [Authorize]
@attribute [Authorize(Policy = Policies.AdminOnly)]
@layout AdminLayout
@inject IEventService EventService
@inject NavigationManager NavigationManager
Expand All @@ -23,7 +23,8 @@
</EditForm> *@
<EditForm class="flex gap-1" OnValidSubmit="@HandleSearch" Model="@SearchBarModel" FormName="SearchForm">
<label for="search-input" class="flex items-center px-4 gap-1 border border-gray-400 rounded-lg">
<InputText class="p-2 w-full" id="search-input" @bind-Value="@SearchBarModel.Text" title="text" required type="text" placeholder="Search"/>
<InputText class="p-2 w-full" id="search-input" @bind-Value="@SearchBarModel.Text" title="text" required
type="text" placeholder="Search" />
</label>
<button class="btn btn-secondary center" type="submit">
<i class="fa-solid fa-magnifying-glass"></i>
Expand All @@ -32,35 +33,37 @@
</div>
</div>

@if(_isLoading)
@if (_isLoading)
{
<p>Loading ...</p>
}
else
{
@if(!string.IsNullOrWhiteSpace(_errorMessage))
@if (!string.IsNullOrWhiteSpace(_errorMessage))
{
<p>@_errorMessage</p>
}
else
{
<table class="w-full">
<thead class="bg-primary">
<th class="text-left p-2 text-white">Date</th>
<th class="text-left p-2 text-white">Event Name</th>
<th class="text-left p-2 text-white">Status</th>
<th class="text-left p-2 text-white">Actions</th>
</thead>
<tbody class="bg-slate-100">
@foreach(var item in _events.Items)
<thead class="bg-primary">
<th class="text-left p-2 text-white">Date</th>
<th class="text-left p-2 text-white">Event Name</th>
<th class="text-left p-2 text-white">Status</th>
<th class="text-left p-2 text-white">Actions</th>
</thead>
<tbody class="bg-slate-100">
@foreach (var item in _events.Items)
{
<tr class="border-b-white border-b-2">
<td class="p-2" >@item.Schedule.ToFriendlyDateString()</td>
<td class="p-2" >@item.Title</td>
<td class="p-2" >@item.Status</td>
<td class="p-2" >
<a data-enhance-nav="false" class="text-secondary" href="/admin/events/@item.Id"><i class="fa-solid fa-pen-to-square"></i></a>
<a data-enhance-nav="false" class="text-secondary ml-4" href="/admin/events/delete/@item.Id"><i class="fa-solid fa-trash-can"></i></a>
<td class="p-2">@item.Schedule.ToFriendlyDateString()</td>
<td class="p-2">@item.Title</td>
<td class="p-2">@item.Status</td>
<td class="p-2">
<a data-enhance-nav="false" class="text-secondary" href="/admin/events/@item.Id"><i
class="fa-solid fa-pen-to-square"></i></a>
<a data-enhance-nav="false" class="text-secondary ml-4" href="/admin/events/delete/@item.Id"><i
class="fa-solid fa-trash-can"></i></a>
</td>
</tr>
}
Expand All @@ -70,23 +73,25 @@ else
}

<div class="flex justify-between items-center mt-4">
<a data-enhance-nav="false" href="/admin/events" class="btn btn-secondary cursor-pointer"><i class="fa-regular fa-square-plus"></i> New event</a>
<a data-enhance-nav="false" href="/admin/events" class="btn btn-secondary cursor-pointer"><i
class="fa-regular fa-square-plus"></i> New event</a>
<div class="flex gap-4 items-center">
<!-- Pagination -->
<p>Page @Page / @_events.TotalPages</p>

@if(_events.HasPreviousPage)
@if (_events.HasPreviousPage)
{
<a data-enhance-nav="false" href="/admin?Page=@(Page - 1)" class="link link-secondary" target="_parent">Previous</a>
<a data-enhance-nav="false" href="/admin?Page=@(Page - 1)" class="link link-secondary"
target="_parent">Previous</a>
}
@if(_events.HasNextPage)
@if (_events.HasNextPage)
{
<a data-enhance-nav="false" href="/admin?Page=@(Page + 1)" class="link link-secondary" target="_parent">Next</a>
}
</div>
</div>

@code{
@code {
private const int DefaultPage = 1;
private const int DefaultPageSize = 5;

Expand Down Expand Up @@ -117,15 +122,15 @@ else
SearchBarModel.Text = Search;
_isLoading = true;
ErrorOr<PagedList<Event>> eventsResult = default;
if(string.IsNullOrWhiteSpace(Search))
if (string.IsNullOrWhiteSpace(Search))
{
eventsResult = await EventService.GetAllAsync(Page, Size);
}
else
{
eventsResult = await EventService.SearchAsync(e => e.Title.Contains(Search), Page, Size);
}
if(eventsResult.IsError)
if (eventsResult.IsError)
{
_errorMessage = string.Join("\n", eventsResult.Errors.Select(e => e.Description));
_isLoading = false;
Expand All @@ -140,10 +145,10 @@ else
{
Console.WriteLine(SearchBarModel.Text);
var path = NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
var newPath = NavigationManager.GetUriWithQueryParameters(path, new Dictionary<string,object?>
{
{ "Search", SearchBarModel.Text }
});
var newPath = NavigationManager.GetUriWithQueryParameters(path, new Dictionary<string, object?>
{
{ "Search", SearchBarModel.Text }
});

NavigationManager.NavigateTo($"/{newPath}", true);
}
Expand Down
2 changes: 1 addition & 1 deletion app.client/Pages/Admin/Events/NewOrEdit.razor
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
@page "/admin/events/{Id:guid}"
@layout AdminLayout
@rendermode InteractiveWebAssembly
@attribute [Authorize]
@attribute [Authorize(Policy = Policies.AdminOnly)]

<PageTitle>Admin | Edit Event</PageTitle>
<NewActivityPopup Parent="@this" />
Expand Down
82 changes: 82 additions & 0 deletions app.client/Pages/Admin/ExternalApp/RegisterApp.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
@page "/admin/apps/register"
@using Microsoft.AspNetCore.Identity
@using app.domain.Models.ExternalAppAggregate
@attribute [Authorize(Policy = Policies.AdminOnly)]
@layout AdminLayout
@rendermode InteractiveServer
@inject NavigationManager NavigationManager
@inject IExternalAppService ExternalAppService
@inject IPasswordHasher<Application> PasswordHasher

<PageTitle>Admin | Register External Application</PageTitle>

<h1>Register External App</h1>

@if (_errors.Length > 0)
{
<div class="text-red-600 bg-red-100 p-4 rounded-md border border-red-300">
@foreach (var error in _errors)
{
<p>* @error</p>
}
</div>
}

<div class="max-w-md">
<div class="mb-4">
<label for="appName" class="block font-bold">Application Name:</label>
<InputText id="appName" class="p-2 w-full px-4 border border-gray-400 rounded-lg" @bind-Value="appModel.Name"
required />
</div>

<button @onclick="HandleSubmit" class="btn btn-secondary">Register Application</button>
@if (!string.IsNullOrEmpty(_clientSecret))
{
<div class="mt-4 flex items-center gap-2">
<p><span class="font-bold">Application ID:</span> @_application.Id</p>
</div>
<div class="mt-2 p-4 bg-yellow-100 border border-yellow-400 rounded-lg">
<p class="text-yellow-800 font-bold mb-2">⚠️ Warning: Save this client secret now. It will not be shown again!
</p>
<div class="flex items-center gap-2">
<input id="secret" type="password" readonly value="@_clientSecret" class="flex-grow p-2 border rounded" />
<button id="secret-button" onclick="copySecretToClipboard()" class="btn btn-primary">
<i class="fa-regular fa-clipboard"></i>
</button>
</div>
</div>
}
</div>

@code {
private AppModel appModel = new();
private string[] _errors = [];
private string _clientSecret = string.Empty;
private Application _application = default!;

private async Task HandleSubmit()
{
_errors = [];
if (string.IsNullOrWhiteSpace(appModel.Name))
{
_errors = ["Application name is required"];
return;
}

var result = await ExternalAppService.RegisterAsync(appModel.Name, PasswordHasher);
if (result.IsError)
{
_errors = result.Errors.Select(e => e.Description).ToArray();
}
else
{
_clientSecret = result.Value.secret;
_application = result.Value.application;
}
}

private class AppModel
{
public string Name { get; set; } = string.Empty;
}
}
104 changes: 104 additions & 0 deletions app.client/Pages/Admin/ExternalApp/RegisteredApps.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
@page "/admin/apps"
@using app.domain.Models.ExternalAppAggregate
@attribute [Authorize(Policy = Policies.AdminOnly)]
@attribute [StreamRendering]
@layout AdminLayout
@inject IExternalAppService ExternalAppService

<PageTitle>Admin | External Applications</PageTitle>

<div class="flex justify-between items-center">
<h1 class="heading heading-1">Applications</h1>
<a href="/admin/apps/register" data-enhance-nav="false" class="btn btn-secondary cursor-pointer">
<i class="fa-regular fa-square-plus"></i>
Register a New Application
</a>
</div>

@if (_isLoading)
{
<p>Loading ...</p>
}
else
{
@if (!string.IsNullOrWhiteSpace(_errorMessage))
{
<p class="text-red-600 bg-red-100 p-4 rounded-md border border-red-300">@_errorMessage</p>
}
else
{
<table class="w-full">
<thead class="bg-primary">
<th class="text-left p-2 text-white">Applications Name</th>
<th class="text-left p-2 text-white">Applications Id</th>
@* <th class="text-left p-2 text-white">Actions</th> *@
</thead>
<tbody class="bg-slate-100">
@foreach (var item in _applications.Items)
{
<tr class="border-b-white border-b-2">
<td class="p-2">@item.ClientName</td>
<td class="p-2">@item.Id</td>
@* <td class="p-2">
<a data-enhance-nav="false" class="text-secondary" href="/admin/events/@item.Id"><i
class="fa-solid fa-pen-to-square"></i></a>
<a data-enhance-nav="false" class="text-secondary ml-4" href="/admin/events/delete/@item.Id"><i
class="fa-solid fa-trash-can"></i></a>
</td> *@
</tr>
}
</tbody>
</table>
}
}

<div class="flex justify-between items-center mt-4">
<div class="flex gap-4 items-center">
<!-- Pagination -->
<p>Page @Page / @_applications.TotalPages</p>

@if (_applications.HasPreviousPage)
{
<a data-enhance-nav="false" href="/admin/apps?Page=@(Page - 1)" class="link link-secondary"
target="_parent">Previous</a>
}
@if (_applications.HasNextPage)
{
<a data-enhance-nav="false" href="/admin/apps?Page=@(Page + 1)" class="link link-secondary"
target="_parent">Next</a>
}
</div>
</div>

@code {
private const int DefaultPage = 1;
private const int DefaultPageSize = 5;

[SupplyParameterFromQuery]
public int Page { get; set; }

[SupplyParameterFromQuery]
public int Size { get; set; }

private string _errorMessage = string.Empty;
private bool _isLoading;
private PagedList<Application> _applications = PagedList.Empty<Application>();

protected override async Task OnInitializedAsync()
{
Page = Page == 0 ? DefaultPage : Page;
Size = Size == 0 ? DefaultPageSize : Size;
_isLoading = true;
ErrorOr<PagedList<Application>> appsResult = default;
appsResult = await ExternalAppService.GetAllAsync(Page, Size);
if (appsResult.IsError)
{
_errorMessage = string.Join("\n", appsResult.Errors.Select(e => e.Description));
_isLoading = false;
return;
}

_applications = appsResult.Value;
_isLoading = false;
}
}
Loading

0 comments on commit 88d9c7c

Please sign in to comment.