Create models and Clean Arch boilerplate
This commit is contained in:
parent
3926db5446
commit
872dc1e263
21 changed files with 576 additions and 27 deletions
|
@ -1,6 +0,0 @@
|
|||
namespace CSR.Application;
|
||||
|
||||
public class Class1
|
||||
{
|
||||
|
||||
}
|
13
CSR.Application/Interfaces/IRoleRepository.cs
Normal file
13
CSR.Application/Interfaces/IRoleRepository.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
namespace CSR.Application.Interfaces;
|
||||
|
||||
using CSR.Domain.Entities;
|
||||
|
||||
public interface IRoleRepository
|
||||
{
|
||||
Task<Role?> GetByIdAsync(int id);
|
||||
// Task<Role?> GetByNameAsync(string name);
|
||||
// Task<IEnumerable<Role>> GetAllAsync();
|
||||
Task AddAsync(Role role);
|
||||
// Task UpdateAsync(Role role);
|
||||
Task DeleteAsync(int id);
|
||||
}
|
14
CSR.Application/Interfaces/IUserRepository.cs
Normal file
14
CSR.Application/Interfaces/IUserRepository.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
namespace CSR.Application.Interfaces;
|
||||
|
||||
using CSR.Domain.Entities;
|
||||
|
||||
public interface IUserRepository
|
||||
{
|
||||
Task<User?> GetByIdAsync(int id);
|
||||
// Task<User?> GetByEmailAsync(string email);
|
||||
// Task<User?> GetByUsernameAsync(string username);
|
||||
// Task<IEnumerable<User>> GetAllAsync();
|
||||
Task AddAsync(User user);
|
||||
Task UpdateAsync(User user);
|
||||
Task DeleteAsync(int id);
|
||||
}
|
8
CSR.Application/Interfaces/IUserService.cs
Normal file
8
CSR.Application/Interfaces/IUserService.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
namespace CSR.Application.Interfaces;
|
||||
|
||||
public interface IUserService
|
||||
{
|
||||
public record CreateUserResult(Domain.Entities.User? User, string? ErrorMessage);
|
||||
|
||||
public void CreateUser(string username, string email, string password, bool isAdmin = false);
|
||||
}
|
14
CSR.Application/Services/UserService.cs
Normal file
14
CSR.Application/Services/UserService.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
namespace CSR.Application.Services;
|
||||
|
||||
using CSR.Application.Interfaces;
|
||||
using CSR.Domain.Entities;
|
||||
|
||||
public class UserService(IUserRepository userRepository) : IUserService
|
||||
{
|
||||
private readonly IUserRepository _userRepository = userRepository;
|
||||
|
||||
public void CreateNewUser(string username, string email, string password, bool isAdmin = false)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
|
@ -6,4 +6,8 @@
|
|||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="CSR.Infrastructure" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
33
CSR.Domain/Entities/Role.cs
Normal file
33
CSR.Domain/Entities/Role.cs
Normal file
|
@ -0,0 +1,33 @@
|
|||
namespace CSR.Domain.Entities;
|
||||
|
||||
public record Role
|
||||
{
|
||||
public int Id { get; private set; }
|
||||
public string Name { get; private set; }
|
||||
|
||||
private Role(int id, string name)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
internal static Role LoadExisting(int id, string name)
|
||||
{
|
||||
return new Role(id, name);
|
||||
}
|
||||
|
||||
public static Role Admin
|
||||
{
|
||||
get
|
||||
{
|
||||
return new Role(1, "Admin");
|
||||
}
|
||||
}
|
||||
public static Role User
|
||||
{
|
||||
get
|
||||
{
|
||||
return new Role(2, "User");
|
||||
}
|
||||
}
|
||||
}
|
173
CSR.Domain/Entities/User.cs
Normal file
173
CSR.Domain/Entities/User.cs
Normal file
|
@ -0,0 +1,173 @@
|
|||
namespace CSR.Domain.Entities;
|
||||
|
||||
using System.Net.Mail;
|
||||
using CSR.Domain.Interfaces;
|
||||
|
||||
public class User
|
||||
{
|
||||
public int Id { get; private set; }
|
||||
public string Username { get; private set; }
|
||||
public string Email { get; private set; }
|
||||
public string PasswordHash { get; private set; }
|
||||
|
||||
public int RoleId { get; private set; }
|
||||
public Role Role { get; private set; }
|
||||
|
||||
private User(int id, string username, string email, string passwordHash, int roleId, Role role)
|
||||
{
|
||||
Id = id;
|
||||
Username = username;
|
||||
Email = email;
|
||||
PasswordHash = passwordHash;
|
||||
RoleId = roleId;
|
||||
Role = role;
|
||||
}
|
||||
|
||||
// This role is used when a new user is created
|
||||
public static readonly Role DefaultRole = Role.User;
|
||||
|
||||
|
||||
internal static User LoadExisting(int id, string username, string email, string passwordHash, int roleId, Role role)
|
||||
{
|
||||
return new User(id, username, email, passwordHash, roleId, role);
|
||||
}
|
||||
|
||||
|
||||
public (bool Success, IEnumerable<string>? Errors) UpdatePassword(string password, IPasswordHasher passwordHasher, User requestingUser)
|
||||
{
|
||||
if (requestingUser.Id != Id || requestingUser.Role.Name != "Admin")
|
||||
{
|
||||
throw new UnauthorizedAccessException("Only admins or the same user can update passwords.");
|
||||
}
|
||||
|
||||
var validityCheck = IsValidPassword(password);
|
||||
|
||||
if (validityCheck.IsValid)
|
||||
{
|
||||
PasswordHash = passwordHasher.HashPassword(password);
|
||||
}
|
||||
|
||||
return validityCheck;
|
||||
}
|
||||
|
||||
|
||||
public bool VerifyPasswordAgainstHash(string providedPassword, IPasswordHasher passwordHasher, User requestingUser)
|
||||
{
|
||||
if (requestingUser.Id != Id || requestingUser.Role.Name != "Admin")
|
||||
{
|
||||
throw new UnauthorizedAccessException("Only admins or the same user can verify passwords.");
|
||||
}
|
||||
|
||||
return passwordHasher.VerifyHashedPassword(PasswordHash, providedPassword);
|
||||
}
|
||||
|
||||
|
||||
public static (bool IsValid, IEnumerable<string>? Errors) IsValidPassword(string password)
|
||||
{
|
||||
var Errors = new List<string>();
|
||||
if (password.Length < 8 || password.Length > 32)
|
||||
{
|
||||
Errors.Add("Password must be between 8 and 32 characters long.");
|
||||
}
|
||||
if (!password.Any(char.IsUpper))
|
||||
{
|
||||
Errors.Add("Password must contain at least one uppercase letter.");
|
||||
}
|
||||
if (!password.Any(char.IsLower))
|
||||
{
|
||||
Errors.Add("Password must contain at least one lowercase letter.");
|
||||
}
|
||||
if (!password.Any(char.IsDigit))
|
||||
{
|
||||
Errors.Add("Password must contain at least one digit.");
|
||||
}
|
||||
if (Errors.Count > 0)
|
||||
{
|
||||
return (false, Errors);
|
||||
}
|
||||
return (true, null);
|
||||
}
|
||||
|
||||
|
||||
public (bool Success, IEnumerable<string>? Errors) UpdateEmail(string email, User requestingUser)
|
||||
{
|
||||
if (requestingUser.Id != Id || requestingUser.Role.Name != "Admin")
|
||||
{
|
||||
throw new UnauthorizedAccessException("Only admins or the same user can update email.");
|
||||
}
|
||||
|
||||
if (IsValidEmail(email))
|
||||
{
|
||||
Email = email;
|
||||
return (true, null);
|
||||
}
|
||||
return (false, new List<string> { "Invalid email format." });
|
||||
}
|
||||
|
||||
|
||||
public static bool IsValidEmail(string email)
|
||||
{
|
||||
try
|
||||
{
|
||||
var mailAddress = new MailAddress(email);
|
||||
return true;
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public (bool Success, IEnumerable<string>? Errors) UpdateUsername(string username, User requestingUser)
|
||||
{
|
||||
if (requestingUser.Id != Id || requestingUser.Role.Name != "Admin")
|
||||
{
|
||||
throw new UnauthorizedAccessException("Only admins can update username.");
|
||||
}
|
||||
|
||||
var validityCheck = IsValidUsername(username);
|
||||
if (validityCheck.IsValid)
|
||||
{
|
||||
Username = username;
|
||||
}
|
||||
|
||||
return validityCheck;
|
||||
}
|
||||
|
||||
|
||||
public static (bool IsValid, IEnumerable<string>? Errors) IsValidUsername(string username)
|
||||
{
|
||||
var Errors = new List<string>();
|
||||
if (!username.All(char.IsLetterOrDigit))
|
||||
{
|
||||
Errors.Add("Username must be alphanumeric.");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(username) || username.Length < 3)
|
||||
{
|
||||
Errors.Add("Username must be at least 3 characters long.");
|
||||
}
|
||||
if (username.Length > 32)
|
||||
{
|
||||
Errors.Add("Username must be less than 32 characters long.");
|
||||
}
|
||||
|
||||
if (Errors.Count > 0)
|
||||
{
|
||||
return (false, Errors);
|
||||
}
|
||||
|
||||
return (true, null);
|
||||
}
|
||||
|
||||
|
||||
public void UpdateRole(Role role, User requestingUser)
|
||||
{
|
||||
if (requestingUser.Role.Name != "Admin")
|
||||
{
|
||||
throw new UnauthorizedAccessException("Only admins can assign roles.");
|
||||
}
|
||||
Role = role;
|
||||
RoleId = role.Id;
|
||||
}
|
||||
}
|
7
CSR.Domain/Interfaces/IPasswordHasher.cs
Normal file
7
CSR.Domain/Interfaces/IPasswordHasher.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace CSR.Domain.Interfaces;
|
||||
|
||||
public interface IPasswordHasher
|
||||
{
|
||||
string HashPassword(string password);
|
||||
bool VerifyHashedPassword(string hashedPassword, string providedPassword);
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
namespace CSR.Domain;
|
||||
|
||||
public class Role
|
||||
{
|
||||
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
namespace CSR.Domain;
|
||||
|
||||
public class User
|
||||
{
|
||||
|
||||
}
|
|
@ -2,6 +2,14 @@
|
|||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CSR.Application\CSR.Application.csproj" />
|
||||
<ProjectReference Include="..\CSR.Domain\CSR.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
namespace CSR.Infrastructure;
|
||||
|
||||
public class Class1
|
||||
{
|
||||
|
||||
}
|
37
CSR.Infrastructure/Persistence/CSRDbContext.cs
Normal file
37
CSR.Infrastructure/Persistence/CSRDbContext.cs
Normal file
|
@ -0,0 +1,37 @@
|
|||
namespace CSR.Infrastructure.Persistence;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using CSR.Infrastructure.Persistence.Configurations;
|
||||
|
||||
|
||||
public class CSRDbContext(DbContextOptions<CSRDbContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<User> Users { get; set; }
|
||||
public DbSet<Role> Roles { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.ApplyConfiguration(new UserConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new RoleConfiguration());
|
||||
|
||||
modelBuilder.Entity<User>()
|
||||
.HasIndex(u => u.Username)
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder.Entity<User>()
|
||||
.HasOne(u => u.Role)
|
||||
.WithMany(r => r.Users)
|
||||
.HasForeignKey(u => u.RoleId);
|
||||
|
||||
// --- Seed data --- //
|
||||
|
||||
var adminRole = new Role { Id = 1, Name = "Admin" };
|
||||
var userRole = new Role { Id = 2, Name = "User" };
|
||||
|
||||
modelBuilder.Entity<Role>()
|
||||
.HasData(adminRole, userRole);
|
||||
|
||||
|
||||
base.OnModelCreating(modelBuilder);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
namespace CSR.Infrastructure.Persistence.Configurations;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
|
||||
public class RoleConfiguration : IEntityTypeConfiguration<Role>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Role> builder)
|
||||
{
|
||||
builder.Property(u => u.Id)
|
||||
.HasColumnName("RoleId")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(u => u.Name)
|
||||
.HasColumnName("RoleName")
|
||||
.IsRequired();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
namespace CSR.Infrastructure.Persistence.Configurations;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
|
||||
public class UserConfiguration : IEntityTypeConfiguration<User>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<User> builder)
|
||||
{
|
||||
builder.Property(u => u.PasswordHash)
|
||||
.HasColumnName("Password")
|
||||
.IsRequired();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
namespace Csr.Infrastructure.Persistence.Repositories;
|
||||
|
||||
using CSR.Domain.Entities;
|
||||
using CSR.Application.Interfaces;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
public class RoleRepository(CSR.Infrastructure.Persistence.CSRDbContext context) : IRoleRepository
|
||||
{
|
||||
private readonly CSR.Infrastructure.Persistence.CSRDbContext _context = context;
|
||||
|
||||
public async Task<Role?> GetByIdAsync(int id)
|
||||
{
|
||||
var roleEntity = await _context.Roles
|
||||
.Include(r => r.Id)
|
||||
.SingleOrDefaultAsync(r => r.Id == id);
|
||||
|
||||
if (roleEntity == null)
|
||||
{
|
||||
return null; // No entity found, return null domain model
|
||||
}
|
||||
|
||||
var role = Role.LoadExisting(
|
||||
roleEntity.Id,
|
||||
roleEntity.Name
|
||||
);
|
||||
|
||||
return role;
|
||||
}
|
||||
|
||||
|
||||
public async Task AddAsync(Role role)
|
||||
{
|
||||
var roleEntity = new CSR.Infrastructure.Persistence.Role
|
||||
{
|
||||
Id = role.Id,
|
||||
Name = role.Name
|
||||
};
|
||||
|
||||
_context.Roles.Add(roleEntity);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
|
||||
public async Task DeleteAsync(int id)
|
||||
{
|
||||
var roleEntity = new CSR.Infrastructure.Persistence.Role
|
||||
{
|
||||
Id = id,
|
||||
Name = string.Empty
|
||||
};
|
||||
|
||||
_context.Roles.Remove(roleEntity);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
105
CSR.Infrastructure/Persistence/Repositories/UserRepository.cs
Normal file
105
CSR.Infrastructure/Persistence/Repositories/UserRepository.cs
Normal file
|
@ -0,0 +1,105 @@
|
|||
namespace Csr.Infrastructure.Persistence.Repositories;
|
||||
|
||||
using CSR.Domain.Entities;
|
||||
using CSR.Application.Interfaces;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
public class UserRepository(CSR.Infrastructure.Persistence.CSRDbContext context) : IUserRepository
|
||||
{
|
||||
private readonly CSR.Infrastructure.Persistence.CSRDbContext _context = context;
|
||||
|
||||
public async Task<User?> GetByIdAsync(int id)
|
||||
{
|
||||
var userEntity = await _context.Users
|
||||
.Include(u => u.Role)
|
||||
.SingleOrDefaultAsync(u => u.Id == id);
|
||||
|
||||
if (userEntity == null)
|
||||
{
|
||||
return null; // No entity found, return null domain model
|
||||
}
|
||||
|
||||
var user = User.LoadExisting(
|
||||
userEntity.Id,
|
||||
userEntity.Username,
|
||||
userEntity.Email,
|
||||
userEntity.PasswordHash,
|
||||
userEntity.RoleId,
|
||||
Role.LoadExisting(
|
||||
userEntity.Role.Id,
|
||||
userEntity.Role.Name
|
||||
)
|
||||
);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
|
||||
public async Task AddAsync(User user)
|
||||
{
|
||||
var userEntity = new CSR.Infrastructure.Persistence.User
|
||||
{
|
||||
Username = user.Username,
|
||||
Email = user.Email,
|
||||
PasswordHash = user.PasswordHash,
|
||||
RoleId = user.RoleId,
|
||||
Role = new CSR.Infrastructure.Persistence.Role
|
||||
{
|
||||
Id = user.Role.Id,
|
||||
Name = user.Role.Name
|
||||
}
|
||||
};
|
||||
|
||||
_context.Users.Add(userEntity);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
|
||||
public async Task UpdateAsync(User user)
|
||||
{
|
||||
var userEntity = await _context.Users
|
||||
.Include(u => u.Role)
|
||||
.FirstOrDefaultAsync(u => u.Id == user.Id);
|
||||
|
||||
if (userEntity == null)
|
||||
{
|
||||
// NOTE should I throw an exception here?
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
userEntity.Id = user.Id;
|
||||
userEntity.Username = user.Username;
|
||||
userEntity.Email = user.Email;
|
||||
userEntity.PasswordHash = user.PasswordHash;
|
||||
userEntity.RoleId = user.RoleId;
|
||||
userEntity.Role = new CSR.Infrastructure.Persistence.Role { Id = user.Role.Id, Name = user.Role.Name };
|
||||
|
||||
// Prevent EF from trying to update the Role entity
|
||||
_context.Entry(userEntity.Role).State = EntityState.Unchanged;
|
||||
|
||||
_context.Users.Update(userEntity);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
|
||||
public async Task DeleteAsync(int id)
|
||||
{
|
||||
var userEntity = new CSR.Infrastructure.Persistence.User
|
||||
{
|
||||
Id = id,
|
||||
Username = string.Empty,
|
||||
Email = string.Empty,
|
||||
PasswordHash = string.Empty,
|
||||
RoleId = 0,
|
||||
Role = new CSR.Infrastructure.Persistence.Role
|
||||
{
|
||||
Id = 0,
|
||||
Name = string.Empty
|
||||
}
|
||||
};
|
||||
|
||||
_context.Users.Remove(userEntity);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
10
CSR.Infrastructure/Persistence/Role.cs
Normal file
10
CSR.Infrastructure/Persistence/Role.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
namespace CSR.Infrastructure.Persistence;
|
||||
|
||||
public class Role
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public required string Name { get; set; }
|
||||
|
||||
// Navigation property
|
||||
public ICollection<User> Users { get; set; } = new HashSet<User>();
|
||||
}
|
17
CSR.Infrastructure/Persistence/User.cs
Normal file
17
CSR.Infrastructure/Persistence/User.cs
Normal file
|
@ -0,0 +1,17 @@
|
|||
namespace CSR.Infrastructure.Persistence;
|
||||
|
||||
public class User
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public required string Username { get; set; }
|
||||
public required string Email { get; set; }
|
||||
public required string PasswordHash { get; set; }
|
||||
public required int RoleId { get; set; }
|
||||
|
||||
// Navigation property
|
||||
public Role Role { get; set; } = null!;
|
||||
|
||||
// prevent direct instantiation
|
||||
// use UserService to create a new user
|
||||
internal User() { }
|
||||
}
|
|
@ -1,16 +1,57 @@
|
|||
using CSR.Infrastructure.Persistence;
|
||||
using CSR.Application.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Get configuration from appsettings.json, environment variables, and Docker secrets in that order
|
||||
builder.Configuration
|
||||
.AddJsonFile("appsettings.json", optional: true)
|
||||
.AddEnvironmentVariables()
|
||||
.AddKeyPerFile("/run/secrets", optional: true);
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddRazorPages();
|
||||
|
||||
// Set up connection to SQLite database
|
||||
var dbPath = builder.Configuration["Database:Path"];
|
||||
if (string.IsNullOrEmpty(dbPath))
|
||||
{
|
||||
var folder = Environment.SpecialFolder.LocalApplicationData;
|
||||
var path = Environment.GetFolderPath(folder);
|
||||
dbPath = Path.Join(path, "csr.db");
|
||||
}
|
||||
|
||||
builder.Services.AddDbContext<CSRDbContext>(options =>
|
||||
{
|
||||
options.UseSqlite($"Data Source={dbPath}");
|
||||
});
|
||||
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var services = scope.ServiceProvider;
|
||||
var config = services.GetRequiredService<IConfiguration>();
|
||||
|
||||
var context = services.GetRequiredService<CSRDbContext>();
|
||||
var userService = services.GetRequiredService<UserService>();
|
||||
|
||||
var adminUsername = config["Admin:Username"] ?? "admin";
|
||||
var adminEmail = config["Admin:Email"] ?? "";
|
||||
var adminPassword = config["Admin:Password"] ?? "password";
|
||||
|
||||
userService.CreateUser(adminUsername, adminEmail, adminPassword, true);
|
||||
}
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseExceptionHandler("/Error");
|
||||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||
app.UseHsts();
|
||||
app.UseExceptionHandler("/Error");
|
||||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||
app.UseHsts();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue