Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 18 additions & 2 deletions Api/Api.csproj
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Abp.ZeroCore.IdentityServer4" Version="8.4.0" />
<PackageReference Include="Abp.ZeroCore.IdentityServer4.EntityFrameworkCore" Version="8.4.0" />
<PackageReference Include="Duende.IdentityServer.Storage" Version="7.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.14" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.20" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.20" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.20">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.6.1" />
</ItemGroup>

<ItemGroup>
<Folder Include="Database\Migrations\" />
<Folder Include="Migrations\" />
</ItemGroup>

</Project>
41 changes: 41 additions & 0 deletions Api/Config.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using IdentityServer4.Models;

namespace Api
{
public static class Config
{
public static IEnumerable<ApiResource> GetApiResources() =>
new List<ApiResource>
{
new ApiResource("myresourceapi", "My Resource API")
{
Scopes = { "apiscope" }
}
};

public static IEnumerable<ApiScope> GetApiScopes() =>
new List<ApiScope>
{
new ApiScope("apiscope", "API Scope")
};

public static IEnumerable<Client> GetClients() =>
new List<Client>
{
new Client
{
ClientId = "secret_client_id",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedScopes = { "apiscope" }
}
};

public static IEnumerable<IdentityResource> GetIdentityResources() =>
new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile()
};
}
}
96 changes: 96 additions & 0 deletions Api/Controllers/AccountController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using Api.DTOs.Account;
using Api.Entities;
using Api.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Security.Claims;

namespace Api.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AccountController : ControllerBase
{
private readonly JWTService _jwtService;
private readonly SignInManager<User> _signInManager;
private readonly UserManager<User> _userManager;

public AccountController(JWTService jwtService,
SignInManager<User> signInManager,
UserManager<User> userManager)
{
_jwtService = jwtService;
_signInManager = signInManager;
_userManager = userManager;
}

[Authorize]
[HttpGet("refresh-user-token")]
public async Task<ActionResult<UserDto>> RefreshUserToken()
{
var user = await _userManager.FindByNameAsync(User.FindFirst(ClaimTypes.Email)?.Value);
return await CreateApplicationUserDto(user);
}

[HttpPost("login")]
public async Task<ActionResult<UserDto>> Login(LoginDto model)
{
var user = await _userManager.FindByNameAsync(model.UserName);
if (user == null) return Unauthorized("Invalid UserName or Password");

if (user.EmailConfirmed == false) return Unauthorized("Please confirm your email");

var result = await _signInManager.CheckPasswordSignInAsync(user, model.Password, false);

if (!result.Succeeded) return Unauthorized("Invalid Username or Password");

return await CreateApplicationUserDto(user);
}

[HttpPost("register")]
public async Task<IActionResult> Register(RegisterDto model)
{
if(await CheckEmailExistsAsync(model.Email))
{
return BadRequest($"An existing account is using {model.Email} email adress. Please try with another email adress");
}

var userToAdd = new User
{
FirstName = model.FirstName.ToLower(),
LastName = model.LastName.ToLower(),
UserName = model.Email.ToLower(),
Email = model.Email.ToLower(),
EmailConfirmed = true
};

var result = await _userManager.CreateAsync(userToAdd, model.Password);

if (!result.Succeeded) return BadRequest(result.Errors);

await _userManager.AddToRoleAsync(userToAdd, "User");

return Ok("Your account has been created successfully, you can login");
}

#region Private Helper Methods
private async Task<UserDto> CreateApplicationUserDto(User user)
{
return new UserDto
{
FirstName = user.FirstName,
LastName = user.LastName,
JWT = await _jwtService.CreateJWT(user)
};
}

private async Task<bool> CheckEmailExistsAsync(string email)
{
return await _userManager.Users.AnyAsync(x => x.Email == email.ToLower());
}
#endregion
}
}
33 changes: 0 additions & 33 deletions Api/Controllers/WeatherForecastController.cs

This file was deleted.

12 changes: 12 additions & 0 deletions Api/DTOs/Account/LoginDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.ComponentModel.DataAnnotations;

namespace Api.DTOs.Account
{
public class LoginDto
{
[Required]
public string UserName { get; set; }
[Required]
public string Password { get; set; }
}
}
20 changes: 20 additions & 0 deletions Api/DTOs/Account/RegisterDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.ComponentModel.DataAnnotations;

namespace Api.DTOs.Account
{
public class RegisterDto
{
[Required]
[StringLength(15,MinimumLength =3,ErrorMessage = "First name must be at least {2}, and maximum {1} characters")]
public string FirstName { get; set; }
[Required]
[StringLength(15, MinimumLength = 3, ErrorMessage = "Last name must be at least {2}, and maximum {1} characters")]
public string LastName { get; set; }
[Required]
[RegularExpression("^\\w+@[a-zA-Z_]+?\\.[a-zA-Z]{2,3}$", ErrorMessage = "Invalid email adress")]
public string Email { get; set; }
[Required]
[StringLength(15, MinimumLength = 6, ErrorMessage = "Password must be at least {2}, and maximum {1} characters")]
public string Password { get; set; }
}
}
9 changes: 9 additions & 0 deletions Api/DTOs/Account/UserDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Api.DTOs.Account
{
public class UserDto
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string JWT { get; set; }
}
}
21 changes: 21 additions & 0 deletions Api/Database/Context.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Api.Entities;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace Api.Database
{
public class Context : IdentityDbContext<User>
{
public Context(DbContextOptions<Context> options) : base(options)
{

}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.SeedRoles();
modelBuilder.SeedAdmin();
}
}
}
47 changes: 47 additions & 0 deletions Api/Database/DataSeed.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using Api.Entities;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

namespace Api.Database
{
public static class DataSeed
{
public static void SeedRoles(this ModelBuilder modelBuilder)
{
modelBuilder.Entity<IdentityRole>().HasData(new[]
{
new IdentityRole { Id = "1", Name = "Admin", NormalizedName = "ADMIN" },
new IdentityRole { Id = "2", Name = "User", NormalizedName = "USER" }
});
}

public static void SeedAdmin(this ModelBuilder modelBuilder)
{
const string adminId = "1";
const string adminEmail = "vladusmoroz@gmail.com";
var hasher = new PasswordHasher<User>();
var admin = new User
{
Id = adminId,
EmailConfirmed = true,
UserName = adminEmail,
NormalizedUserName = adminEmail.ToUpper(),
FirstName = "Vladyslav",
LastName = "Moroz",
Email = adminEmail,
NormalizedEmail = adminEmail.ToUpper(),
PhoneNumber = "+380955638293",
SecurityStamp = Guid.NewGuid().ToString()
};
admin.PasswordHash = hasher.HashPassword(admin, "qwerty12345");

modelBuilder.Entity<User>().HasData(admin);

modelBuilder.Entity<IdentityUserRole<string>>().HasData(new[]
{
new IdentityUserRole<string> { UserId = adminId, RoleId = "1" },
new IdentityUserRole<string> { UserId = adminId, RoleId = "2" }
});
}
}
}
Loading