Dev #11
@@ -115,15 +115,15 @@ public class ApiWebApplicationFactory : WebApplicationFactory<Program>
|
|||||||
var stub = Substitute.For<IAuthService>();
|
var stub = Substitute.For<IAuthService>();
|
||||||
var authResult = new AuthResult(
|
var authResult = new AuthResult(
|
||||||
new AuthResponse("access_token", DateTime.UtcNow.AddHours(1),
|
new AuthResponse("access_token", DateTime.UtcNow.AddHours(1),
|
||||||
new UserAuthDto(1, "test@test.com", "Test User", UserRole.Student)),
|
new UserAuthDto(1, "test@test.com", "Test User", [UserRole.Student])),
|
||||||
"refresh_token");
|
"refresh_token");
|
||||||
stub.LoginWithMicrosoftAsync(Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<string?>())
|
stub.LoginWithMicrosoftAsync(Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<string?>())
|
||||||
.Returns(authResult);
|
.Returns(authResult);
|
||||||
stub.DevLoginAsync(Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<UserRole>(), Arg.Any<string?>())
|
stub.DevLoginAsync(Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<IReadOnlyCollection<UserRole>>(), Arg.Any<string?>())
|
||||||
.Returns(authResult);
|
.Returns(authResult);
|
||||||
stub.RefreshTokenAsync(Arg.Any<string>()).Returns(authResult);
|
stub.RefreshTokenAsync(Arg.Any<string>()).Returns(authResult);
|
||||||
stub.GetCurrentUserAsync(Arg.Any<int>())
|
stub.GetCurrentUserAsync(Arg.Any<int>())
|
||||||
.Returns(new UserDto(1, "test@test.com", "Test", null, UserRole.Student, true, 0, 0, 1, DateTime.UtcNow));
|
.Returns(new UserDto(1, "test@test.com", "Test", null, [UserRole.Student], true, 0, 0, 1, DateTime.UtcNow));
|
||||||
return stub;
|
return stub;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,14 +140,14 @@ public class ApiWebApplicationFactory : WebApplicationFactory<Program>
|
|||||||
private static IUserService CreateUserServiceStub()
|
private static IUserService CreateUserServiceStub()
|
||||||
{
|
{
|
||||||
var stub = Substitute.For<IUserService>();
|
var stub = Substitute.For<IUserService>();
|
||||||
var userDto = new UserDto(1, "test@test.com", "Test", null, UserRole.Student, true, 0, 0, 1, DateTime.UtcNow);
|
var userDto = new UserDto(1, "test@test.com", "Test", null, [UserRole.Student], true, 0, 0, 1, DateTime.UtcNow);
|
||||||
var pagedUsers = PagedResult<UserDto>.Create([userDto], 1, 1, 20);
|
var pagedUsers = PagedResult<UserDto>.Create([userDto], 1, 1, 20);
|
||||||
|
|
||||||
stub.GetByIdAsync(Arg.Any<int>()).Returns(userDto);
|
stub.GetByIdAsync(Arg.Any<int>()).Returns(userDto);
|
||||||
stub.UpdateProfileAsync(Arg.Any<int>(), Arg.Any<UpdateUserRequest>()).Returns(userDto);
|
stub.UpdateProfileAsync(Arg.Any<int>(), Arg.Any<UpdateUserRequest>()).Returns(userDto);
|
||||||
stub.GetStatsAsync(Arg.Any<int>()).Returns(new UserStatsDto(0, 0, 0, 0, 0, 1, 0));
|
stub.GetStatsAsync(Arg.Any<int>()).Returns(new UserStatsDto(0, 0, 0, 0, 0, 1, 0));
|
||||||
stub.GetAllAsync(Arg.Any<UserFilterRequest>()).Returns(pagedUsers);
|
stub.GetAllAsync(Arg.Any<UserFilterRequest>()).Returns(pagedUsers);
|
||||||
stub.SetRoleAsync(Arg.Any<int>(), Arg.Any<UserRole>()).Returns(Task.CompletedTask);
|
stub.SetRolesAsync(Arg.Any<int>(), Arg.Any<IReadOnlyCollection<UserRole>>()).Returns(Task.CompletedTask);
|
||||||
stub.SetActiveAsync(Arg.Any<int>(), Arg.Any<bool>()).Returns(Task.CompletedTask);
|
stub.SetActiveAsync(Arg.Any<int>(), Arg.Any<bool>()).Returns(Task.CompletedTask);
|
||||||
return stub;
|
return stub;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using Microsoft.AspNetCore.WebUtilities;
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
using UniVerse.Application.DTOs.Auth;
|
using UniVerse.Application.DTOs.Auth;
|
||||||
using UniVerse.Application.Interfaces;
|
using UniVerse.Application.Interfaces;
|
||||||
|
using UniVerse.Domain.Enums;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
|
||||||
@@ -184,7 +185,8 @@ public class AuthController : ControllerBase
|
|||||||
{
|
{
|
||||||
if (!HttpContext.RequestServices.GetRequiredService<IWebHostEnvironment>().IsDevelopment())
|
if (!HttpContext.RequestServices.GetRequiredService<IWebHostEnvironment>().IsDevelopment())
|
||||||
return NotFound();
|
return NotFound();
|
||||||
var result = await _auth.DevLoginAsync(request.Email, request.DisplayName, request.Role, GetClientIpAddress());
|
var roles = request.Roles?.Count > 0 ? request.Roles : [UserRole.Student];
|
||||||
|
var result = await _auth.DevLoginAsync(request.Email, request.DisplayName, roles, GetClientIpAddress());
|
||||||
SetRefreshTokenCookie(result.RefreshToken);
|
SetRefreshTokenCookie(result.RefreshToken);
|
||||||
return Ok(result.Response);
|
return Ok(result.Response);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,23 +137,26 @@ public class UsersController : ControllerBase
|
|||||||
public async Task<ActionResult> GetAll([FromQuery] UserFilterRequest filter) =>
|
public async Task<ActionResult> GetAll([FromQuery] UserFilterRequest filter) =>
|
||||||
Ok(await _users.GetAllAsync(filter));
|
Ok(await _users.GetAllAsync(filter));
|
||||||
|
|
||||||
/// <summary>Изменить роль пользователя.</summary>
|
/// <summary>Изменить набор ролей пользователя.</summary>
|
||||||
/// <remarks>Только Admin. Доступные роли: Student, Teacher, Admin.</remarks>
|
/// <remarks>Только Admin. Доступные роли: Student, Teacher, Admin.</remarks>
|
||||||
/// <param name="id">ID пользователя.</param>
|
/// <param name="id">ID пользователя.</param>
|
||||||
/// <param name="role">Новая роль.</param>
|
/// <param name="roles">Новый набор ролей пользователя.</param>
|
||||||
/// <response code="204">Роль успешно изменена.</response>
|
/// <response code="204">Роли успешно изменены.</response>
|
||||||
/// <response code="401">Требуется аутентификация.</response>
|
/// <response code="401">Требуется аутентификация.</response>
|
||||||
/// <response code="403">Требуется роль Admin.</response>
|
/// <response code="403">Требуется роль Admin.</response>
|
||||||
/// <response code="404">Пользователь не найден.</response>
|
/// <response code="404">Пользователь не найден.</response>
|
||||||
[Authorize(Roles = "Admin")]
|
[Authorize(Roles = "Admin")]
|
||||||
[HttpPatch("{id:int}/role")]
|
[HttpPatch("{id:int}/role")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<IActionResult> SetRole(int id, [FromBody] UserRole role)
|
public async Task<IActionResult> SetRole(int id, [FromBody] IReadOnlyCollection<UserRole> roles)
|
||||||
{
|
{
|
||||||
await _users.SetRoleAsync(id, role);
|
if (roles.Count == 0)
|
||||||
|
return BadRequest("At least one role is required.");
|
||||||
|
await _users.SetRolesAsync(id, roles);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ namespace UniVerse.Application.DTOs.Auth;
|
|||||||
public record AuthResponse(string AccessToken, DateTime ExpiresAt, UserAuthDto User);
|
public record AuthResponse(string AccessToken, DateTime ExpiresAt, UserAuthDto User);
|
||||||
public record AuthResult(AuthResponse Response, string RefreshToken);
|
public record AuthResult(AuthResponse Response, string RefreshToken);
|
||||||
|
|
||||||
public record UserAuthDto(int Id, string Email, string? DisplayName, UserRole Role);
|
public record UserAuthDto(int Id, string Email, string? DisplayName, IReadOnlyList<UserRole> Roles);
|
||||||
|
|
||||||
public record LoginMicrosoftRequest(string AuthorizationCode, string? RedirectUri = null);
|
public record LoginMicrosoftRequest(string AuthorizationCode, string? RedirectUri = null);
|
||||||
|
|
||||||
public record DevLoginRequest(string Email, string? DisplayName = null, UserRole Role = UserRole.Student);
|
public record DevLoginRequest(string Email, string? DisplayName = null, IReadOnlyList<UserRole>? Roles = null);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ public record UserDto(
|
|||||||
string Email,
|
string Email,
|
||||||
string? DisplayName,
|
string? DisplayName,
|
||||||
string? AvatarUrl,
|
string? AvatarUrl,
|
||||||
UserRole Role,
|
IReadOnlyList<UserRole> Roles,
|
||||||
bool IsActive,
|
bool IsActive,
|
||||||
int Xp,
|
int Xp,
|
||||||
int Coins,
|
int Coins,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace UniVerse.Application.Interfaces;
|
|||||||
public interface IAuthService
|
public interface IAuthService
|
||||||
{
|
{
|
||||||
Task<AuthResult> LoginWithMicrosoftAsync(string authorizationCode, string? redirectUri = null, string? ipAddress = null);
|
Task<AuthResult> LoginWithMicrosoftAsync(string authorizationCode, string? redirectUri = null, string? ipAddress = null);
|
||||||
Task<AuthResult> DevLoginAsync(string email, string? displayName, Domain.Enums.UserRole role, string? ipAddress = null);
|
Task<AuthResult> DevLoginAsync(string email, string? displayName, IReadOnlyCollection<Domain.Enums.UserRole> roles, string? ipAddress = null);
|
||||||
Task<AuthResult> RefreshTokenAsync(string refreshToken);
|
Task<AuthResult> RefreshTokenAsync(string refreshToken);
|
||||||
Task RevokeRefreshTokenAsync(string refreshToken);
|
Task RevokeRefreshTokenAsync(string refreshToken);
|
||||||
Task<UserDto> GetCurrentUserAsync(int userId);
|
Task<UserDto> GetCurrentUserAsync(int userId);
|
||||||
|
|||||||
@@ -10,6 +10,6 @@ public interface IUserService
|
|||||||
Task<UserDto> UpdateProfileAsync(int id, UpdateUserRequest request);
|
Task<UserDto> UpdateProfileAsync(int id, UpdateUserRequest request);
|
||||||
Task<UserStatsDto> GetStatsAsync(int id);
|
Task<UserStatsDto> GetStatsAsync(int id);
|
||||||
Task<PagedResult<UserDto>> GetAllAsync(UserFilterRequest filter);
|
Task<PagedResult<UserDto>> GetAllAsync(UserFilterRequest filter);
|
||||||
Task SetRoleAsync(int id, UserRole role);
|
Task SetRolesAsync(int id, IReadOnlyCollection<UserRole> roles);
|
||||||
Task SetActiveAsync(int id, bool isActive);
|
Task SetActiveAsync(int id, bool isActive);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ public static class MappingExtensions
|
|||||||
// --- User ---
|
// --- User ---
|
||||||
public static UserDto ToDto(this User user, int level) => new(
|
public static UserDto ToDto(this User user, int level) => new(
|
||||||
user.Id, user.Email, user.DisplayName, user.AvatarUrl,
|
user.Id, user.Email, user.DisplayName, user.AvatarUrl,
|
||||||
user.Role, user.IsActive, user.Xp, user.Coins, level, user.CreatedAt
|
user.Roles.Select(r => r.Role).OrderBy(r => r).ToList(), user.IsActive, user.Xp, user.Coins, level, user.CreatedAt
|
||||||
);
|
);
|
||||||
|
|
||||||
public static UserAuthDto ToAuthDto(this User user) => new(
|
public static UserAuthDto ToAuthDto(this User user) => new(
|
||||||
user.Id, user.Email, user.DisplayName, user.Role
|
user.Id, user.Email, user.DisplayName, user.Roles.Select(r => r.Role).OrderBy(r => r).ToList()
|
||||||
);
|
);
|
||||||
|
|
||||||
// --- Tag ---
|
// --- Tag ---
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ public class User
|
|||||||
public string Email { get; set; } = string.Empty;
|
public string Email { get; set; } = string.Empty;
|
||||||
public string? DisplayName { get; set; }
|
public string? DisplayName { get; set; }
|
||||||
public string? AvatarUrl { get; set; }
|
public string? AvatarUrl { get; set; }
|
||||||
public UserRole Role { get; set; } = UserRole.Student;
|
|
||||||
public bool IsActive { get; set; } = true;
|
public bool IsActive { get; set; } = true;
|
||||||
public string? MicrosoftId { get; set; }
|
public string? MicrosoftId { get; set; }
|
||||||
public int Xp { get; set; }
|
public int Xp { get; set; }
|
||||||
@@ -19,6 +18,7 @@ public class User
|
|||||||
// Navigation properties
|
// Navigation properties
|
||||||
public StudentProfile? StudentProfile { get; set; }
|
public StudentProfile? StudentProfile { get; set; }
|
||||||
public TeacherProfile? TeacherProfile { get; set; }
|
public TeacherProfile? TeacherProfile { get; set; }
|
||||||
|
public ICollection<UserRoleAssignment> Roles { get; set; } = new List<UserRoleAssignment>();
|
||||||
public ICollection<LectureEnrollment> Enrollments { get; set; } = new List<LectureEnrollment>();
|
public ICollection<LectureEnrollment> Enrollments { get; set; } = new List<LectureEnrollment>();
|
||||||
public ICollection<Review> Reviews { get; set; } = new List<Review>();
|
public ICollection<Review> Reviews { get; set; } = new List<Review>();
|
||||||
public ICollection<UserAchievement> UserAchievements { get; set; } = new List<UserAchievement>();
|
public ICollection<UserAchievement> UserAchievements { get; set; } = new List<UserAchievement>();
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using UniVerse.Domain.Enums;
|
||||||
|
|
||||||
|
namespace UniVerse.Domain.Entities;
|
||||||
|
|
||||||
|
public class UserRoleAssignment
|
||||||
|
{
|
||||||
|
public int UserId { get; set; }
|
||||||
|
public UserRole Role { get; set; }
|
||||||
|
|
||||||
|
public User User { get; set; } = null!;
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ public class AppDbContext : DbContext
|
|||||||
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
|
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
|
||||||
|
|
||||||
public DbSet<User> Users { get; set; } = null!;
|
public DbSet<User> Users { get; set; } = null!;
|
||||||
|
public DbSet<UserRoleAssignment> UserRoles { get; set; } = null!;
|
||||||
public DbSet<StudentProfile> StudentProfiles { get; set; } = null!;
|
public DbSet<StudentProfile> StudentProfiles { get; set; } = null!;
|
||||||
public DbSet<TeacherProfile> TeacherProfiles { get; set; } = null!;
|
public DbSet<TeacherProfile> TeacherProfiles { get; set; } = null!;
|
||||||
public DbSet<Course> Courses { get; set; } = null!;
|
public DbSet<Course> Courses { get; set; } = null!;
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ public class UserConfiguration : IEntityTypeConfiguration<User>
|
|||||||
builder.Property(u => u.Email).HasColumnName("email").HasMaxLength(255).IsRequired();
|
builder.Property(u => u.Email).HasColumnName("email").HasMaxLength(255).IsRequired();
|
||||||
builder.Property(u => u.DisplayName).HasColumnName("display_name").HasMaxLength(255);
|
builder.Property(u => u.DisplayName).HasColumnName("display_name").HasMaxLength(255);
|
||||||
builder.Property(u => u.AvatarUrl).HasColumnName("avatar_url").HasMaxLength(500);
|
builder.Property(u => u.AvatarUrl).HasColumnName("avatar_url").HasMaxLength(500);
|
||||||
builder.Property(u => u.Role).HasColumnName("role");
|
|
||||||
builder.Property(u => u.IsActive).HasColumnName("is_active").HasDefaultValue(true);
|
builder.Property(u => u.IsActive).HasColumnName("is_active").HasDefaultValue(true);
|
||||||
builder.Property(u => u.MicrosoftId).HasColumnName("microsoft_id").HasMaxLength(255);
|
builder.Property(u => u.MicrosoftId).HasColumnName("microsoft_id").HasMaxLength(255);
|
||||||
builder.Property(u => u.Xp).HasColumnName("xp").HasDefaultValue(0);
|
builder.Property(u => u.Xp).HasColumnName("xp").HasDefaultValue(0);
|
||||||
@@ -25,5 +24,10 @@ public class UserConfiguration : IEntityTypeConfiguration<User>
|
|||||||
|
|
||||||
builder.HasIndex(u => u.Email).IsUnique();
|
builder.HasIndex(u => u.Email).IsUnique();
|
||||||
builder.HasIndex(u => u.MicrosoftId).IsUnique().HasFilter("microsoft_id IS NOT NULL");
|
builder.HasIndex(u => u.MicrosoftId).IsUnique().HasFilter("microsoft_id IS NOT NULL");
|
||||||
|
|
||||||
|
builder.HasMany(u => u.Roles)
|
||||||
|
.WithOne(ur => ur.User)
|
||||||
|
.HasForeignKey(ur => ur.UserId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using UniVerse.Domain.Entities;
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Data.Configurations;
|
||||||
|
|
||||||
|
public class UserRoleAssignmentConfiguration : IEntityTypeConfiguration<UserRoleAssignment>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<UserRoleAssignment> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("user_roles");
|
||||||
|
|
||||||
|
builder.HasKey(ur => new { ur.UserId, ur.Role });
|
||||||
|
builder.Property(ur => ur.UserId).HasColumnName("user_id");
|
||||||
|
builder.Property(ur => ur.Role).HasColumnName("role");
|
||||||
|
}
|
||||||
|
}
|
||||||
+979
@@ -0,0 +1,979 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
using UniVerse.Infrastructure.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260511011508_UserRolesJoinTable")]
|
||||||
|
partial class UserRolesJoinTable
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.7")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "coin_transaction_type", "coin_transaction_type", new[] { "review_reward", "achievement_reward", "attendance_reward", "admin_adjustment" });
|
||||||
|
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "review_llm_status", "review_llm_status", new[] { "pending", "analyzed", "rejected" });
|
||||||
|
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "review_sentiment", "review_sentiment", new[] { "positive", "neutral", "negative" });
|
||||||
|
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "tag_type", "tag_type", new[] { "institute", "faculty", "subject", "organization", "topic", "other" });
|
||||||
|
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "user_role", "user_role", new[] { "student", "teacher", "admin" });
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Achievement", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("CoinReward")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("coin_reward");
|
||||||
|
|
||||||
|
b.Property<string>("Condition")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("condition");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<string>("IconUrl")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)")
|
||||||
|
.HasColumnName("icon_url");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<int>("XpReward")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("xp_reward");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("achievements", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.CoinTransaction", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int?>("AchievementId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("achievement_id");
|
||||||
|
|
||||||
|
b.Property<int>("Amount")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("amount");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<int?>("ReviewId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("review_id");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("type");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AchievementId");
|
||||||
|
|
||||||
|
b.HasIndex("ReviewId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("coin_transactions", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Course", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<string>("ExternalId")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("external_id");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSynced")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_synced");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ExternalId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasFilter("external_id IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("courses", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.CourseTag", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("CourseId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("course_id");
|
||||||
|
|
||||||
|
b.Property<int>("TagId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("tag_id");
|
||||||
|
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.HasKey("CourseId", "TagId");
|
||||||
|
|
||||||
|
b.HasIndex("TagId");
|
||||||
|
|
||||||
|
b.HasIndex("CourseId", "TagId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("course_tags", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Lecture", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("CourseId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("course_id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<DateTime>("EndsAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("ends_at");
|
||||||
|
|
||||||
|
b.Property<string>("ExternalId")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("external_id");
|
||||||
|
|
||||||
|
b.Property<int>("Format")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("format");
|
||||||
|
|
||||||
|
b.Property<bool>("IsOpen")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(true)
|
||||||
|
.HasColumnName("is_open");
|
||||||
|
|
||||||
|
b.Property<int?>("LocationId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("location_id");
|
||||||
|
|
||||||
|
b.Property<int>("MaxEnrollments")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("max_enrollments");
|
||||||
|
|
||||||
|
b.Property<string>("OnlineUrl")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)")
|
||||||
|
.HasColumnName("online_url");
|
||||||
|
|
||||||
|
b.Property<DateTime>("StartsAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("starts_at");
|
||||||
|
|
||||||
|
b.Property<int?>("TeacherId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("teacher_id");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)")
|
||||||
|
.HasColumnName("title");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CourseId");
|
||||||
|
|
||||||
|
b.HasIndex("ExternalId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasFilter("external_id IS NOT NULL");
|
||||||
|
|
||||||
|
b.HasIndex("LocationId");
|
||||||
|
|
||||||
|
b.HasIndex("StartsAt");
|
||||||
|
|
||||||
|
b.HasIndex("TeacherId");
|
||||||
|
|
||||||
|
b.ToTable("lectures", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.LectureEnrollment", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("LectureId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("lecture_id");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.Property<bool>("Attended")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("attended");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.HasKey("LectureId", "UserId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.HasIndex("LectureId", "UserId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("lecture_enrollments", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Location", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Address")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)")
|
||||||
|
.HasColumnName("address");
|
||||||
|
|
||||||
|
b.Property<string>("Building")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("building");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<string>("ExternalId")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("external_id");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<string>("Room")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)")
|
||||||
|
.HasColumnName("room");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ExternalId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasFilter("external_id IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("locations", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.RefreshToken", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExpiresAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("expires_at");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("RevokedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("revoked_at");
|
||||||
|
|
||||||
|
b.Property<string>("Token")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)")
|
||||||
|
.HasColumnName("token");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Token")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("refresh_tokens", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Review", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<bool?>("IsInformative")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("is_informative");
|
||||||
|
|
||||||
|
b.Property<int>("LectureId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("lecture_id");
|
||||||
|
|
||||||
|
b.Property<int>("LlmStatus")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("llm_status");
|
||||||
|
|
||||||
|
b.PrimitiveCollection<string[]>("LlmTags")
|
||||||
|
.HasColumnType("text[]")
|
||||||
|
.HasColumnName("llm_tags");
|
||||||
|
|
||||||
|
b.Property<double?>("QualityScore")
|
||||||
|
.HasColumnType("double precision")
|
||||||
|
.HasColumnName("quality_score");
|
||||||
|
|
||||||
|
b.Property<int>("Rating")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("rating");
|
||||||
|
|
||||||
|
b.Property<int?>("Sentiment")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("sentiment");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("LlmStatus");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.HasIndex("LectureId", "UserId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("reviews", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.StudentProfile", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int?>("EnrollmentYear")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("enrollment_year");
|
||||||
|
|
||||||
|
b.Property<string>("Faculty")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("faculty");
|
||||||
|
|
||||||
|
b.Property<string>("GroupName")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)")
|
||||||
|
.HasColumnName("group_name");
|
||||||
|
|
||||||
|
b.Property<string>("Specialty")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("specialty");
|
||||||
|
|
||||||
|
b.Property<string>("StudentId")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)")
|
||||||
|
.HasColumnName("student_id");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("student_profiles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Tag", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<int?>("ParentId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("parent_id");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("type");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ParentId");
|
||||||
|
|
||||||
|
b.ToTable("tags", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.TeacherProfile", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Bio")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("bio");
|
||||||
|
|
||||||
|
b.Property<string>("Department")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("department");
|
||||||
|
|
||||||
|
b.Property<string>("ModeusId")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("modeus_id");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("title");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("teacher_profiles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("AvatarUrl")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)")
|
||||||
|
.HasColumnName("avatar_url");
|
||||||
|
|
||||||
|
b.Property<int>("Coins")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("coins");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<string>("DisplayName")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("display_name");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("email");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(true)
|
||||||
|
.HasColumnName("is_active");
|
||||||
|
|
||||||
|
b.Property<string>("MicrosoftId")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
|
.HasColumnName("microsoft_id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<int>("Xp")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("xp");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Email")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("MicrosoftId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasFilter("microsoft_id IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("users", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.UserAchievement", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.Property<int>("AchievementId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("achievement_id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("AwardedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("awarded_at")
|
||||||
|
.HasDefaultValueSql("NOW()");
|
||||||
|
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "AchievementId");
|
||||||
|
|
||||||
|
b.HasIndex("AchievementId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId", "AchievementId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("user_achievements", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.UserRoleAssignment", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.Property<int>("Role")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("role");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "Role");
|
||||||
|
|
||||||
|
b.ToTable("user_roles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.CoinTransaction", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.Achievement", "Achievement")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AchievementId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.Review", "Review")
|
||||||
|
.WithMany("CoinTransactions")
|
||||||
|
.HasForeignKey("ReviewId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.User", "User")
|
||||||
|
.WithMany("CoinTransactions")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Achievement");
|
||||||
|
|
||||||
|
b.Navigation("Review");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.CourseTag", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.Course", "Course")
|
||||||
|
.WithMany("CourseTags")
|
||||||
|
.HasForeignKey("CourseId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.Tag", "Tag")
|
||||||
|
.WithMany("CourseTags")
|
||||||
|
.HasForeignKey("TagId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Course");
|
||||||
|
|
||||||
|
b.Navigation("Tag");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Lecture", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.Course", "Course")
|
||||||
|
.WithMany("Lectures")
|
||||||
|
.HasForeignKey("CourseId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.Location", "Location")
|
||||||
|
.WithMany("Lectures")
|
||||||
|
.HasForeignKey("LocationId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.User", "Teacher")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TeacherId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("Course");
|
||||||
|
|
||||||
|
b.Navigation("Location");
|
||||||
|
|
||||||
|
b.Navigation("Teacher");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.LectureEnrollment", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.Lecture", "Lecture")
|
||||||
|
.WithMany("Enrollments")
|
||||||
|
.HasForeignKey("LectureId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.User", "User")
|
||||||
|
.WithMany("Enrollments")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Lecture");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.RefreshToken", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.User", "User")
|
||||||
|
.WithMany("RefreshTokens")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Review", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.Lecture", "Lecture")
|
||||||
|
.WithMany("Reviews")
|
||||||
|
.HasForeignKey("LectureId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.User", "User")
|
||||||
|
.WithMany("Reviews")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Lecture");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.StudentProfile", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.User", "User")
|
||||||
|
.WithOne("StudentProfile")
|
||||||
|
.HasForeignKey("UniVerse.Domain.Entities.StudentProfile", "UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Tag", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.Tag", "Parent")
|
||||||
|
.WithMany("Children")
|
||||||
|
.HasForeignKey("ParentId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("Parent");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.TeacherProfile", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.User", "User")
|
||||||
|
.WithOne("TeacherProfile")
|
||||||
|
.HasForeignKey("UniVerse.Domain.Entities.TeacherProfile", "UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.UserAchievement", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.Achievement", "Achievement")
|
||||||
|
.WithMany("UserAchievements")
|
||||||
|
.HasForeignKey("AchievementId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.User", "User")
|
||||||
|
.WithMany("UserAchievements")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Achievement");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.UserRoleAssignment", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.User", "User")
|
||||||
|
.WithMany("Roles")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Achievement", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("UserAchievements");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Course", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("CourseTags");
|
||||||
|
|
||||||
|
b.Navigation("Lectures");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Lecture", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Enrollments");
|
||||||
|
|
||||||
|
b.Navigation("Reviews");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Location", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Lectures");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Review", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("CoinTransactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.Tag", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Children");
|
||||||
|
|
||||||
|
b.Navigation("CourseTags");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("CoinTransactions");
|
||||||
|
|
||||||
|
b.Navigation("Enrollments");
|
||||||
|
|
||||||
|
b.Navigation("RefreshTokens");
|
||||||
|
|
||||||
|
b.Navigation("Reviews");
|
||||||
|
|
||||||
|
b.Navigation("Roles");
|
||||||
|
|
||||||
|
b.Navigation("StudentProfile");
|
||||||
|
|
||||||
|
b.Navigation("TeacherProfile");
|
||||||
|
|
||||||
|
b.Navigation("UserAchievements");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace UniVerse.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class UserRolesJoinTable : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "user_roles",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
user_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
role = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_user_roles", x => new { x.user_id, x.role });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_user_roles_users_user_id",
|
||||||
|
column: x => x.user_id,
|
||||||
|
principalTable: "users",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.Sql("""
|
||||||
|
INSERT INTO user_roles (user_id, role)
|
||||||
|
SELECT id, role FROM users;
|
||||||
|
""");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "role",
|
||||||
|
table: "users");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "role",
|
||||||
|
table: "users",
|
||||||
|
type: "integer",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
|
||||||
|
migrationBuilder.Sql("""
|
||||||
|
UPDATE users
|
||||||
|
SET role = COALESCE((
|
||||||
|
SELECT MIN(ur.role)
|
||||||
|
FROM user_roles ur
|
||||||
|
WHERE ur.user_id = users.id
|
||||||
|
), 0);
|
||||||
|
""");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "user_roles");
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ namespace UniVerse.Infrastructure.Migrations
|
|||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "10.0.0")
|
.HasAnnotation("ProductVersion", "10.0.7")
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "coin_transaction_type", "coin_transaction_type", new[] { "review_reward", "achievement_reward", "attendance_reward", "admin_adjustment" });
|
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "coin_transaction_type", "coin_transaction_type", new[] { "review_reward", "achievement_reward", "attendance_reward", "admin_adjustment" });
|
||||||
@@ -667,10 +667,6 @@ namespace UniVerse.Infrastructure.Migrations
|
|||||||
.HasColumnType("character varying(255)")
|
.HasColumnType("character varying(255)")
|
||||||
.HasColumnName("microsoft_id");
|
.HasColumnName("microsoft_id");
|
||||||
|
|
||||||
b.Property<int>("Role")
|
|
||||||
.HasColumnType("integer")
|
|
||||||
.HasColumnName("role");
|
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedAt")
|
b.Property<DateTime>("UpdatedAt")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
@@ -725,6 +721,21 @@ namespace UniVerse.Infrastructure.Migrations
|
|||||||
b.ToTable("user_achievements", (string)null);
|
b.ToTable("user_achievements", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.UserRoleAssignment", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.Property<int>("Role")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("role");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "Role");
|
||||||
|
|
||||||
|
b.ToTable("user_roles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("UniVerse.Domain.Entities.CoinTransaction", b =>
|
modelBuilder.Entity("UniVerse.Domain.Entities.CoinTransaction", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("UniVerse.Domain.Entities.Achievement", "Achievement")
|
b.HasOne("UniVerse.Domain.Entities.Achievement", "Achievement")
|
||||||
@@ -894,6 +905,17 @@ namespace UniVerse.Infrastructure.Migrations
|
|||||||
b.Navigation("User");
|
b.Navigation("User");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UniVerse.Domain.Entities.UserRoleAssignment", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("UniVerse.Domain.Entities.User", "User")
|
||||||
|
.WithMany("Roles")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("UniVerse.Domain.Entities.Achievement", b =>
|
modelBuilder.Entity("UniVerse.Domain.Entities.Achievement", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("UserAchievements");
|
b.Navigation("UserAchievements");
|
||||||
@@ -940,6 +962,8 @@ namespace UniVerse.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.Navigation("Reviews");
|
b.Navigation("Reviews");
|
||||||
|
|
||||||
|
b.Navigation("Roles");
|
||||||
|
|
||||||
b.Navigation("StudentProfile");
|
b.Navigation("StudentProfile");
|
||||||
|
|
||||||
b.Navigation("TeacherProfile");
|
b.Navigation("TeacherProfile");
|
||||||
|
|||||||
@@ -85,20 +85,21 @@ public class AuthService : IAuthService
|
|||||||
throw new UnauthorizedException("Email не найден в токене Microsoft.");
|
throw new UnauthorizedException("Email не найден в токене Microsoft.");
|
||||||
|
|
||||||
// Automatically provision user
|
// Automatically provision user
|
||||||
var user = await _db.Users.FirstOrDefaultAsync(u => u.Email == email);
|
var user = await _db.Users
|
||||||
|
.Include(u => u.Roles)
|
||||||
|
.FirstOrDefaultAsync(u => u.Email == email);
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
user = new User
|
user = new User
|
||||||
{
|
{
|
||||||
Email = email,
|
Email = email,
|
||||||
DisplayName = name ?? email.Split('@')[0],
|
DisplayName = name ?? email.Split('@')[0],
|
||||||
Role = UserRole.Student, // Default role
|
|
||||||
IsActive = true
|
IsActive = true
|
||||||
};
|
};
|
||||||
_db.Users.Add(user);
|
_db.Users.Add(user);
|
||||||
await _db.SaveChangesAsync();
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
// Create corresponding profile
|
user.Roles.Add(new UserRoleAssignment { UserId = user.Id, Role = UserRole.Student });
|
||||||
_db.StudentProfiles.Add(new StudentProfile { UserId = user.Id });
|
_db.StudentProfiles.Add(new StudentProfile { UserId = user.Id });
|
||||||
await _db.SaveChangesAsync();
|
await _db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
@@ -107,6 +108,13 @@ public class AuthService : IAuthService
|
|||||||
throw new ForbiddenException("Аккаунт деактивирован.");
|
throw new ForbiddenException("Аккаунт деактивирован.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user.Roles.Count == 0)
|
||||||
|
{
|
||||||
|
user.Roles.Add(new UserRoleAssignment { UserId = user.Id, Role = UserRole.Student });
|
||||||
|
await EnsureProfilesForRolesAsync(user.Id, [UserRole.Student]);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
var accessToken = GenerateAccessToken(user);
|
var accessToken = GenerateAccessToken(user);
|
||||||
var refreshToken = await GenerateRefreshTokenAsync(user.Id);
|
var refreshToken = await GenerateRefreshTokenAsync(user.Id);
|
||||||
await TrySendLoginNotificationAsync(user, ipAddress);
|
await TrySendLoginNotificationAsync(user, ipAddress);
|
||||||
@@ -121,9 +129,12 @@ public class AuthService : IAuthService
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<AuthResult> DevLoginAsync(string email, string? displayName, UserRole role, string? ipAddress = null)
|
public async Task<AuthResult> DevLoginAsync(string email, string? displayName, IReadOnlyCollection<UserRole> roles, string? ipAddress = null)
|
||||||
{
|
{
|
||||||
var user = await _db.Users.FirstOrDefaultAsync(u => u.Email == email);
|
var normalizedRoles = (roles.Count > 0 ? roles : [UserRole.Student]).Distinct().ToList();
|
||||||
|
var user = await _db.Users
|
||||||
|
.Include(u => u.Roles)
|
||||||
|
.FirstOrDefaultAsync(u => u.Email == email);
|
||||||
|
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
@@ -131,25 +142,23 @@ public class AuthService : IAuthService
|
|||||||
{
|
{
|
||||||
Email = email,
|
Email = email,
|
||||||
DisplayName = displayName ?? email.Split('@')[0],
|
DisplayName = displayName ?? email.Split('@')[0],
|
||||||
Role = role,
|
|
||||||
IsActive = true
|
IsActive = true
|
||||||
};
|
};
|
||||||
_db.Users.Add(user);
|
_db.Users.Add(user);
|
||||||
await _db.SaveChangesAsync();
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
// Create profile based on role
|
|
||||||
if (role == UserRole.Student)
|
|
||||||
{
|
|
||||||
_db.StudentProfiles.Add(new StudentProfile { UserId = user.Id });
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
else if (role == UserRole.Teacher)
|
|
||||||
{
|
|
||||||
_db.TeacherProfiles.Add(new TeacherProfile { UserId = user.Id });
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var existing = user.Roles.Select(r => r.Role).ToHashSet();
|
||||||
|
var toRemove = user.Roles.Where(r => !normalizedRoles.Contains(r.Role)).ToList();
|
||||||
|
foreach (var item in toRemove)
|
||||||
|
user.Roles.Remove(item);
|
||||||
|
|
||||||
|
foreach (var role in normalizedRoles.Where(r => !existing.Contains(r)))
|
||||||
|
user.Roles.Add(new UserRoleAssignment { UserId = user.Id, Role = role });
|
||||||
|
|
||||||
|
await EnsureProfilesForRolesAsync(user.Id, normalizedRoles);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
if (!user.IsActive)
|
if (!user.IsActive)
|
||||||
throw new ForbiddenException("Аккаунт деактивирован.");
|
throw new ForbiddenException("Аккаунт деактивирован.");
|
||||||
|
|
||||||
@@ -171,6 +180,7 @@ public class AuthService : IAuthService
|
|||||||
{
|
{
|
||||||
var token = await _db.RefreshTokens
|
var token = await _db.RefreshTokens
|
||||||
.Include(rt => rt.User)
|
.Include(rt => rt.User)
|
||||||
|
.ThenInclude(u => u.Roles)
|
||||||
.FirstOrDefaultAsync(rt => rt.Token == refreshToken);
|
.FirstOrDefaultAsync(rt => rt.Token == refreshToken);
|
||||||
|
|
||||||
if (token == null || !token.IsActive)
|
if (token == null || !token.IsActive)
|
||||||
@@ -207,7 +217,9 @@ public class AuthService : IAuthService
|
|||||||
|
|
||||||
public async Task<UserDto> GetCurrentUserAsync(int userId)
|
public async Task<UserDto> GetCurrentUserAsync(int userId)
|
||||||
{
|
{
|
||||||
var user = await _db.Users.FindAsync(userId)
|
var user = await _db.Users
|
||||||
|
.Include(u => u.Roles)
|
||||||
|
.FirstOrDefaultAsync(u => u.Id == userId)
|
||||||
?? throw new NotFoundException("User", userId);
|
?? throw new NotFoundException("User", userId);
|
||||||
return user.ToDto(_gamification.CalculateLevel(user.Xp));
|
return user.ToDto(_gamification.CalculateLevel(user.Xp));
|
||||||
}
|
}
|
||||||
@@ -245,13 +257,15 @@ public class AuthService : IAuthService
|
|||||||
Encoding.UTF8.GetBytes(_config["Jwt:Secret"]!));
|
Encoding.UTF8.GetBytes(_config["Jwt:Secret"]!));
|
||||||
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||||
|
|
||||||
var claims = new[]
|
var claims = new List<Claim>
|
||||||
{
|
{
|
||||||
new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
|
new(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
|
||||||
new Claim(JwtRegisteredClaimNames.Email, user.Email),
|
new(JwtRegisteredClaimNames.Email, user.Email),
|
||||||
new Claim(ClaimTypes.Role, user.Role.ToString()),
|
new("display_name", user.DisplayName ?? "")
|
||||||
new Claim("display_name", user.DisplayName ?? "")
|
|
||||||
};
|
};
|
||||||
|
var roles = user.Roles.Select(r => r.Role).Distinct().ToList();
|
||||||
|
if (roles.Count == 0) roles.Add(UserRole.Student);
|
||||||
|
claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role.ToString())));
|
||||||
|
|
||||||
var token = new JwtSecurityToken(
|
var token = new JwtSecurityToken(
|
||||||
issuer: _config["Jwt:Issuer"],
|
issuer: _config["Jwt:Issuer"],
|
||||||
@@ -264,6 +278,23 @@ public class AuthService : IAuthService
|
|||||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task EnsureProfilesForRolesAsync(int userId, IReadOnlyCollection<UserRole> roles)
|
||||||
|
{
|
||||||
|
if (roles.Contains(UserRole.Student))
|
||||||
|
{
|
||||||
|
var hasStudentProfile = await _db.StudentProfiles.AnyAsync(p => p.UserId == userId);
|
||||||
|
if (!hasStudentProfile)
|
||||||
|
_db.StudentProfiles.Add(new StudentProfile { UserId = userId });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roles.Contains(UserRole.Teacher))
|
||||||
|
{
|
||||||
|
var hasTeacherProfile = await _db.TeacherProfiles.AnyAsync(p => p.UserId == userId);
|
||||||
|
if (!hasTeacherProfile)
|
||||||
|
_db.TeacherProfiles.Add(new TeacherProfile { UserId = userId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<string> GenerateRefreshTokenAsync(int userId)
|
private async Task<string> GenerateRefreshTokenAsync(int userId)
|
||||||
{
|
{
|
||||||
var randomBytes = RandomNumberGenerator.GetBytes(64);
|
var randomBytes = RandomNumberGenerator.GetBytes(64);
|
||||||
|
|||||||
@@ -22,14 +22,18 @@ public class UserService : IUserService
|
|||||||
|
|
||||||
public async Task<UserDto> GetByIdAsync(int id)
|
public async Task<UserDto> GetByIdAsync(int id)
|
||||||
{
|
{
|
||||||
var user = await _db.Users.FindAsync(id)
|
var user = await _db.Users
|
||||||
|
.Include(u => u.Roles)
|
||||||
|
.FirstOrDefaultAsync(u => u.Id == id)
|
||||||
?? throw new NotFoundException("User", id);
|
?? throw new NotFoundException("User", id);
|
||||||
return user.ToDto(_gamification.CalculateLevel(user.Xp));
|
return user.ToDto(_gamification.CalculateLevel(user.Xp));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<UserDto> UpdateProfileAsync(int id, UpdateUserRequest request)
|
public async Task<UserDto> UpdateProfileAsync(int id, UpdateUserRequest request)
|
||||||
{
|
{
|
||||||
var user = await _db.Users.FindAsync(id)
|
var user = await _db.Users
|
||||||
|
.Include(u => u.Roles)
|
||||||
|
.FirstOrDefaultAsync(u => u.Id == id)
|
||||||
?? throw new NotFoundException("User", id);
|
?? throw new NotFoundException("User", id);
|
||||||
|
|
||||||
if (request.DisplayName != null) user.DisplayName = request.DisplayName;
|
if (request.DisplayName != null) user.DisplayName = request.DisplayName;
|
||||||
@@ -68,8 +72,15 @@ public class UserService : IUserService
|
|||||||
(u.DisplayName != null && u.DisplayName.ToLower().Contains(search)));
|
(u.DisplayName != null && u.DisplayName.ToLower().Contains(search)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
query = query.Include(u => u.Roles);
|
||||||
|
|
||||||
if (filter.Role.HasValue)
|
if (filter.Role.HasValue)
|
||||||
query = query.Where(u => u.Role == filter.Role.Value);
|
{
|
||||||
|
var role = filter.Role.Value;
|
||||||
|
query = query.Where(u =>
|
||||||
|
u.Roles.Count == 1 &&
|
||||||
|
u.Roles.Any(ur => ur.Role == role));
|
||||||
|
}
|
||||||
|
|
||||||
if (filter.IsActive.HasValue)
|
if (filter.IsActive.HasValue)
|
||||||
query = query.Where(u => u.IsActive == filter.IsActive.Value);
|
query = query.Where(u => u.IsActive == filter.IsActive.Value);
|
||||||
@@ -86,11 +97,27 @@ public class UserService : IUserService
|
|||||||
return PagedResult<UserDto>.Create(items, total, filter.Page, filter.PageSize);
|
return PagedResult<UserDto>.Create(items, total, filter.Page, filter.PageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SetRoleAsync(int id, UserRole role)
|
public async Task SetRolesAsync(int id, IReadOnlyCollection<UserRole> roles)
|
||||||
{
|
{
|
||||||
var user = await _db.Users.FindAsync(id)
|
var normalizedRoles = roles.Distinct().ToList();
|
||||||
|
if (normalizedRoles.Count == 0)
|
||||||
|
throw new ForbiddenException("At least one role is required.");
|
||||||
|
|
||||||
|
var user = await _db.Users
|
||||||
|
.Include(u => u.Roles)
|
||||||
|
.FirstOrDefaultAsync(u => u.Id == id)
|
||||||
?? throw new NotFoundException("User", id);
|
?? throw new NotFoundException("User", id);
|
||||||
user.Role = role;
|
|
||||||
|
var existing = user.Roles.Select(r => r.Role).ToHashSet();
|
||||||
|
var toRemove = user.Roles.Where(r => !normalizedRoles.Contains(r.Role)).ToList();
|
||||||
|
foreach (var item in toRemove)
|
||||||
|
user.Roles.Remove(item);
|
||||||
|
|
||||||
|
var toAdd = normalizedRoles.Where(r => !existing.Contains(r)).ToList();
|
||||||
|
foreach (var role in toAdd)
|
||||||
|
user.Roles.Add(new Domain.Entities.UserRoleAssignment { UserId = user.Id, Role = role });
|
||||||
|
|
||||||
|
await EnsureProfilesForRolesAsync(user.Id, normalizedRoles);
|
||||||
user.UpdatedAt = DateTime.UtcNow;
|
user.UpdatedAt = DateTime.UtcNow;
|
||||||
await _db.SaveChangesAsync();
|
await _db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
@@ -103,4 +130,21 @@ public class UserService : IUserService
|
|||||||
user.UpdatedAt = DateTime.UtcNow;
|
user.UpdatedAt = DateTime.UtcNow;
|
||||||
await _db.SaveChangesAsync();
|
await _db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task EnsureProfilesForRolesAsync(int userId, IReadOnlyCollection<UserRole> roles)
|
||||||
|
{
|
||||||
|
if (roles.Contains(UserRole.Student))
|
||||||
|
{
|
||||||
|
var hasStudentProfile = await _db.StudentProfiles.AnyAsync(p => p.UserId == userId);
|
||||||
|
if (!hasStudentProfile)
|
||||||
|
_db.StudentProfiles.Add(new Domain.Entities.StudentProfile { UserId = userId });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roles.Contains(UserRole.Teacher))
|
||||||
|
{
|
||||||
|
var hasTeacherProfile = await _db.TeacherProfiles.AnyAsync(p => p.UserId == userId);
|
||||||
|
if (!hasTeacherProfile)
|
||||||
|
_db.TeacherProfiles.Add(new Domain.Entities.TeacherProfile { UserId = userId });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ export const usersApi = {
|
|||||||
)
|
)
|
||||||
return extractItems(payload)
|
return extractItems(payload)
|
||||||
},
|
},
|
||||||
setRole: (id: string | number, role: 'Student' | 'Teacher' | 'Admin') =>
|
setRole: (id: string | number, roles: Array<'Student' | 'Teacher' | 'Admin'>) =>
|
||||||
apiRequest<void>(`/users/${id}/role`, { method: 'PATCH', body: JSON.stringify(role) }),
|
apiRequest<void>(`/users/${id}/role`, { method: 'PATCH', body: JSON.stringify(roles) }),
|
||||||
setActive: (id: string | number, isActive: boolean) =>
|
setActive: (id: string | number, isActive: boolean) =>
|
||||||
apiRequest<void>(`/users/${id}/active`, { method: 'PATCH', body: JSON.stringify(isActive) }),
|
apiRequest<void>(`/users/${id}/active`, { method: 'PATCH', body: JSON.stringify(isActive) }),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,12 +16,26 @@ export function mapApiRole(role: string | undefined): UserRole {
|
|||||||
return 'student'
|
return 'student'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mapApiRoles(roles: string[] | undefined): UserRole[] {
|
||||||
|
if (!roles?.length) return ['student']
|
||||||
|
return Array.from(new Set(roles.map(mapApiRole)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultActiveRole(roles: UserRole[]): UserRole {
|
||||||
|
if (roles.includes('student')) return 'student'
|
||||||
|
if (roles.includes('teacher')) return 'teacher'
|
||||||
|
if (roles.includes('admin')) return 'admin'
|
||||||
|
return 'student'
|
||||||
|
}
|
||||||
|
|
||||||
export function mapApiUser(user: UserAuthDto | UserDto, stats?: UserStatsDto): User {
|
export function mapApiUser(user: UserAuthDto | UserDto, stats?: UserStatsDto): User {
|
||||||
|
const roles = mapApiRoles(user.roles)
|
||||||
return {
|
return {
|
||||||
id: String(user.id),
|
id: String(user.id),
|
||||||
name: user.displayName || user.email || 'Пользователь UniVerse',
|
name: user.displayName || user.email || 'Пользователь UniVerse',
|
||||||
email: user.email || '',
|
email: user.email || '',
|
||||||
role: mapApiRole(user.role),
|
roles,
|
||||||
|
activeRole: getDefaultActiveRole(roles),
|
||||||
avatar: 'avatarUrl' in user ? user.avatarUrl ?? undefined : undefined,
|
avatar: 'avatarUrl' in user ? user.avatarUrl ?? undefined : undefined,
|
||||||
institute: 'ЮФУ',
|
institute: 'ЮФУ',
|
||||||
department: '',
|
department: '',
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export interface UserAuthDto {
|
|||||||
id: number
|
id: number
|
||||||
email: string
|
email: string
|
||||||
displayName?: string | null
|
displayName?: string | null
|
||||||
role: ApiUserRole
|
roles: ApiUserRole[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserDto extends UserAuthDto {
|
export interface UserDto extends UserAuthDto {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const auth = useAuthStore()
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const navItems = computed(() => {
|
const navItems = computed(() => {
|
||||||
const role = auth.user?.role ?? 'student'
|
const role = auth.user?.activeRole ?? 'student'
|
||||||
if (role === 'teacher') return [
|
if (role === 'teacher') return [
|
||||||
{ label: 'Дашборд', icon: 'chart-bar', to: '/teacher' },
|
{ label: 'Дашборд', icon: 'chart-bar', to: '/teacher' },
|
||||||
{ label: 'Лекции', icon: 'book-2', to: '/teacher/lectures' },
|
{ label: 'Лекции', icon: 'book-2', to: '/teacher/lectures' },
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const router = useRouter()
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
interface NavItem { label: string; icon: string; to: string; roles: string[] }
|
interface NavItem { label: string; icon: string; to: string; roles: string[] }
|
||||||
|
type AppRole = 'student' | 'teacher' | 'admin'
|
||||||
|
|
||||||
const navItems: NavItem[] = [
|
const navItems: NavItem[] = [
|
||||||
{ label: 'Главная', icon: 'home', to: '/', roles: ['student'] },
|
{ label: 'Главная', icon: 'home', to: '/', roles: ['student'] },
|
||||||
@@ -30,9 +31,32 @@ const navItems: NavItem[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const visible = computed(() =>
|
const visible = computed(() =>
|
||||||
navItems.filter(n => auth.user && n.roles.includes(auth.user.role))
|
navItems.filter(n => auth.user && n.roles.includes(auth.user.activeRole))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const roleButtons = computed(() => {
|
||||||
|
if (!auth.user) return []
|
||||||
|
const labels: Record<AppRole, string> = {
|
||||||
|
student: 'Студент',
|
||||||
|
teacher: 'Преподаватель',
|
||||||
|
admin: 'Администратор',
|
||||||
|
}
|
||||||
|
const targets: Record<AppRole, string> = {
|
||||||
|
student: '/',
|
||||||
|
teacher: '/teacher',
|
||||||
|
admin: '/admin',
|
||||||
|
}
|
||||||
|
return auth.user.roles
|
||||||
|
.filter(role => role !== auth.user?.activeRole)
|
||||||
|
.map(role => ({ role, label: labels[role], to: targets[role] }))
|
||||||
|
})
|
||||||
|
|
||||||
|
function switchToRole(role: AppRole, to: string) {
|
||||||
|
if (auth.setActiveRole(role)) {
|
||||||
|
router.push(to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function isActive(to: string) {
|
function isActive(to: string) {
|
||||||
if (to === '/') return route.path === '/'
|
if (to === '/') return route.path === '/'
|
||||||
return route.path.startsWith(to) && to !== '/'
|
return route.path.startsWith(to) && to !== '/'
|
||||||
@@ -55,6 +79,16 @@ function isActive(to: string) {
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="sidebar-footer">
|
<div class="sidebar-footer">
|
||||||
|
<div class="role-switches" v-if="roleButtons.length">
|
||||||
|
<button
|
||||||
|
v-for="item in roleButtons"
|
||||||
|
:key="item.role"
|
||||||
|
class="role-switch-btn"
|
||||||
|
@click="switchToRole(item.role, item.to)"
|
||||||
|
>
|
||||||
|
Перейти: {{ item.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<button class="logout-btn" @click="auth.logout().then(() => router.push('/login'))">
|
<button class="logout-btn" @click="auth.logout().then(() => router.push('/login'))">
|
||||||
<AppIcon class="logout-icon" icon="logout" :size="16" />
|
<AppIcon class="logout-icon" icon="logout" :size="16" />
|
||||||
Выйти
|
Выйти
|
||||||
@@ -110,7 +144,21 @@ function isActive(to: string) {
|
|||||||
box-shadow: 0 2px 8px rgba(34,197,94,0.12);
|
box-shadow: 0 2px 8px rgba(34,197,94,0.12);
|
||||||
}
|
}
|
||||||
.nav-icon { flex-shrink: 0; color: currentColor; }
|
.nav-icon { flex-shrink: 0; color: currentColor; }
|
||||||
.sidebar-footer { padding: 10px 18px 8px; }
|
.sidebar-footer { padding: 10px 18px 8px; display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
.role-switches { display: flex; flex-direction: column; gap: 6px; }
|
||||||
|
.role-switch-btn {
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(34,197,94,0.08);
|
||||||
|
border: 1px solid rgba(34,197,94,0.2);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-primary-dark);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.role-switch-btn:hover { background: rgba(34,197,94,0.15); }
|
||||||
.logout-btn {
|
.logout-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: rgba(239,68,68,0.08);
|
background: rgba(239,68,68,0.08);
|
||||||
|
|||||||
@@ -21,13 +21,6 @@ const roleLabels: Record<string, string> = {
|
|||||||
|
|
||||||
const unreadCount = computed(() => userStore.unreadCount())
|
const unreadCount = computed(() => userStore.unreadCount())
|
||||||
|
|
||||||
function switchRole() {
|
|
||||||
auth.switchRole()
|
|
||||||
if (auth.user?.role === 'teacher') router.push('/teacher')
|
|
||||||
else if (auth.user?.role === 'admin') router.push('/admin')
|
|
||||||
else router.push('/')
|
|
||||||
}
|
|
||||||
|
|
||||||
function openProfile() {
|
function openProfile() {
|
||||||
router.push('/profile')
|
router.push('/profile')
|
||||||
}
|
}
|
||||||
@@ -47,9 +40,9 @@ function openProfile() {
|
|||||||
<div class="topbar-right">
|
<div class="topbar-right">
|
||||||
<CoinChip v-if="auth.user" :amount="auth.user.coins" />
|
<CoinChip v-if="auth.user" :amount="auth.user.coins" />
|
||||||
|
|
||||||
<button class="role-btn" @click="switchRole" v-if="auth.user">
|
<span class="role-chip" v-if="auth.user">
|
||||||
{{ roleLabels[auth.user.role] }} ↔
|
{{ roleLabels[auth.user.activeRole] }}
|
||||||
</button>
|
</span>
|
||||||
|
|
||||||
<button class="notif-btn" @click="$router.push('/notifications')">
|
<button class="notif-btn" @click="$router.push('/notifications')">
|
||||||
<AppIcon icon="bell" :size="18" />
|
<AppIcon icon="bell" :size="18" />
|
||||||
@@ -115,7 +108,7 @@ function openProfile() {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.role-btn {
|
.role-chip {
|
||||||
background: rgba(34,197,94,0.12);
|
background: rgba(34,197,94,0.12);
|
||||||
border: 1px solid rgba(34,197,94,0.3);
|
border: 1px solid rgba(34,197,94,0.3);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
@@ -123,11 +116,8 @@ function openProfile() {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--color-primary-dark);
|
color: var(--color-primary-dark);
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.role-btn:hover { background: rgba(34,197,94,0.2); }
|
|
||||||
.notif-btn {
|
.notif-btn {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: none;
|
background: none;
|
||||||
@@ -173,6 +163,6 @@ function openProfile() {
|
|||||||
.topbar-center { display: none; }
|
.topbar-center { display: none; }
|
||||||
.brand-name { display: none; }
|
.brand-name { display: none; }
|
||||||
.avatar-name { display: none; }
|
.avatar-name { display: none; }
|
||||||
.role-btn { display: none; }
|
.role-chip { display: none; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -39,16 +39,19 @@ const router = createRouter({
|
|||||||
|
|
||||||
router.beforeEach(async (to) => {
|
router.beforeEach(async (to) => {
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
|
const resolveDefaultRoute = () => {
|
||||||
|
if (auth.user?.activeRole === 'teacher') return '/teacher'
|
||||||
|
if (auth.user?.activeRole === 'admin') return '/admin'
|
||||||
|
return '/'
|
||||||
|
}
|
||||||
if (!auth.initialized && !to.meta.public) {
|
if (!auth.initialized && !to.meta.public) {
|
||||||
await auth.initialize()
|
await auth.initialize()
|
||||||
}
|
}
|
||||||
if (!to.meta.public && !auth.isAuthenticated) {
|
if (!to.meta.public && !auth.isAuthenticated) {
|
||||||
return '/login'
|
return '/login'
|
||||||
}
|
}
|
||||||
if (to.meta.role && auth.user && auth.user.role !== to.meta.role) {
|
if (to.meta.role && auth.user && !auth.user.roles.includes(to.meta.role as 'student' | 'teacher' | 'admin')) {
|
||||||
if (auth.user.role === 'teacher') return '/teacher'
|
return resolveDefaultRoute()
|
||||||
if (auth.user.role === 'admin') return '/admin'
|
|
||||||
return '/'
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
async function completeMicrosoftLogin(code: string, state: string | null) {
|
async function completeMicrosoftLogin(code: string, _state: string | null) {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
@@ -136,8 +136,10 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
user.value = nextUser
|
user.value = nextUser
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchRole() {
|
function setActiveRole(role: User['activeRole']) {
|
||||||
error.value = 'Смена роли доступна только через backend.'
|
if (!user.value || !user.value.roles.includes(role)) return false
|
||||||
|
user.value = { ...user.value, activeRole: role }
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -154,6 +156,6 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
logout,
|
logout,
|
||||||
clearSession,
|
clearSession,
|
||||||
setUser,
|
setUser,
|
||||||
switchRole,
|
setActiveRole,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ export interface User {
|
|||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
email: string
|
email: string
|
||||||
role: UserRole
|
roles: UserRole[]
|
||||||
|
activeRole: UserRole
|
||||||
avatar?: string
|
avatar?: string
|
||||||
institute?: string
|
institute?: string
|
||||||
department?: string
|
department?: string
|
||||||
|
|||||||
@@ -25,14 +25,20 @@ const columns = [
|
|||||||
|
|
||||||
const roleLabels = { Student: 'Студент', Teacher: 'Преподаватель', Admin: 'Администратор' } as const
|
const roleLabels = { Student: 'Студент', Teacher: 'Преподаватель', Admin: 'Администратор' } as const
|
||||||
const roleApi = { Студент: 'Student', Преподаватель: 'Teacher', Администратор: 'Admin' } as const
|
const roleApi = { Студент: 'Student', Преподаватель: 'Teacher', Администратор: 'Admin' } as const
|
||||||
|
type ApiUserRole = 'Student' | 'Teacher' | 'Admin'
|
||||||
|
const roleSetSequence: ApiUserRole[][] = [
|
||||||
|
['Student'],
|
||||||
|
['Student', 'Teacher'],
|
||||||
|
['Student', 'Teacher', 'Admin'],
|
||||||
|
]
|
||||||
|
|
||||||
const rows = computed(() =>
|
const rows = computed(() =>
|
||||||
users.value.map(user => ({
|
users.value.map(user => ({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
name: user.displayName || user.email,
|
name: user.displayName || user.email,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
role: roleLabels[user.role],
|
role: user.roles.map(role => roleLabels[role]).join(', '),
|
||||||
apiRole: user.role,
|
apiRoles: user.roles,
|
||||||
institute: 'ЮФУ',
|
institute: 'ЮФУ',
|
||||||
activity: user.isActive ? 'Активен' : 'Заблокирован',
|
activity: user.isActive ? 'Активен' : 'Заблокирован',
|
||||||
isActive: user.isActive,
|
isActive: user.isActive,
|
||||||
@@ -56,14 +62,24 @@ async function fetchUsers() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleActive(row: Record<string, any>) {
|
async function toggleActive(row: Record<string, unknown>) {
|
||||||
await usersApi.setActive(row.id, !row.isActive)
|
const id = Number(row.id)
|
||||||
|
const isActive = Boolean(row.isActive)
|
||||||
|
await usersApi.setActive(id, !isActive)
|
||||||
await fetchUsers()
|
await fetchUsers()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function promoteRole(row: Record<string, any>) {
|
async function promoteRole(row: Record<string, unknown>) {
|
||||||
const next = row.apiRole === 'Student' ? 'Teacher' : row.apiRole === 'Teacher' ? 'Admin' : 'Student'
|
const id = Number(row.id)
|
||||||
await usersApi.setRole(row.id, next)
|
const apiRoles = (Array.isArray(row.apiRoles) ? row.apiRoles : []) as ApiUserRole[]
|
||||||
|
const currentKey = [...new Set(apiRoles)].sort().join(',')
|
||||||
|
const currentIndex = roleSetSequence.findIndex(set => set.slice().sort().join(',') === currentKey)
|
||||||
|
const next: ApiUserRole[] = (
|
||||||
|
currentIndex >= 0
|
||||||
|
? roleSetSequence[(currentIndex + 1) % roleSetSequence.length]
|
||||||
|
: roleSetSequence[0]
|
||||||
|
) ?? ['Student']
|
||||||
|
await usersApi.setRole(id, next)
|
||||||
await fetchUsers()
|
await fetchUsers()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ onMounted(async () => {
|
|||||||
else throw new Error('Microsoft не вернул токен авторизации.')
|
else throw new Error('Microsoft не вернул токен авторизации.')
|
||||||
|
|
||||||
window.history.replaceState({}, document.title, window.location.pathname)
|
window.history.replaceState({}, document.title, window.location.pathname)
|
||||||
const role = auth.user?.role
|
const role = auth.user?.activeRole
|
||||||
if (role === 'teacher') await router.replace('/teacher')
|
if (role === 'teacher') await router.replace('/teacher')
|
||||||
else if (role === 'admin') await router.replace('/admin')
|
else if (role === 'admin') await router.replace('/admin')
|
||||||
else await router.replace('/')
|
else await router.replace('/')
|
||||||
|
|||||||
Reference in New Issue
Block a user