Пользователь + авторизация и аутентификация + токен

This commit is contained in:
2025-05-28 19:41:07 +04:00
parent 4320469e88
commit eeaeca8918
18 changed files with 480 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
using GradeBookServer.Application.DTOs.User;
using GradeBookServer.Domain.Entities;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
namespace GradeBookServer.Application.Common.Authentication
{
public class JwtTokenGenerator
{
private readonly IConfiguration _config;
public JwtTokenGenerator(IConfiguration config)
{
_config = config;
}
public string GenerateToken(User user)
{
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, user.ID.ToString()),
new Claim(ClaimTypes.Name, user.Name),
new Claim(ClaimTypes.Role, user.Role.ToString())
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]!));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _config["Jwt:Issuer"],
audience: _config["Jwt:Audience"],
claims: claims,
expires: DateTime.Now.AddHours(2),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
}

View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GradeBookServer.Application.Common
{
public class TempLoginSession
{
public int UserId { get; set; }
public string Code { get; set; } = string.Empty;
public DateTime ExpiresAt { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GradeBookServer.Application.DTOs.User
{
public class TokenDto
{
public required string Token { get; set; }
public UserReadDto? User { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GradeBookServer.Application.DTOs.User
{
public class UserLoginDto
{
public required string Login { get; set; }
public required string Password { get; set; }
}
}

View File

@@ -0,0 +1,18 @@
using GradeBookServer.Domain.Enums;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GradeBookServer.Application.DTOs.User
{
public class UserReadDto
{
public int ID { get; set; }
public required string Name { get; set; }
public required string Email { get; set; }
public string? PhoneNumber { get; set; }
public UserRole Role { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GradeBookServer.Application.DTOs.User
{
public class VerifyCodeDto
{
public string SessionId { get; set; } = string.Empty;
public string Code { get; set; } = string.Empty;
}
}

View File

@@ -6,6 +6,10 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.16" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GradeBookServer.Domain\GradeBookServer.Domain.csproj" />
</ItemGroup>

View File

@@ -0,0 +1,16 @@
using GradeBookServer.Domain.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GradeBookServer.Application.Interfaces
{
public interface IUserRepository
{
Task<User?> GetByLoginAsync(string login);
Task<User?> GetByIdAsync(int id);
Task<IEnumerable<User>> GetTeachersAsync();
}
}

View File

@@ -0,0 +1,103 @@
using GradeBookServer.Application.Common.Authentication;
using GradeBookServer.Application.DTOs.User;
using GradeBookServer.Application.Interfaces;
using GradeBookServer.Domain.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GradeBookServer.Application.Services
{
public class UserService
{
private readonly IUserRepository _repository;
private readonly JwtTokenGenerator _tokenGenerator;
public UserService(IUserRepository repository, JwtTokenGenerator tokenGenerator)
{
_repository = repository;
_tokenGenerator = tokenGenerator;
}
public async Task<UserReadDto?> AuthenticateAsync(UserLoginDto loginDto)
{
var user = await _repository.GetByLoginAsync(loginDto.Login);
if (user == null || user.Password != loginDto.Password)
return null;
return new UserReadDto
{
ID = user.ID,
Name = user.Name,
Email = user.Email,
PhoneNumber = user.PhoneNumber,
Role = user.Role
};
}
public async Task<UserReadDto?> GetCurrentUserAsync(int userId)
{
var user = await _repository.GetByIdAsync(userId);
if (user == null)
return null;
return new UserReadDto
{
ID = user.ID,
Name = user.Name,
Email = user.Email,
PhoneNumber = user.PhoneNumber,
Role = user.Role
};
}
public async Task<List<UserReadDto>> GetTeachersAsync()
{
var teachers = await _repository.GetTeachersAsync();
return teachers.Select(u => new UserReadDto
{
ID = u.ID,
Name = u.Name,
Email = u.Email,
PhoneNumber = u.PhoneNumber,
Role = u.Role
}).ToList();
}
public async Task<UserReadDto?> ValidateCredentialsAsync(string login, string password)
{
var user = await _repository.GetByLoginAsync(login);
if (user == null || user.Password != password)
return null;
return new UserReadDto
{
ID = user.ID,
Name = user.Name,
Email = user.Email,
PhoneNumber = user.PhoneNumber,
Role = user.Role
};
}
public async Task<TokenDto?> GenerateToken(int userID)
{
var user = await _repository.GetByIdAsync(userID);
if (user == null)
return null;
var token = _tokenGenerator.GenerateToken(user);
return (new TokenDto
{
Token = token,
User = new UserReadDto { ID = user.ID, Name = user.Name, Email = user.Email, PhoneNumber = user.PhoneNumber, Role = user.Role }
});
}
}
}

View File

@@ -0,0 +1,41 @@
using GradeBookServer.Application.Interfaces;
using GradeBookServer.Domain.Entities;
using GradeBookServer.Domain.Enums;
using GradeBookServer.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GradeBookServer.Infrastructure.Repositories
{
public class UserRepository : IUserRepository
{
private readonly ApplicationDbContext _context;
public UserRepository(ApplicationDbContext context)
{
_context = context;
}
public async Task<User?> GetByLoginAsync(string login)
{
return await _context.Users
.FirstOrDefaultAsync(u => u.Email == login || u.PhoneNumber == login);
}
public async Task<User?> GetByIdAsync(int id)
{
return await _context.Users.FindAsync(id);
}
public async Task<IEnumerable<User>> GetTeachersAsync()
{
return await _context.Users
.Where(u => u.Role == UserRole.Professor)
.ToListAsync();
}
}
}

View File

@@ -1,5 +1,6 @@
using GradeBookServer.Application.DTOs.Direction;
using GradeBookServer.Application.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace GradeBookServer.WebAPI.Controllers
@@ -16,6 +17,7 @@ namespace GradeBookServer.WebAPI.Controllers
}
[HttpGet]
[Authorize(Roles = "Employee")]
public async Task<ActionResult<IEnumerable<DirectionTableDto>>> GetAll()
{
var directions = await _directionService.GetAllDirectionsAsync();
@@ -23,6 +25,7 @@ namespace GradeBookServer.WebAPI.Controllers
}
[HttpGet("{id}")]
[Authorize(Roles = "Employee")]
public async Task<ActionResult<DirectionDetailDto>> GetById(int id)
{
var direction = await _directionService.GetDirectionByIdAsync(id);
@@ -30,6 +33,7 @@ namespace GradeBookServer.WebAPI.Controllers
}
[HttpPost]
[Authorize(Roles = "Employee")]
public async Task<IActionResult> Create(DirectionCreateDto dto)
{
await _directionService.AddDirectionAsync(dto);
@@ -37,6 +41,7 @@ namespace GradeBookServer.WebAPI.Controllers
}
[HttpPut("{id}")]
[Authorize(Roles = "Employee")]
public async Task<IActionResult> Update(int id, DirectionCreateDto dto)
{
await _directionService.UpdateDirectionAsync(id, dto);
@@ -44,6 +49,7 @@ namespace GradeBookServer.WebAPI.Controllers
}
[HttpDelete("{id}")]
[Authorize(Roles = "Employee")]
public async Task<IActionResult> Delete(int id)
{
await _directionService.DeleteDirectionAsync(id);

View File

@@ -1,5 +1,6 @@
using GradeBookServer.Application.DTOs.Discipline;
using GradeBookServer.Application.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace GradeBookServer.WebAPI.Controllers
@@ -16,6 +17,7 @@ namespace GradeBookServer.WebAPI.Controllers
}
[HttpGet]
[Authorize(Roles = "Employee")]
public async Task<ActionResult<IEnumerable<DisciplineTableDto>>> GetAllDisciplines()
{
var disciplines = await _disciplineService.GetAllDisciplinesAsync();
@@ -23,6 +25,7 @@ namespace GradeBookServer.WebAPI.Controllers
}
[HttpGet("{id}")]
[Authorize(Roles = "Employee")]
public async Task<ActionResult<DisciplineDetailDto>> GetDisciplineById(int id)
{
var discipline = await _disciplineService.GetDisciplineByIdAsync(id);
@@ -33,6 +36,7 @@ namespace GradeBookServer.WebAPI.Controllers
}
[HttpPost]
[Authorize(Roles = "Employee")]
public async Task<ActionResult> AddDiscipline([FromBody] DisciplineCreateDto disciplineDto)
{
await _disciplineService.AddDisciplineAsync(disciplineDto);
@@ -40,6 +44,7 @@ namespace GradeBookServer.WebAPI.Controllers
}
[HttpPut("{id}")]
[Authorize(Roles = "Employee")]
public async Task<ActionResult> UpdateDiscipline(int id, [FromBody] DisciplineCreateDto disciplineDto)
{
await _disciplineService.UpdateDisciplineAsync(id, disciplineDto);
@@ -47,6 +52,7 @@ namespace GradeBookServer.WebAPI.Controllers
}
[HttpDelete("{id}")]
[Authorize(Roles = "Employee")]
public async Task<ActionResult> DeleteDiscipline(int id)
{
await _disciplineService.DeleteDisciplineAsync(id);

View File

@@ -1,5 +1,6 @@
using GradeBookServer.Application.DTOs.Faculty;
using GradeBookServer.Application.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace GradeBookServer.WebAPI.Controllers
@@ -16,6 +17,7 @@ namespace GradeBookServer.WebAPI.Controllers
}
[HttpGet]
[Authorize(Roles = "Employee")]
public async Task<ActionResult<IEnumerable<FacultyReadDto>>> GetAllFaculties()
{
var faculties = await _facultyService.GetAllFacultiesAsync();
@@ -23,6 +25,7 @@ namespace GradeBookServer.WebAPI.Controllers
}
[HttpGet("{id}")]
[Authorize(Roles = "Employee")]
public async Task<ActionResult<FacultyReadDto>> GetFacultyById(int id)
{
var faculty = await _facultyService.GetFacultyByIdAsync(id);
@@ -34,6 +37,7 @@ namespace GradeBookServer.WebAPI.Controllers
}
[HttpPost]
[Authorize(Roles = "Employee")]
public async Task<ActionResult> AddFaculty([FromBody] FacultyCreateDto facultyDto)
{
await _facultyService.AddFacultyAsync(facultyDto);
@@ -41,6 +45,7 @@ namespace GradeBookServer.WebAPI.Controllers
}
[HttpPut("{id}")]
[Authorize(Roles = "Employee")]
public async Task<ActionResult> UpdateFaculty(int id, [FromBody] FacultyCreateDto facultyDto)
{
await _facultyService.UpdateFacultyAsync(id, facultyDto);
@@ -48,6 +53,7 @@ namespace GradeBookServer.WebAPI.Controllers
}
[HttpDelete("{id}")]
[Authorize(Roles = "Employee")]
public async Task<ActionResult> DeleteFaculty(int id)
{
await _facultyService.DeleteFacultyAsync(id);

View File

@@ -1,5 +1,6 @@
using GradeBookServer.Application.DTOs.Group;
using GradeBookServer.Application.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace GradeBookServer.WebAPI.Controllers
@@ -16,6 +17,7 @@ namespace GradeBookServer.WebAPI.Controllers
}
[HttpGet]
[Authorize(Roles = "Employee")]
public async Task<ActionResult<IEnumerable<GroupTableDto>>> GetAll()
{
var groups = await _groupService.GetAllGroupsAsync();
@@ -23,6 +25,7 @@ namespace GradeBookServer.WebAPI.Controllers
}
[HttpGet("{id}")]
[Authorize(Roles = "Employee")]
public async Task<ActionResult<GroupDetailDto>> GetById(int id)
{
var group = await _groupService.GetGroupByIdAsync(id);
@@ -30,6 +33,7 @@ namespace GradeBookServer.WebAPI.Controllers
}
[HttpPost]
[Authorize(Roles = "Employee")]
public async Task<IActionResult> Create(GroupCreateDto dto)
{
await _groupService.AddGroupAsync(dto);
@@ -37,6 +41,7 @@ namespace GradeBookServer.WebAPI.Controllers
}
[HttpPut("{id}")]
[Authorize(Roles = "Employee")]
public async Task<IActionResult> Update(int id, GroupCreateDto dto)
{
await _groupService.UpdateGroupAsync(id, dto);
@@ -44,6 +49,7 @@ namespace GradeBookServer.WebAPI.Controllers
}
[HttpDelete("{id}")]
[Authorize(Roles = "Employee")]
public async Task<IActionResult> Delete(int id)
{
await _groupService.DeleteGroupAsync(id);

View File

@@ -2,6 +2,7 @@
using GradeBookServer.Application.DTOs.Student;
using GradeBookServer.Application.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Threading.Tasks;
@@ -20,6 +21,7 @@ namespace WebAPI.Controllers
}
[HttpGet]
[Authorize(Roles = "Employee")]
public async Task<ActionResult<IEnumerable<StudentReadDto>>> GetAllStudents()
{
var students = await _studentService.GetAllStudentsAsync();
@@ -27,6 +29,7 @@ namespace WebAPI.Controllers
}
[HttpGet("{id}")]
[Authorize(Roles = "Employee")]
public async Task<ActionResult<StudentReadDto>> GetStudentById(int id)
{
var student = await _studentService.GetStudentByIdAsync(id);
@@ -38,6 +41,7 @@ namespace WebAPI.Controllers
}
[HttpPost]
[Authorize(Roles = "Employee")]
public async Task<ActionResult> AddStudent([FromBody] StudentCreateDto studentDto)
{
await _studentService.AddStudentAsync(studentDto);
@@ -45,6 +49,7 @@ namespace WebAPI.Controllers
}
[HttpPut("{id}")]
[Authorize(Roles = "Employee")]
public async Task<ActionResult> UpdateStudent(int id, [FromBody] StudentCreateDto studentDto)
{
await _studentService.UpdateStudentAsync(id, studentDto);
@@ -52,6 +57,7 @@ namespace WebAPI.Controllers
}
[HttpDelete("{id}")]
[Authorize(Roles = "Employee")]
public async Task<ActionResult> DeleteStudent(int id)
{
await _studentService.DeleteStudentAsync(id);

View File

@@ -0,0 +1,98 @@
using GradeBookServer.Application.Common;
using GradeBookServer.Application.DTOs.User;
using GradeBookServer.Application.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.Security.Claims;
namespace GradeBookServer.WebAPI.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private static readonly Dictionary<string, TempLoginSession> _sessions = new();
private readonly UserService _service;
public UsersController(UserService userService)
{
_service = userService;
}
[HttpPost("login")]
public async Task<IActionResult> Login(UserLoginDto dto)
{
var user = await _service.AuthenticateAsync(dto);
if (user == null)
return Unauthorized("Invalid login or password");
var code = new Random().Next(100000, 999999).ToString();
var sessionId = Guid.NewGuid().ToString();
_sessions[sessionId] = new TempLoginSession
{
UserId = user.ID,
Code = code,
ExpiresAt = DateTime.UtcNow.AddMinutes(5)
};
// Пока нет отправки кода, просто выводим в консоль
Console.WriteLine($"[DEBUG] Verification code for user {user.Name}: {code}");
return Ok(new VerifyCodeDto
{
SessionId = sessionId,
Code = string.IsNullOrEmpty(user.PhoneNumber) ? "email" : "sms"
});
}
[HttpPost("verify-code")]
public async Task<IActionResult> Verify([FromBody] VerifyCodeDto dto)
{
if (!_sessions.TryGetValue(dto.SessionId, out var session))
return Unauthorized("Invalid session");
if (session.ExpiresAt < DateTime.UtcNow)
{
_sessions.Remove(dto.SessionId);
return Unauthorized("Code expired");
}
if (session.Code != dto.Code)
return Unauthorized("Invalid code");
var token = await _service.GenerateToken(session.UserId);
_sessions.Remove(dto.SessionId);
return Ok(token);
}
[Authorize]
[HttpPost("cancel-session")]
public IActionResult CancelSession([FromBody] string sessionId)
{
_sessions.Remove(sessionId);
return Ok("Session cancelled");
}
[Authorize]
[HttpGet("me")]
public async Task<IActionResult> GetCurrentUser()
{
var userId = int.Parse(User.FindFirst(ClaimTypes.NameIdentifier)!.Value);
var user = await _service.GetCurrentUserAsync(userId);
return user == null ? NotFound() : Ok(user);
}
[HttpGet("teachers")]
[Authorize(Roles = "Employee")]
public async Task<IActionResult> GetTeachers()
{
var teachers = await _service.GetTeachersAsync();
return Ok(teachers);
}
}
}

View File

@@ -1,9 +1,13 @@
using GradeBookServer.Application.Common.Authentication;
using GradeBookServer.Application.Interfaces;
using GradeBookServer.Application.Services;
using GradeBookServer.Infrastructure.Data;
using GradeBookServer.Infrastructure.Repositories;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
@@ -24,23 +28,82 @@ builder.Services.AddSwaggerGen(options =>
Title = "YumAsia API",
Description = "API <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>",
});
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Name = "Authorization",
Type = SecuritySchemeType.Http,
Scheme = "Bearer",
BearerFormat = "JWT",
In = ParameterLocation.Header,
Description = "<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: Bearer {token}"
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
new string[] {}
}
});
});
var configuration = builder.Configuration;
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = configuration["Jwt:Issuer"],
ValidateAudience = true,
ValidAudience = configuration["Jwt:Audience"],
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Key"]!)),
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
});
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
builder.Services.AddScoped<DbContext, ApplicationDbContext>();
builder.Services.AddAuthorization();
builder.Services.AddScoped(typeof(IBaseRepository<>), typeof(BaseRepository<>));
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<DirectionService>();
builder.Services.AddScoped<DisciplineService>();
builder.Services.AddScoped<FacultyService>();
builder.Services.AddScoped<GroupService>();
builder.Services.AddScoped<StudentService>();
builder.Services.AddScoped<UserService>();
builder.Services.AddScoped<JwtTokenGenerator>();
builder.Services.AddControllers();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{

View File

@@ -8,5 +8,10 @@
"Microsoft.AspNetCore": "Warning"
}
},
"Jwt": {
"Key": "viPzKiSOMouxZNxIIHoRqvN23jBt3Uzo!",
"Issuer": "YourAppIssuer",
"Audience": "YourAppAudience"
},
"AllowedHosts": "*"
}