Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b16bcd4
feature/AB#34582-TightenApi
JamesPasta Oct 14, 2026
70c0de2
feature/AB#34582-TightenApi-Copilot
JamesPasta Oct 14, 2026
7594868
feature/AB#34582-TightenApi-Co-Pilot
JamesPasta Oct 14, 2026
89b9033
feature/AB#34582-TightenApi-Co-Pilot
JamesPasta Oct 14, 2026
72d4b7d
feature/AB#34582-TightenApi-Co-Pilot
JamesPasta Oct 14, 2026
546f66e
feature/AB#34582-TightenApi-Queue-Retry
JamesPasta Oct 14, 2026
72845ab
feature/AB#34582-TightenApi-Co-Pilot
JamesPasta Oct 14, 2026
b908f56
feature/AB#34582-TightenApi-Co-Pilot
JamesPasta Oct 14, 2026
b3c3255
feature/AB#34582-TightenApi-Co-Pilot
JamesPasta Oct 14, 2026
a89a24e
feature/AB#34582-TightenApi-Co-Pilot
JamesPasta Oct 14, 2026
c8bf58c
feature/AB#34582-TightenApi-Co-Pilot
JamesPasta Oct 14, 2026
2121dd1
feature/AB#34582-TightenApi-Co-Pilot
JamesPasta Oct 14, 2026
423db8e
feature/AB#34582-TightenApi-Co-Pilot
JamesPasta Oct 15, 2026
a453415
Potential fix for pull request finding 'Concurrent retry publishing s…
JamesPasta Oct 15, 2026
4ce698b
feature/AB#34582-TightenApi-Co-Pilot
JamesPasta Oct 15, 2026
f61749f
Merge branch 'dev' into feature/AB#34582-TightenAPISecurity
JamesPasta Oct 15, 2026
735fd36
feature/AB#34582-TightenApi-Co-Pilot
JamesPasta Oct 15, 2026
40733d3
Merge branch 'feature/AB#34582-TightenAPISecurity' of https://github.…
JamesPasta Oct 15, 2026
4e7e978
feature/AB#34582-TightenApi-Co-Pilot-UnitTestFix
JamesPasta Oct 16, 2026
55d8c8a
Merge branch 'dev' into feature/AB#34582-TightenAPISecurity
JamesPasta Oct 16, 2026
4c60b16
feature/AB#34582-TightenApi-Co-Pilot-UnitTestFix
JamesPasta Oct 16, 2026
c50c667
Merge branch 'feature/AB#34582-TightenAPISecurity' of https://github.…
JamesPasta Oct 16, 2026
9ee59e8
feature/AB#34582-TightenApi-Co-Pilot-UnitTestFix
JamesPasta Oct 16, 2026
d1b5cb4
feature/AB#34582-TightenApi-Co-Pilot
JamesPasta Oct 16, 2026
a843c3d
feature/AB#34582-TightenApi-Co-Pilot
JamesPasta Oct 16, 2026
5f3cf7c
feature/AB#34582-TightenApi-Co-Pilot
JamesPasta Oct 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion applications/Unity.GrantManager/common.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<Target Name="NoWarnOnRazorViewImportedTypeConflicts" BeforeTargets="RazorCoreCompile">
<PropertyGroup>
<NoWarn>$(NoWarn);0436</NoWarn>
<NoWarn>$(NoWarn);NU1510</NoWarn>
</PropertyGroup>
</Target>

Expand All @@ -24,7 +25,7 @@
<!-- System.Security.Cryptography.Xml 8.0.2 (pulled in transitively) has several known high
severity vulnerabilities; pin to the same patched version already used directly by
Unity.AI.Application so every project importing common.props gets the safe version. -->
<PackageReference Include="System.Security.Cryptography.Xml" Version="10.0.10" />
<PackageReference Include="System.Security.Cryptography.Xml" Version="10.0.13" />
</ItemGroup>

</Project>
7 changes: 7 additions & 0 deletions applications/Unity.GrantManager/global.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"sdk": {
"version": "10.0.303",
"rollForward": "latestPatch",
"allowPrerelease": false
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\common.props" />
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
Expand All @@ -10,7 +10,7 @@
<PackageReference Include="Azure.AI.OpenAI" Version="2.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="NPOI" Version="2.7.5" />
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.13" />
<PackageReference Include="SixLabors.ImageSharp" Version="4.1.2" />
<PackageReference Include="PdfPig" Version="0.1.15" />
<PackageReference Include="Volo.Abp.AspNetCore.Mvc" Version="10.5.0" />
<PackageReference Include="Volo.Abp.Mapperly" Version="10.5.0" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ internal async Task<WriteDataRowResponse> UpdateRowAsync(RowInputData rowInputDa
};
}

internal async Task DeleteRowAsync(Guid valueId, uint row, Guid worksheetInstanceId)
public async Task DeleteRowAsync(Guid valueId, uint row, Guid worksheetInstanceId)
{
var currentValue = await customFieldValueAppService.GetAsync(valueId);
var dataGridValue = DataGridServiceUtils.DeserializeDataGridValue(currentValue.CurrentValue);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public class QuestionSelectListViewModel : RequiredFieldViewModel
public Guid QuestionId { get; set; }
public bool IsDisabled { get; set; }
public string Answer { get; set; } = string.Empty;
public string Definition { get; set; } = "{}";
public new string Definition { get; set; } = "{}";
public bool IsHumanConfirmed { get; set; } = true;
public string? AICitation { get; set; }
public int? AIConfidence { get; set; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,38 +1,37 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using Volo.Abp.Application.Services;
using Volo.Abp.DependencyInjection;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Uow;
using Unity.Notifications.Permissions;

namespace Unity.Notifications.Templates;


[Dependency(ReplaceServices = false)]
[ExposeServices(typeof(TemplateService), typeof(ITemplateService))]
public class TemplateService : ApplicationService, ITemplateService
public class TemplateService(
ITemplatesRepository templatesRepository,
ICurrentTenant currentTenant,
ITemplateVariablesRepository templateVariablesRepository,
IUnitOfWorkManager unitOfWorkManager,
Volo.Abp.Domain.Repositories.IRepository<GrantManager.Notifications.ScheduledNotification, Guid> scheduledNotificationsRepository

) : ApplicationService, ITemplateService
{

private readonly ITemplatesRepository _templatesRepository;
private readonly ICurrentTenant _currentTenant;
private readonly ITemplateVariablesRepository _templateVariablesRepository;
private readonly ITemplatesRepository _templatesRepository = templatesRepository;
private readonly ICurrentTenant _currentTenant = currentTenant;
private readonly ITemplateVariablesRepository _templateVariablesRepository = templateVariablesRepository;
private readonly IUnitOfWorkManager _unitOfWorkManager = unitOfWorkManager;
private readonly Volo.Abp.Domain.Repositories.IRepository<GrantManager.Notifications.ScheduledNotification, Guid> _scheduledNotificationsRepository = scheduledNotificationsRepository;


public TemplateService(
ITemplatesRepository templatesRepository,
ICurrentTenant currentTenant,
ITemplateVariablesRepository templateVariablesRepository

)
{
_templatesRepository = templatesRepository;
_currentTenant = currentTenant;
_templateVariablesRepository = templateVariablesRepository;
}

[Authorize]
[Authorize(NotificationsPermissions.Email.Default)]
public async Task<EmailTemplate?> CreateAsync(EmailTempateDto templateDto)
{
var templateType = NormalizeTemplateType(templateDto.TemplateType);
Expand All @@ -51,16 +50,16 @@ ITemplateVariablesRepository templateVariablesRepository
templateType));
}

[Authorize]
[Authorize(NotificationsPermissions.Email.Send)]
public async Task<EmailTemplate?> UpdateTemplate(Guid id, EmailTempateDto templateDto)
{

EmailTemplate template = await _templatesRepository.GetAsync(id);

template.Description = templateDto.Description;
template.Subject = templateDto.Subject;
template.BodyText = templateDto.BodyText;
template.BodyHTML = templateDto.BodyHTML != null ? templateDto.BodyHTML : "";
template.BodyHTML = templateDto.BodyHTML ?? "";
template.Name = templateDto.Name;
template.SendFrom = templateDto.SendFrom;
template.RecipientCategory = templateDto.RecipientCategory;
Expand All @@ -72,31 +71,46 @@ ITemplateVariablesRepository templateVariablesRepository
return updatedTemplate;
}

[Authorize]
[Authorize(NotificationsPermissions.Email.Default)]
public async Task<List<EmailTemplate>> GetTemplatesByTenant()
{
var tenentId = _currentTenant.Id;
return await _templatesRepository.GetByTenentIdAsync(tenentId);
}
[Authorize]

// Shared lookup endpoint: accessible to anyone with email/notification access (lookup, schedule, send)
[Authorize(NotificationsPermissions.Email.Default)]
public async Task<EmailTemplate?> GetTemplateById(Guid id)
{
return await _templatesRepository.GetAsync(id);
}

[Authorize]
[Authorize(NotificationsPermissions.Email.Send)]
public async Task DeleteTemplate(Guid id)
{
await _templatesRepository.DeleteAsync(id);
using var uow = _unitOfWorkManager.Begin(requiresNew: true, isTransactional: true);
var template = await _templatesRepository.LockActiveAsync(id)
?? throw new Volo.Abp.Domain.Entities.EntityNotFoundException(typeof(EmailTemplate), id);

var scheduledQuery = await _scheduledNotificationsRepository.GetQueryableAsync();
var inUse = await scheduledQuery.AnyAsync(x => x.EmailTemplateId == id);
if (inUse)
{
throw new Volo.Abp.BusinessException("Notifications:TemplateInUse", "Template is referenced by scheduled notifications and cannot be deleted.");
}

await _templatesRepository.DeleteAsync(template);
await uow.CompleteAsync();
}
[Authorize]
[Authorize(NotificationsPermissions.Email.Send)]
public async Task<EmailTemplate?> GetTemplateByName(string name)
{
var data = await _templatesRepository.GetByNameAsync(name);
return data;
}

[Authorize]
}

// Shared lookup endpoint: accessible to anyone with email/notification access
[Authorize(NotificationsPermissions.Email.Default)]
public Task<List<string>> GetTemplateTypes()
{
return Task.FromResult(new List<string>
Expand All @@ -106,15 +120,15 @@ public Task<List<string>> GetTemplateTypes()
});
}

[Authorize]
// Shared lookup endpoint: accessible to anyone with email/notification access (forms, templates, scheduling)
[Authorize(NotificationsPermissions.Email.Default)]
public async Task<List<TemplateVariable>> GetTemplateVariables(string? templateType = null)
{
var selectedType = NormalizeTemplateType(templateType);
var templateVariables = await _templateVariablesRepository.GetQueryableAsync();
return templateVariables
return [.. templateVariables
.Where(x => x.TemplateType == selectedType)
.OrderBy(x => x.Name)
.ToList();
.OrderBy(x => x.Name)];
}

private static string NormalizeTemplateType(string? templateType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Unity.Notifications.Templates
public interface ITemplatesRepository : IBasicRepository<EmailTemplate, Guid>
{
Task<EmailTemplate?> GetByIdAsync(Guid id, bool includeDetails = false);
Task<EmailTemplate?> LockActiveAsync(Guid id);
Task<List<EmailTemplate>> GetByTenentIdAsync(Guid? tenentId);
Task<EmailTemplate?> GetByNameAsync(string name);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ public static void ConfigureNotifications(
b.ConfigureByConvention();
b.Property(x => x.TemplateType)
.HasMaxLength(64)
.HasDefaultValue(TemplateTypes.Applicant)
.HasDefaultValue(TemplateTypes.Application)
.IsRequired();
});
modelBuilder.Entity<EmailGroup>(b =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,40 @@

namespace Unity.Notifications.Repositories
{
public class TemplatesRepository : EfCoreRepository<NotificationsDbContext,EmailTemplate, Guid>, ITemplatesRepository
public class TemplatesRepository(IDbContextProvider<NotificationsDbContext> dbContextProvider) : EfCoreRepository<NotificationsDbContext,EmailTemplate, Guid>(dbContextProvider), ITemplatesRepository
{
public TemplatesRepository(IDbContextProvider<NotificationsDbContext> dbContextProvider) : base(dbContextProvider)
{
}

public async Task<EmailTemplate?> GetByIdAsync(Guid id, bool includeDetails = false)
{
var dbSet = await GetDbSetAsync();
return await dbSet.FirstOrDefaultAsync(s => s.Id == id);
}

public async Task<EmailTemplate?> LockActiveAsync(Guid id)
{
var dbContext = await GetDbContextAsync();
if (dbContext.Database.ProviderName == "Npgsql.EntityFrameworkCore.PostgreSQL")
{
if (dbContext.Database.CurrentTransaction == null)
{
throw new InvalidOperationException("Template row locking requires a database transaction.");
}

return await dbContext.EmailTemplates
.FromSqlInterpolated($"""SELECT * FROM "Notifications"."EmailTemplates" WHERE "Id" = {id} FOR UPDATE""")
.SingleOrDefaultAsync();
}

if (dbContext.Database.ProviderName == "Microsoft.EntityFrameworkCore.Sqlite")
{
// SQLite has no SELECT FOR UPDATE; a write serializes access inside a transaction.
await dbContext.Database.ExecuteSqlInterpolatedAsync(
$"""UPDATE "EmailTemplates" SET "IsDeleted" = "IsDeleted" WHERE "Id" = {id}""");
return await dbContext.EmailTemplates.SingleOrDefaultAsync(x => x.Id == id);
}

throw new NotSupportedException("Template row locking requires PostgreSQL or SQLite.");
}

public async Task<List<EmailTemplate>> GetByTenentIdAsync(Guid? tenentId)
{
var dbSet = await GetDbSetAsync();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ $(function () {
setDateRangeFilters(selectedRange, range);
setDateRangeLocalStorage(selectedRange, range);
updateDateInputsVisibility();
notificationsTable.ajax.reload(null, true);
dataTable.ajax.reload(null, true);
}

function handleInputFilterChange() {
Expand All @@ -261,8 +261,7 @@ $(function () {
localStorage.setItem(STORAGE_KEYS.quickRange, 'custom');
localStorage.setItem(STORAGE_KEYS.fromDate, notificationTableFilters.dateFrom || '');
localStorage.setItem(STORAGE_KEYS.toDate, notificationTableFilters.dateTo || '');

notificationsTable.ajax.reload(null, true);
dataTable.ajax.reload(null, true);
}

function setDateRangeFilters(quickDateRange, range) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -844,10 +844,8 @@ $(function () {
}

function handleDeleteCanCheckError(templateId) {
// If check fails, proceed with deletion
proceedWithDelete(templateId, function () {
PubSub.publish('reload_templates_table_with_close');
});
// If check fails, do not proceed with deletion. Let the user know the preflight check failed.
abp.notify.error('Unable to verify whether this template can be safely deleted. Deletion has been cancelled.');
}

function checkCanDeleteTemplate(templateId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,35 @@ public void BuildTokenValues_ForApplicationTemplate_PreservesApplicationValues()
values["unity_application_id"].ShouldBe("UNITY-456");
}

[Fact]
public void BuildTokenValues_ForDeclinedApplication_MapsDisplayDeclineRationale()
{
var application = new Application
{
Applicant = new Applicant { ApplicantName = "VelangTest2", OrgName = "DEC DESIGN" },
ApplicationStatus = new ApplicationStatus { InternalStatus = "Declined" },
ApplicationForm = new ApplicationForm { Category = "Community" },
ReferenceNo = "SUB-123",
DeclineRational = "INELIGIBLE_PROJECT",
ProjectName = "Test project",
RequestedAmount = 1000m,
UnityApplicationId = "UNITY-123"
};
var applicantAgent = new ApplicantAgent { Name = "Test Contact", Title = "Director" };

var values = ScheduledNotificationHelper.BuildTokenValues(application, applicantAgent);

values["applicant_name"].ShouldBe("VelangTest2");
values["organization_name"].ShouldBe("DEC DESIGN");
values["decline_rationale"].ShouldBe("Ineligible Project");
values["status"].ShouldBe("Declined");
values["contact_full_name"].ShouldBe("Test Contact");
values["contact_title"].ShouldBe("Director");
values["category"].ShouldBe("Community");
values["submission_number"].ShouldBe("SUB-123");
values["unity_application_id"].ShouldBe("UNITY-123");
}

[Fact]
public void BuildTokenValues_ForApplicantTemplate_RendersMissingOptionalValuesAsEmpty()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NSubstitute;
Expand Down Expand Up @@ -52,6 +52,8 @@ private static TemplateService CreateService(IEnumerable<TemplateVariable> varia
return new TemplateService(
Substitute.For<ITemplatesRepository>(),
Substitute.For<ICurrentTenant>(),
variablesRepository);
variablesRepository,
Substitute.For<Volo.Abp.Uow.IUnitOfWorkManager>(),
Substitute.For<Volo.Abp.Domain.Repositories.IRepository<Unity.GrantManager.Notifications.ScheduledNotification, System.Guid>>());
}
}
Loading
Loading