Preview:
//BACKEND
//Controller
using av_motion_api.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Newtonsoft.Json.Linq;
using NuGet.Configuration;

namespace av_motion_api.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class DeletionSettingsController : ControllerBase
    {
        private readonly IOptionsMonitor<DeletionSettings> _deletionSettings;
        //private readonly IConfiguration _configuration;
        private readonly IConfigurationRoot _configurationRoot;

        public DeletionSettingsController(IOptionsMonitor<DeletionSettings> deletionSettings, IConfiguration configuration)
        {
            _deletionSettings = deletionSettings;
            //_configuration = configuration;
            _configurationRoot = (IConfigurationRoot)configuration;
        }

        [HttpPost]
        [Route("UpdateDeletionTime")]
        public IActionResult UpdateDeletionTime([FromBody] DeletionSettings settings)
        {
            if (settings.DeletionTimeValue < 0)
            {
                return BadRequest(new { message = "Deletion time value must be non-negative" });
            }

            var configurationFile = "appsettings.Development.json";
            var configurationPath = Path.Combine(Directory.GetCurrentDirectory(), configurationFile);

            var json = System.IO.File.ReadAllText(configurationPath);
            dynamic jsonObj = Newtonsoft.Json.JsonConvert.DeserializeObject(json);

            if (jsonObj["DeletionSettings"] == null)
            {
                jsonObj["DeletionSettings"] = new Newtonsoft.Json.Linq.JObject();
            }

            jsonObj["DeletionSettings"]["DeletionTimeValue"] = settings.DeletionTimeValue;
            jsonObj["DeletionSettings"]["DeletionTimeUnit"] = settings.DeletionTimeUnit;

            string output = Newtonsoft.Json.JsonConvert.SerializeObject(jsonObj, Newtonsoft.Json.Formatting.Indented);
            System.IO.File.WriteAllText(configurationPath, output);

            // Reload the configuration
            _configurationRoot.Reload();

            return Ok(new { message = "Deletion time updated successfully" });
        }

        [HttpGet]
        [Route("GetDeletionSettings")]
        public IActionResult GetDeletionSettings()
        {
            var settings = _deletionSettings.CurrentValue;
            if (settings == null)
            {
                return NotFound(new { message = "Deletion settings not found" });
            }
            return Ok(settings);
        }

    }
}

//Model
namespace av_motion_api.Models
{
    public class DeletionSettings
    {
        public int DeletionTimeValue { get; set; }
        public string DeletionTimeUnit { get; set; }
    }
}

//Service
using av_motion_api.Data; 
using av_motion_api.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;

namespace av_motion_api.Services
{
    public class UserDeletionService : IHostedService, IDisposable
    {
        private readonly IServiceProvider _serviceProvider;
        private readonly IOptionsMonitor<DeletionSettings> _settings;
        private Timer _timer;
        private long _remainingIntervals;
        private const long MaxInterval = Int32.MaxValue - 2;

        public UserDeletionService(IServiceProvider serviceProvider, IOptionsMonitor<DeletionSettings> settings)
        {
            _serviceProvider = serviceProvider;
            _settings = settings;
        }

        public Task StartAsync(CancellationToken cancellationToken)
        {
            ScheduleDeletionTask();
            _settings.OnChange(settings => ScheduleDeletionTask());
            return Task.CompletedTask;
        }

        private void ScheduleDeletionTask()
        {
            var interval = GetTimeSpan(_settings.CurrentValue.DeletionTimeValue, _settings.CurrentValue.DeletionTimeUnit);
            _remainingIntervals = (long)Math.Ceiling(interval.TotalMilliseconds / MaxInterval);

            _timer?.Dispose();
            _timer = new Timer(OnTimerElapsed, null, TimeSpan.Zero, TimeSpan.FromMilliseconds(MaxInterval));
        }



        private void OnTimerElapsed(object state)
        {
            if (--_remainingIntervals <= 0)
            {
                DeleteDeactivatedUsers(state);
                ScheduleDeletionTask(); // Reschedule for the next interval
            }
        }

        private void DeleteDeactivatedUsers(object state)
        {
            using (var scope = _serviceProvider.CreateScope())
            {
                var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
                var userManager = scope.ServiceProvider.GetRequiredService<UserManager<User>>();

                var deletionThreshold = DateTime.UtcNow.Subtract(GetTimeSpan(_settings.CurrentValue.DeletionTimeValue, _settings.CurrentValue.DeletionTimeUnit));

                var usersToDelete = context.Users
                    .Where(u => u.User_Status_ID == 2 && u.DeactivatedAt < deletionThreshold)
                    .ToList();

                foreach (var user in usersToDelete)
                {
                    userManager.DeleteAsync(user).Wait();
                }

                context.SaveChanges();
            }
        }

        private TimeSpan GetTimeSpan(int value, string unit)
        {
            return unit.ToLower() switch
            {
                "minutes" => TimeSpan.FromMinutes(value),
                "hours" => TimeSpan.FromHours(value),
                "days" => TimeSpan.FromDays(value),
                "weeks" => TimeSpan.FromDays(value * 7),
                "months" => TimeSpan.FromDays(value * 30), // Approximation
                "years" => TimeSpan.FromDays(value * 365), // Approximation
                _ => TimeSpan.FromMinutes(value),
            };
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            _timer?.Change(Timeout.Infinite, 0);
            return Task.CompletedTask;
        }

        public void Dispose()
        {
            _timer?.Dispose();
        }
    }
}

//Program.cs
using av_motion_api.Data;
using av_motion_api.Factory;
using av_motion_api.Models;
using av_motion_api.Interfaces;
using av_motion_api.Services;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Logging;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Configuration; // Ensure this is here
using Microsoft.Extensions.Hosting;

var builder = WebApplication.CreateBuilder(args);

// Configure the app environment
var configuration = builder.Configuration;

builder.Configuration.SetBasePath(Directory.GetCurrentDirectory())
    //.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: false);
    .AddJsonFile("appsettings.Development.json", optional: true, reloadOnChange: true); // Add reloadOnChange

builder.Host.ConfigureAppConfiguration((hostingContext, config) =>
{
    //config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
    config.AddJsonFile("appsettings.Development.json", optional: true, reloadOnChange: true); // Add reloadOnChange
    config.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true);
});

// Configure logging
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.AddDebug();

// CORS
if (builder.Environment.IsDevelopment())
{
    builder.Services.AddCors(options =>
    {
        options.AddPolicy("AllowAll", policy =>
        {
            policy.AllowAnyOrigin()
                  .AllowAnyHeader()
                  .AllowAnyMethod();
        });
    });
}

// Add services to the container
builder.Services.AddControllers()
                .AddJsonOptions(options =>
                {
                    options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
                    options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
                });

// SQL
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddScoped<IRepository, Repository>();

builder.Services.AddIdentity<User, Role>(options =>
                {
                    options.Password.RequireUppercase = false;
                    options.Password.RequireLowercase = false;
                    options.Password.RequireNonAlphanumeric = false;
                    options.Password.RequireDigit = true;
                    options.User.RequireUniqueEmail = true;
                })
                .AddRoles<Role>()
                .AddEntityFrameworkStores<AppDbContext>()
                .AddDefaultTokenProviders();

builder.Services.AddAuthentication(options =>
                {
                    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
                })
                .AddCookie()
                .AddJwtBearer(options =>
                {
                    options.TokenValidationParameters = new TokenValidationParameters()
                    {
                        ValidateIssuer = true,
                        ValidateAudience = true,
                        ValidateLifetime = true,
                        ValidateIssuerSigningKey = true,
                        ValidIssuer = builder.Configuration["Tokens:Issuer"],
                        ValidAudience = builder.Configuration["Tokens:Audience"],
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Tokens:Key"]))
                    };
                });

// Configure FormOptions for file uploads
builder.Services.Configure<FormOptions>(o =>
{
    o.ValueLengthLimit = int.MaxValue;
    o.MultipartBodyLengthLimit = int.MaxValue;
    o.MemoryBufferThreshold = int.MaxValue;
});

builder.Services.AddScoped<IUserClaimsPrincipalFactory<User>, AppUserClaimsPrincipalFactory>();

builder.Services.Configure<DataProtectionTokenProviderOptions>(options => options.TokenLifespan = TimeSpan.FromHours(3));


// Register the OrderStatusUpdater hosted service
builder.Services.AddHostedService<OrderStatusUpdater>();

// Register the DeletionSettings configuration section
builder.Services.Configure<DeletionSettings>(configuration.GetSection("DeletionSettings"));

// Register the UserDeletionService hosted service
builder.Services.AddHostedService<UserDeletionService>();

// Register ContractLinkingService
//builder.Services.AddHostedService<ContractLinkingService>();


// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

// Use CORS
app.UseCors("AllowAll");

app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Use(async (context, next) =>
{
    var logger = app.Services.GetRequiredService<ILogger<Program>>();
    logger.LogInformation("Handling request: " + context.Request.Path);
    await next.Invoke();
    logger.LogInformation("Finished handling request.");
});

app.Run();

//appsettings.Development.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "DefaultConnection": "Server=DESKTOP-9QI4G7U\\SQLEXPRESS;Database=AV_Motion;Trusted_Connection=True;TrustServerCertificate=True"
  },
  "Tokens": {
    "Key": "hLKjZkF5q2wX3rQ$@1hy+VRv[&)0XhxJ<sk=yUpW{yE5CH@xh",
    "Issuer": "https://localhost:7185",
    "Audience": "http://localhost:4200"
  },
  "SendGrid": {
    "ApiKey": "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",
    "SenderEmail": "datalungeteam43@gmail.com",
    "SenderName": "AVSFitnessAdmin@AVMotion.com",
    "ContractsDirectory": "C:\\Contracts\\AVS_Fitness"
  },
  "DeletionSettings": {
    "DeletionTimeValue": 6,
    "DeletionTimeUnit": "Months"
  }
}

//FRONTEND
//Deletionsettings component
//html
<div class="view-deletion-settings-container">
    <button class="btn btn" (click)="goBack()">            
        <i class="bi bi-arrow-left-circle header-icon"></i>
    </button>
    <h2>Deletion Settings</h2>
    <p><strong>Deletion Time:</strong> {{deletionSettings?.deletionTimeValue}} {{deletionSettings?.deletionTimeUnit}}</p>
    <button mat-raised-button color="primary" (click)="openEditModal()">Update Settings</button>
</div>

<!-- Edit Modal -->
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
    <div class="modal-dialog">
      <div class="modal-content">
        <div class="modal-header">
          <h5 class="modal-title" id="editModalLabel">Update Deletion Time</h5>
        </div>
        <div class="modal-body">
          <form [formGroup]="deletionSettingsForm">
            <div class="form-group">
              <label for="deletionTime">Deletion Time:</label>
              <input type="number" id="deletionTime" formControlName="value" class="form-control" name="deletionTime">
            </div>
            <div class="form-group">
              <label for="timeUnit">Time Unit:</label>
              <select id="timeUnit" formControlName="unit" class="form-control" name="timeUnit">
                <option value="Minutes">Minutes</option>
                <option value="Hours">Hours</option>
                <option value="Days">Days</option>
                <option value="Weeks">Weeks</option>
                <option value="Months">Months</option>
                <option value="Years">Years</option>
              </select>
            </div>
          </form>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
          <button type="button" class="btn btn-primary" (click)="saveDeletionTime()">Update</button>
        </div>
      </div>
    </div>
</div>

//ts
import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { UserService } from '../Services/userprofile.service';
import { DeletionSettings } from '../shared/deletionsettings';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Router, RouterLink } from '@angular/router';
import { Location } from '@angular/common';
declare var $: any; 

@Component({
  selector: 'app-deletion-settings',
  standalone: true,
  imports: [CommonModule, ReactiveFormsModule, RouterLink, FormsModule],
  templateUrl: './deletion-settings.component.html',
  styleUrl: './deletion-settings.component.css'
})
export class DeletionSettingsComponent implements OnInit {
  deletionSettings: DeletionSettings | undefined;
  deletionSettingsForm: FormGroup;

  constructor(
    private userService: UserService,
    private fb: FormBuilder,
    private snackBar: MatSnackBar,
    private router: Router,
    private location: Location
  ) {
    this.deletionSettingsForm  = this.fb.group({
      value: ['', [Validators.required, Validators.min(0)]],
      unit: ['', Validators.required]
    });
  }

  ngOnInit(): void {
    this.loadDeletionSettings();
  }

  loadDeletionSettings(): void {
    this.userService.getDeletionSettings().subscribe({
      next: (settings) => {
        this.deletionSettings = settings;
        this.deletionSettingsForm.patchValue({
          value: settings.deletionTimeValue,
          unit: settings.deletionTimeUnit
        });
      },
      error: (error) => {
        console.error('Error fetching deletion settings', error);
      }
    });
  }

  openEditModal(): void {
    $('#editModal').modal('show');
  }

  saveDeletionTime(): void {
    if (this.deletionSettingsForm.valid) {
      const settings: DeletionSettings = {
        deletionTimeValue: this.deletionSettingsForm.value.value,
        deletionTimeUnit: this.deletionSettingsForm.value.unit
      };

      this.userService.updateDeletionTime(settings).subscribe({
        next: (response) => {
          console.log("Deletion time", response )
          this.snackBar.open('Deletion time updated successfully', 'Close', { duration: 5000 });
          this.loadDeletionSettings();
          $('#editModal').modal('hide');
        },
        error: (error) => {
          console.error('Error updating deletion time', error);
          this.snackBar.open('Failed to update deletion time. Please try again', 'Close', { duration: 5000 });
          this.loadDeletionSettings();
          $('#editModal').modal('hide');
        }
      });
    }
  }

  goBack(): void {
    this.location.back();
  }
}

//Service
export class UserService {

  httpOptions = {
    headers: new HttpHeaders({
      'Content-Type': 'application/json'
    })
  };

  constructor(private http: HttpClient) {}
  endPoint: string = "https://localhost:7185/api/";
getDeletionSettings(): Observable<DeletionSettings> {
    return this.http.get<DeletionSettings>(`${this.endPoint}DeletionSettings/GetDeletionSettings`);
  }
  
  updateDeletionTime(settings: DeletionSettings): Observable<any> {
    return this.http.post<any>(`${this.endPoint}DeletionSettings/UpdateDeletionTime`, settings, this.httpOptions);
  }
//model
export interface DeletionSettings {
    deletionTimeValue: number;
    deletionTimeUnit: string;
  }
//routes
  { path:'deletion-settings', component:DeletionSettingsComponent },
downloadDownload PNG downloadDownload JPEG downloadDownload SVG

Tip: You can change the style, width & colours of the snippet with the inspect tool before clicking Download!

Click to optimize width for Twitter