Skip to content

Commit 0e03edf

Browse files
author
CIS Guru
committed
security-enhancements
1 parent c690314 commit 0e03edf

10 files changed

Lines changed: 1557 additions & 112 deletions

File tree

2-Aquiis.Application/Services/BaseService.cs

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,10 +375,171 @@ private void SetOrganizationId(TEntity entity, Guid organizationId)
375375
/// </summary>
376376
protected virtual async Task<TEntity> SetCreateDefaultsAsync(TEntity entity)
377377
{
378+
// Automatically propagate IsSampleData flag from parent entities
379+
await InheritSampleDataFlagFromParentsAsync(entity);
380+
378381
await Task.CompletedTask;
379382
return entity;
380383
}
381384

385+
/// <summary>
386+
/// Checks parent entities for IsSampleData flag and propagates to child entity.
387+
/// If any parent entity has IsSampleData = true, the child entity is marked as sample data.
388+
/// This ensures sample data "taints" all related records for proper cleanup.
389+
/// </summary>
390+
/// <param name="entity">The entity being created</param>
391+
/// <returns>Task</returns>
392+
protected virtual async Task InheritSampleDataFlagFromParentsAsync(TEntity entity)
393+
{
394+
try
395+
{
396+
// If already marked as sample data, no need to check parents
397+
if (entity.IsSampleData)
398+
{
399+
return;
400+
}
401+
402+
// Check for common parent relationship properties
403+
var entityType = typeof(TEntity);
404+
var properties = entityType.GetProperties();
405+
406+
// Common parent ID properties to check
407+
var parentIdProperties = new[]
408+
{
409+
"PropertyId",
410+
"LeaseId",
411+
"InvoiceId",
412+
"TenantId",
413+
"ProspectiveTenantId",
414+
"RentalApplicationId",
415+
"RepairId",
416+
"InspectionId",
417+
"MaintenanceRequestId",
418+
"OrganizationId" // Check organization itself for multi-tenant sample orgs
419+
};
420+
421+
foreach (var parentPropName in parentIdProperties)
422+
{
423+
var parentIdProperty = properties.FirstOrDefault(p =>
424+
p.Name == parentPropName &&
425+
(p.PropertyType == typeof(Guid) || p.PropertyType == typeof(Guid?)));
426+
427+
if (parentIdProperty == null) continue;
428+
429+
var parentId = parentIdProperty.GetValue(entity);
430+
if (parentId == null || (parentId is Guid guidValue && guidValue == Guid.Empty)) continue;
431+
432+
// Determine parent entity type and check IsSampleData flag
433+
bool parentIsSampleData = await CheckParentEntityIsSampleDataAsync(parentPropName, (Guid)parentId);
434+
435+
if (parentIsSampleData)
436+
{
437+
entity.IsSampleData = true;
438+
_logger.LogInformation(
439+
$"{typeof(TEntity).Name} marked as sample data - inherited from {parentPropName} ({parentId})");
440+
return; // Once marked as sample data, no need to check other parents
441+
}
442+
}
443+
}
444+
catch (Exception ex)
445+
{
446+
// Log but don't fail entity creation if sample data check fails
447+
_logger.LogWarning(ex, $"Error checking parent sample data flag for {typeof(TEntity).Name}");
448+
}
449+
}
450+
451+
/// <summary>
452+
/// Checks if a parent entity has IsSampleData = true.
453+
/// Simple direct query approach for better reliability and maintainability.
454+
/// </summary>
455+
/// <param name="parentPropertyName">Parent property name (e.g., "LeaseId", "PropertyId")</param>
456+
/// <param name="parentId">Parent entity ID</param>
457+
/// <returns>True if parent is sample data, false otherwise</returns>
458+
private async Task<bool> CheckParentEntityIsSampleDataAsync(string parentPropertyName, Guid parentId)
459+
{
460+
try
461+
{
462+
// Direct queries for each entity type - simple and reliable
463+
switch (parentPropertyName)
464+
{
465+
case "PropertyId":
466+
var property = await _context.Properties
467+
.Where(p => p.Id == parentId)
468+
.Select(p => p.IsSampleData)
469+
.FirstOrDefaultAsync();
470+
return property;
471+
472+
case "LeaseId":
473+
var lease = await _context.Leases
474+
.Where(l => l.Id == parentId)
475+
.Select(l => l.IsSampleData)
476+
.FirstOrDefaultAsync();
477+
return lease;
478+
479+
case "InvoiceId":
480+
var invoice = await _context.Invoices
481+
.Where(i => i.Id == parentId)
482+
.Select(i => i.IsSampleData)
483+
.FirstOrDefaultAsync();
484+
return invoice;
485+
486+
case "TenantId":
487+
var tenant = await _context.Tenants
488+
.Where(t => t.Id == parentId)
489+
.Select(t => t.IsSampleData)
490+
.FirstOrDefaultAsync();
491+
return tenant;
492+
493+
case "ProspectiveTenantId":
494+
var prospect = await _context.ProspectiveTenants
495+
.Where(pt => pt.Id == parentId)
496+
.Select(pt => pt.IsSampleData)
497+
.FirstOrDefaultAsync();
498+
return prospect;
499+
500+
case "RentalApplicationId":
501+
var application = await _context.RentalApplications
502+
.Where(ra => ra.Id == parentId)
503+
.Select(ra => ra.IsSampleData)
504+
.FirstOrDefaultAsync();
505+
return application;
506+
507+
case "RepairId":
508+
var repair = await _context.Repairs
509+
.Where(r => r.Id == parentId)
510+
.Select(r => r.IsSampleData)
511+
.FirstOrDefaultAsync();
512+
return repair;
513+
514+
case "InspectionId":
515+
var inspection = await _context.Inspections
516+
.Where(i => i.Id == parentId)
517+
.Select(i => i.IsSampleData)
518+
.FirstOrDefaultAsync();
519+
return inspection;
520+
521+
case "MaintenanceRequestId":
522+
var maintenanceRequest = await _context.MaintenanceRequests
523+
.Where(mr => mr.Id == parentId)
524+
.Select(mr => mr.IsSampleData)
525+
.FirstOrDefaultAsync();
526+
return maintenanceRequest;
527+
528+
// OrganizationId is NOT checked - Organizations don't have IsSampleData flag
529+
// Sample data is marked at the entity level within an organization
530+
531+
default:
532+
_logger.LogDebug($"Unknown parent property: {parentPropertyName}");
533+
return false;
534+
}
535+
}
536+
catch (Exception ex)
537+
{
538+
_logger.LogDebug(ex, $"Could not check IsSampleData for {parentPropertyName}");
539+
return false;
540+
}
541+
}
542+
382543
/// <summary>
383544
/// Hook method called after creating entity for post-creation operations.
384545
/// Override in derived services to handle side effects like updating related entities.

2-Aquiis.Application/Services/CalendarEventService.cs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,12 @@ private CalendarEvent CreateEventFromEntity<T>(T entity)
239239
where T : BaseModel, ISchedulableEntity
240240
{
241241
var eventType = entity.GetEventType();
242+
var propertyId = entity.GetPropertyId();
243+
244+
// Get property address for Location field
245+
var property = propertyId.HasValue
246+
? _context.Properties.FirstOrDefault(p => p.Id == propertyId.Value)
247+
: null;
242248

243249
return new CalendarEvent
244250
{
@@ -249,32 +255,43 @@ private CalendarEvent CreateEventFromEntity<T>(T entity)
249255
EventType = eventType,
250256
Status = entity.GetEventStatus(),
251257
Description = entity.GetEventDescription(),
252-
PropertyId = entity.GetPropertyId(),
258+
PropertyId = propertyId,
259+
Location = property?.Address ?? string.Empty, // Set location to property address
253260
Color = CalendarEventTypes.GetColor(eventType),
254261
Icon = CalendarEventTypes.GetIcon(eventType),
255262
SourceEntityId = entity.Id,
256263
SourceEntityType = typeof(T).Name,
257264
OrganizationId = entity.OrganizationId,
258265
CreatedBy = entity.CreatedBy,
259-
CreatedOn = DateTime.UtcNow
266+
CreatedOn = DateTime.UtcNow,
267+
IsSampleData = entity.IsSampleData // Inherit sample data flag from entity
260268
};
261269
}
262270

263271
/// <summary>
264272
/// Update a CalendarEvent from a schedulable entity
265273
/// </summary>
266274
private void UpdateEventFromEntity<T>(CalendarEvent evt, T entity)
267-
where T : ISchedulableEntity
275+
where T : BaseModel, ISchedulableEntity
268276
{
277+
var propertyId = entity.GetPropertyId();
278+
279+
// Get property address for Location field
280+
var property = propertyId.HasValue
281+
? _context.Properties.FirstOrDefault(p => p.Id == propertyId.Value)
282+
: null;
283+
269284
evt.Title = entity.GetEventTitle();
270285
evt.StartOn = entity.GetEventStart();
271286
evt.DurationMinutes = entity.GetEventDuration();
272287
evt.EventType = entity.GetEventType();
273288
evt.Status = entity.GetEventStatus();
274289
evt.Description = entity.GetEventDescription();
275-
evt.PropertyId = entity.GetPropertyId();
290+
evt.PropertyId = propertyId;
291+
evt.Location = property?.Address ?? string.Empty; // Update location to property address
276292
evt.Color = CalendarEventTypes.GetColor(entity.GetEventType());
277293
evt.Icon = CalendarEventTypes.GetIcon(entity.GetEventType());
294+
evt.IsSampleData = entity.IsSampleData; // Inherit sample data flag from entity
278295
}
279296
}
280297
}

2-Aquiis.Application/Services/DatabaseUnlockService.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,41 @@ public DatabaseUnlockService(
6969
return (false, $"Error unlocking database: {ex.Message}");
7070
}
7171
}
72+
73+
/// <summary>
74+
/// Archive encrypted database and create fresh database when password forgotten
75+
/// </summary>
76+
/// <param name="databasePath">Path to encrypted database</param>
77+
/// <returns>(Success, ArchivedPath, ErrorMessage)</returns>
78+
public async Task<(bool Success, string? ArchivedPath, string? ErrorMessage)> StartWithNewDatabaseAsync(
79+
string databasePath)
80+
{
81+
try
82+
{
83+
_logger.LogWarning("User requested new database - archiving encrypted database");
84+
85+
// Create backups directory if it doesn't exist
86+
var dbDirectory = Path.GetDirectoryName(databasePath)!;
87+
var backupsDir = Path.Combine(dbDirectory, "Backups");
88+
Directory.CreateDirectory(backupsDir);
89+
90+
// Generate archived filename with timestamp and .db extension for easy identification
91+
var dbFileNameWithoutExt = Path.GetFileNameWithoutExtension(databasePath);
92+
var timestamp = DateTime.Now.ToString("yyyyMMdd-HHmmss");
93+
var archivedPath = Path.Combine(backupsDir, $"{dbFileNameWithoutExt}.{timestamp}.encrypted.db");
94+
95+
// Move encrypted database to backups
96+
File.Move(databasePath, archivedPath);
97+
_logger.LogInformation("Encrypted database archived to: {ArchivedPath}", archivedPath);
98+
99+
// New unencrypted database will be created automatically on app restart
100+
// The app will detect no database exists and go through first-time setup
101+
return (true, archivedPath, null);
102+
}
103+
catch (Exception ex)
104+
{
105+
_logger.LogError(ex, "Error archiving encrypted database");
106+
return (false, null, $"Error archiving database: {ex.Message}");
107+
}
108+
}
72109
}

2-Aquiis.Application/Services/InspectionService.cs

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -152,33 +152,26 @@ public async Task<List<Inspection>> GetByPropertyIdAsync(Guid propertyId)
152152
/// </summary>
153153
public override async Task<Inspection> CreateAsync(Inspection inspection)
154154
{
155-
// Base validation and creation
156-
await ValidateEntityAsync(inspection);
157-
158-
var userId = await GetUserIdAsync();
159-
var organizationId = await GetActiveOrganizationIdAsync();
160-
161-
inspection.Id = Guid.NewGuid();
162-
inspection.OrganizationId = organizationId;
163-
inspection.CreatedBy = userId;
164-
inspection.CreatedOn = DateTime.UtcNow;
165-
166-
await _context.Inspections.AddAsync(inspection);
167-
await _context.SaveChangesAsync();
168-
169-
// Create calendar event for the inspection
170-
await _calendarEventService.CreateOrUpdateEventAsync(inspection);
171-
172-
// Update property inspection tracking if this is a routine inspection
173-
if (inspection.InspectionType == ApplicationConstants.InspectionTypes.Routine)
155+
// Call base.CreateAsync to handle:
156+
// - Sample data propagation from Property
157+
// - Organization context setup
158+
// - Audit tracking fields
159+
// - Validation
160+
var createdInspection = await base.CreateAsync(inspection);
161+
162+
// Custom logic: Create calendar event for the inspection
163+
await _calendarEventService.CreateOrUpdateEventAsync(createdInspection);
164+
165+
// Custom logic: Update property inspection tracking if this is a routine inspection
166+
if (createdInspection.InspectionType == ApplicationConstants.InspectionTypes.Routine)
174167
{
175-
await HandleRoutineInspectionCompletionAsync(inspection);
168+
await HandleRoutineInspectionCompletionAsync(createdInspection);
176169
}
177170

178171
_logger.LogInformation("Created inspection {InspectionId} for property {PropertyId}",
179-
inspection.Id, inspection.PropertyId);
172+
createdInspection.Id, createdInspection.PropertyId);
180173

181-
return inspection;
174+
return createdInspection;
182175
}
183176

184177
/// <summary>

0 commit comments

Comments
 (0)