feat: мультироль
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Successful in 9s
🚀 Create and publish a Docker image / Build & publish backend image (push) Successful in 2m6s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Successful in 26s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Successful in 6s
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Successful in 9s
🚀 Create and publish a Docker image / Build & publish backend image (push) Successful in 2m6s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Successful in 26s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Successful in 6s
This commit is contained in:
@@ -10,6 +10,7 @@ public class AppDbContext : DbContext
|
||||
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
|
||||
|
||||
public DbSet<User> Users { get; set; } = null!;
|
||||
public DbSet<UserRoleAssignment> UserRoles { get; set; } = null!;
|
||||
public DbSet<StudentProfile> StudentProfiles { get; set; } = null!;
|
||||
public DbSet<TeacherProfile> TeacherProfiles { 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.DisplayName).HasColumnName("display_name").HasMaxLength(255);
|
||||
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.MicrosoftId).HasColumnName("microsoft_id").HasMaxLength(255);
|
||||
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.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
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.0")
|
||||
.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" });
|
||||
@@ -667,10 +667,6 @@ namespace UniVerse.Infrastructure.Migrations
|
||||
.HasColumnType("character varying(255)")
|
||||
.HasColumnName("microsoft_id");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("role");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
@@ -725,6 +721,21 @@ namespace UniVerse.Infrastructure.Migrations
|
||||
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")
|
||||
@@ -894,6 +905,17 @@ namespace UniVerse.Infrastructure.Migrations
|
||||
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");
|
||||
@@ -940,6 +962,8 @@ namespace UniVerse.Infrastructure.Migrations
|
||||
|
||||
b.Navigation("Reviews");
|
||||
|
||||
b.Navigation("Roles");
|
||||
|
||||
b.Navigation("StudentProfile");
|
||||
|
||||
b.Navigation("TeacherProfile");
|
||||
|
||||
@@ -85,20 +85,21 @@ public class AuthService : IAuthService
|
||||
throw new UnauthorizedException("Email не найден в токене Microsoft.");
|
||||
|
||||
// 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)
|
||||
{
|
||||
user = new User
|
||||
{
|
||||
Email = email,
|
||||
DisplayName = name ?? email.Split('@')[0],
|
||||
Role = UserRole.Student, // Default role
|
||||
IsActive = true
|
||||
};
|
||||
_db.Users.Add(user);
|
||||
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 });
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
@@ -107,6 +108,13 @@ public class AuthService : IAuthService
|
||||
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 refreshToken = await GenerateRefreshTokenAsync(user.Id);
|
||||
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)
|
||||
{
|
||||
@@ -131,25 +142,23 @@ public class AuthService : IAuthService
|
||||
{
|
||||
Email = email,
|
||||
DisplayName = displayName ?? email.Split('@')[0],
|
||||
Role = role,
|
||||
IsActive = true
|
||||
};
|
||||
_db.Users.Add(user);
|
||||
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)
|
||||
throw new ForbiddenException("Аккаунт деактивирован.");
|
||||
|
||||
@@ -171,6 +180,7 @@ public class AuthService : IAuthService
|
||||
{
|
||||
var token = await _db.RefreshTokens
|
||||
.Include(rt => rt.User)
|
||||
.ThenInclude(u => u.Roles)
|
||||
.FirstOrDefaultAsync(rt => rt.Token == refreshToken);
|
||||
|
||||
if (token == null || !token.IsActive)
|
||||
@@ -207,7 +217,9 @@ public class AuthService : IAuthService
|
||||
|
||||
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);
|
||||
return user.ToDto(_gamification.CalculateLevel(user.Xp));
|
||||
}
|
||||
@@ -245,13 +257,15 @@ public class AuthService : IAuthService
|
||||
Encoding.UTF8.GetBytes(_config["Jwt:Secret"]!));
|
||||
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
var claims = new[]
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
|
||||
new Claim(JwtRegisteredClaimNames.Email, user.Email),
|
||||
new Claim(ClaimTypes.Role, user.Role.ToString()),
|
||||
new Claim("display_name", user.DisplayName ?? "")
|
||||
new(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
|
||||
new(JwtRegisteredClaimNames.Email, user.Email),
|
||||
new("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(
|
||||
issuer: _config["Jwt:Issuer"],
|
||||
@@ -264,6 +278,23 @@ public class AuthService : IAuthService
|
||||
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)
|
||||
{
|
||||
var randomBytes = RandomNumberGenerator.GetBytes(64);
|
||||
|
||||
@@ -22,14 +22,18 @@ public class UserService : IUserService
|
||||
|
||||
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);
|
||||
return user.ToDto(_gamification.CalculateLevel(user.Xp));
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (request.DisplayName != null) user.DisplayName = request.DisplayName;
|
||||
@@ -68,8 +72,15 @@ public class UserService : IUserService
|
||||
(u.DisplayName != null && u.DisplayName.ToLower().Contains(search)));
|
||||
}
|
||||
|
||||
query = query.Include(u => u.Roles);
|
||||
|
||||
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)
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
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;
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
@@ -103,4 +130,21 @@ public class UserService : IUserService
|
||||
user.UpdatedAt = DateTime.UtcNow;
|
||||
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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user