//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 },