feat: перелопатил синхронизацию преподавателей
Backend CI / build-and-test (push) Failing after 13m11s
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Failing after 10m12s
Frontend CI / build-and-check (push) Failing after 16m9s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Failing after 14m6s
🚀 Create and publish a Docker image / Build & publish backend image (push) Failing after 14m58s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Failing after 14m58s
Backend CI / build-and-test (push) Failing after 13m11s
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Failing after 10m12s
Frontend CI / build-and-check (push) Failing after 16m9s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Failing after 14m6s
🚀 Create and publish a Docker image / Build & publish backend image (push) Failing after 14m58s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Failing after 14m58s
This commit is contained in:
@@ -24,5 +24,6 @@ public class TeacherProfileConfiguration : IEntityTypeConfiguration<TeacherProfi
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasIndex(t => t.UserId).IsUnique();
|
||||
builder.HasIndex(t => t.ModeusId).IsUnique().HasFilter("modeus_id IS NOT NULL");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Identity.Client;
|
||||
using UniVerse.Application.Interfaces;
|
||||
using UniVerse.Domain.Exceptions;
|
||||
|
||||
namespace UniVerse.Infrastructure.ExternalServices;
|
||||
|
||||
public class MicrosoftAuthClient : IMicrosoftAuthClient
|
||||
{
|
||||
private readonly IConfiguration _config;
|
||||
|
||||
public MicrosoftAuthClient(IConfiguration config)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public async Task<MicrosoftTokenResult> ExchangeAuthorizationCodeAsync(
|
||||
string authorizationCode,
|
||||
string redirectUri,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tenantId = _config["AzureAd:TenantId"];
|
||||
var clientId = _config["AzureAd:ClientId"];
|
||||
var clientSecret = _config["AzureAd:ClientSecret"];
|
||||
var instance = _config["AzureAd:Instance"] ?? "https://login.microsoftonline.com/";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tenantId)
|
||||
|| string.IsNullOrWhiteSpace(clientId)
|
||||
|| string.IsNullOrWhiteSpace(clientSecret))
|
||||
throw new UnauthorizedException("Аутентификация Microsoft не настроена (AzureAd:TenantId/ClientId/ClientSecret).");
|
||||
|
||||
var authority = $"{instance.TrimEnd('/')}/{tenantId}";
|
||||
|
||||
var app = ConfidentialClientApplicationBuilder.Create(clientId)
|
||||
.WithClientSecret(clientSecret)
|
||||
.WithAuthority(new Uri(authority))
|
||||
.WithRedirectUri(redirectUri)
|
||||
.Build();
|
||||
|
||||
try
|
||||
{
|
||||
var result = await app.AcquireTokenByAuthorizationCode(["User.Read"], authorizationCode)
|
||||
.ExecuteAsync(cancellationToken);
|
||||
|
||||
return new MicrosoftTokenResult(result.IdToken);
|
||||
}
|
||||
catch (MsalException ex)
|
||||
{
|
||||
throw new UnauthorizedException($"Ошибка аутентификации Microsoft: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -142,4 +144,20 @@ public class ModeusApiClient : IModeusApiClient
|
||||
$"/api/schedule/searchemployee?fullname={Uri.EscapeDataString(fullname)}");
|
||||
return response ?? new();
|
||||
}
|
||||
|
||||
public async Task<string?> GetSubIdByFullNameAsync(string fullname, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var request = new HttpRequestMessage(
|
||||
HttpMethod.Get,
|
||||
$"/api/universe/subid?fullname={Uri.EscapeDataString(fullname)}");
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/plain"));
|
||||
|
||||
using var response = await _http.SendAsync(request, cancellationToken);
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
return null;
|
||||
|
||||
await EnsureSuccessAsync(response, "Universe user sub lookup", $"fullname={fullname}");
|
||||
var body = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
return string.IsNullOrWhiteSpace(body) ? null : body.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+1143
File diff suppressed because it is too large
Load Diff
+29
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace UniVerse.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class UniqueTeacherProfileModeusId : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_teacher_profiles_modeus_id",
|
||||
table: "teacher_profiles",
|
||||
column: "modeus_id",
|
||||
unique: true,
|
||||
filter: "modeus_id IS NOT NULL");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_teacher_profiles_modeus_id",
|
||||
table: "teacher_profiles");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -515,16 +515,16 @@ namespace UniVerse.Infrastructure.Migrations
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("lecture_id");
|
||||
|
||||
b.Property<string>("LlmRawOutput")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("llm_raw_output");
|
||||
|
||||
b.Property<int>("LlmStatus")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("llm_status");
|
||||
|
||||
b.Property<string>("LlmRawOutput")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("llm_raw_output");
|
||||
|
||||
b.PrimitiveCollection<string[]>("LlmTags")
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("llm_tags");
|
||||
@@ -710,6 +710,10 @@ namespace UniVerse.Infrastructure.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ModeusId")
|
||||
.IsUnique()
|
||||
.HasFilter("modeus_id IS NOT NULL");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.IsUnique();
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using Microsoft.Identity.Client;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
@@ -23,6 +22,7 @@ public class AuthService : IAuthService
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly IConfiguration _config;
|
||||
private readonly IMicrosoftAuthClient _microsoftAuth;
|
||||
private readonly IGamificationService _gamification;
|
||||
private readonly INotificationService _notifications;
|
||||
private readonly ILogger<AuthService> _logger;
|
||||
@@ -30,12 +30,14 @@ public class AuthService : IAuthService
|
||||
public AuthService(
|
||||
AppDbContext db,
|
||||
IConfiguration config,
|
||||
IMicrosoftAuthClient microsoftAuth,
|
||||
IGamificationService gamification,
|
||||
INotificationService notifications,
|
||||
ILogger<AuthService> logger)
|
||||
{
|
||||
_db = db;
|
||||
_config = config;
|
||||
_microsoftAuth = microsoftAuth;
|
||||
_gamification = gamification;
|
||||
_notifications = notifications;
|
||||
_logger = logger;
|
||||
@@ -43,36 +45,10 @@ public class AuthService : IAuthService
|
||||
|
||||
public async Task<AuthResult> LoginWithMicrosoftAsync(string authorizationCode, string? redirectUri = null, string? ipAddress = null)
|
||||
{
|
||||
var tenantId = _config["AzureAd:TenantId"];
|
||||
var clientId = _config["AzureAd:ClientId"];
|
||||
var clientSecret = _config["AzureAd:ClientSecret"];
|
||||
var instance = _config["AzureAd:Instance"] ?? "https://login.microsoftonline.com/";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tenantId) || string.IsNullOrWhiteSpace(clientId) || string.IsNullOrWhiteSpace(clientSecret))
|
||||
throw new UnauthorizedException("Аутентификация Microsoft не настроена (AzureAd:TenantId/ClientId/ClientSecret).");
|
||||
|
||||
var effectiveRedirectUri = redirectUri
|
||||
?? _config["AzureAd:RedirectUri"]
|
||||
?? "http://localhost:5173/auth/callback";
|
||||
|
||||
var authority = $"{instance.TrimEnd('/')}/{tenantId}";
|
||||
|
||||
var app = ConfidentialClientApplicationBuilder.Create(clientId)
|
||||
.WithClientSecret(clientSecret)
|
||||
.WithAuthority(new Uri(authority))
|
||||
.WithRedirectUri(effectiveRedirectUri)
|
||||
.Build();
|
||||
|
||||
AuthenticationResult result;
|
||||
try
|
||||
{
|
||||
result = await app.AcquireTokenByAuthorizationCode(new[] { "User.Read" }, authorizationCode)
|
||||
.ExecuteAsync();
|
||||
}
|
||||
catch (MsalException ex)
|
||||
{
|
||||
throw new UnauthorizedException($"Ошибка аутентификации Microsoft: {ex.Message}");
|
||||
}
|
||||
var result = await _microsoftAuth.ExchangeAuthorizationCodeAsync(authorizationCode, effectiveRedirectUri);
|
||||
|
||||
// Parse claims directly from the ID token provided by Microsoft
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
@@ -80,13 +56,21 @@ public class AuthService : IAuthService
|
||||
|
||||
var email = idToken.Claims.FirstOrDefault(c => c.Type == "preferred_username" || c.Type == "email" || c.Type == ClaimTypes.Upn)?.Value;
|
||||
var name = idToken.Claims.FirstOrDefault(c => c.Type == "name")?.Value;
|
||||
var microsoftSub = idToken.Claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Sub || c.Type == "sub")?.Value;
|
||||
|
||||
if (string.IsNullOrEmpty(email))
|
||||
throw new UnauthorizedException("Email не найден в токене Microsoft.");
|
||||
if (string.IsNullOrWhiteSpace(microsoftSub))
|
||||
throw new UnauthorizedException("Sub ID не найден в токене Microsoft.");
|
||||
|
||||
// Automatically provision user
|
||||
var user = await _db.Users
|
||||
.Include(u => u.Roles)
|
||||
.Include(u => u.TeacherProfile)
|
||||
.FirstOrDefaultAsync(u => u.MicrosoftId == microsoftSub);
|
||||
user ??= await _db.Users
|
||||
.Include(u => u.Roles)
|
||||
.Include(u => u.TeacherProfile)
|
||||
.FirstOrDefaultAsync(u => u.Email == email);
|
||||
if (user == null)
|
||||
{
|
||||
@@ -94,6 +78,7 @@ public class AuthService : IAuthService
|
||||
{
|
||||
Email = email,
|
||||
DisplayName = name ?? email.Split('@')[0],
|
||||
MicrosoftId = microsoftSub,
|
||||
IsActive = true
|
||||
};
|
||||
_db.Users.Add(user);
|
||||
@@ -107,6 +92,14 @@ public class AuthService : IAuthService
|
||||
{
|
||||
throw new ForbiddenException("Аккаунт деактивирован.");
|
||||
}
|
||||
else
|
||||
{
|
||||
user.Email = email;
|
||||
user.DisplayName = name ?? user.DisplayName ?? email.Split('@')[0];
|
||||
user.MicrosoftId = microsoftSub;
|
||||
user.UpdatedAt = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
if (user.Roles.Count == 0)
|
||||
{
|
||||
|
||||
@@ -82,13 +82,14 @@ public class LectureService : ILectureService
|
||||
return full.ToDto();
|
||||
}
|
||||
|
||||
public async Task<LectureDto> UpdateAsync(int id, UpdateLectureRequest req)
|
||||
public async Task<LectureDto> UpdateAsync(int id, UpdateLectureRequest req, int currentUserId, bool isAdmin = false)
|
||||
{
|
||||
var lecture = await _db.Lectures
|
||||
.Include(l => l.Location)
|
||||
.Include(l => l.Enrollments)
|
||||
.ThenInclude(e => e.User)
|
||||
.FirstOrDefaultAsync(l => l.Id == id) ?? throw new NotFoundException("Lecture", id);
|
||||
EnsureTeacherOwnsLecture(lecture, currentUserId, isAdmin);
|
||||
lecture.TeacherId = req.TeacherId; lecture.LocationId = req.LocationId;
|
||||
lecture.Title = req.Title; lecture.Description = req.Description;
|
||||
lecture.Format = req.Format; lecture.StartsAt = req.StartsAt; lecture.EndsAt = req.EndsAt;
|
||||
@@ -150,8 +151,9 @@ public class LectureService : ILectureService
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task MarkAttendanceAsync(int lectureId, int userId, bool attended)
|
||||
public async Task MarkAttendanceAsync(int lectureId, int userId, bool attended, int currentUserId, bool isAdmin = false)
|
||||
{
|
||||
await EnsureTeacherOwnsLectureAsync(lectureId, currentUserId, isAdmin);
|
||||
var enrollment = await _db.LectureEnrollments
|
||||
.FirstOrDefaultAsync(e => e.LectureId == lectureId && e.UserId == userId)
|
||||
?? throw new NotFoundException("Enrollment not found.");
|
||||
@@ -161,8 +163,9 @@ public class LectureService : ILectureService
|
||||
await _gamification.CheckAndAwardAchievementsAsync(userId);
|
||||
}
|
||||
|
||||
public async Task<PagedResult<EnrollmentDto>> GetEnrollmentsAsync(int lectureId, PaginationRequest pagination)
|
||||
public async Task<PagedResult<EnrollmentDto>> GetEnrollmentsAsync(int lectureId, PaginationRequest pagination, int currentUserId, bool isAdmin = false)
|
||||
{
|
||||
await EnsureTeacherOwnsLectureAsync(lectureId, currentUserId, isAdmin);
|
||||
var query = _db.LectureEnrollments.Include(e => e.User)
|
||||
.Where(e => e.LectureId == lectureId);
|
||||
var total = await query.CountAsync();
|
||||
@@ -171,6 +174,22 @@ public class LectureService : ILectureService
|
||||
return PagedResult<EnrollmentDto>.Create(items.Select(e => e.ToDto()).ToList(), total, pagination.Page, pagination.PageSize);
|
||||
}
|
||||
|
||||
private async Task EnsureTeacherOwnsLectureAsync(int lectureId, int currentUserId, bool isAdmin)
|
||||
{
|
||||
if (isAdmin)
|
||||
return;
|
||||
|
||||
var lecture = await _db.Lectures.FirstOrDefaultAsync(l => l.Id == lectureId)
|
||||
?? throw new NotFoundException("Lecture", lectureId);
|
||||
EnsureTeacherOwnsLecture(lecture, currentUserId, isAdmin: false);
|
||||
}
|
||||
|
||||
private static void EnsureTeacherOwnsLecture(Lecture lecture, int currentUserId, bool isAdmin)
|
||||
{
|
||||
if (!isAdmin && lecture.TeacherId != currentUserId)
|
||||
throw new ForbiddenException("Teacher can access only their own lectures.");
|
||||
}
|
||||
|
||||
private async Task RescheduleLectureRemindersAsync(Lecture lecture)
|
||||
{
|
||||
foreach (var enrollment in lecture.Enrollments)
|
||||
|
||||
@@ -75,8 +75,23 @@ public class ReviewService : IReviewService
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<PagedResult<ReviewDto>> GetByLectureAsync(int lectureId, PaginationRequest pagination)
|
||||
public async Task<PagedResult<ReviewDto>> GetByLectureAsync(
|
||||
int lectureId,
|
||||
PaginationRequest pagination,
|
||||
int? currentUserId = null,
|
||||
bool isAdmin = false)
|
||||
{
|
||||
if (!isAdmin)
|
||||
{
|
||||
if (!currentUserId.HasValue)
|
||||
throw new ForbiddenException();
|
||||
|
||||
var lecture = await _db.Lectures.FirstOrDefaultAsync(l => l.Id == lectureId)
|
||||
?? throw new NotFoundException("Lecture", lectureId);
|
||||
if (lecture.TeacherId != currentUserId.Value)
|
||||
throw new ForbiddenException("Teacher can access reviews only for their own lectures.");
|
||||
}
|
||||
|
||||
var query = BaseQuery().Where(r => r.LectureId == lectureId);
|
||||
var total = await query.CountAsync();
|
||||
var items = await query.OrderByDescending(r => r.CreatedAt)
|
||||
|
||||
@@ -218,15 +218,42 @@ public class ScheduleSyncService : IScheduleSyncService
|
||||
.Include(profile => profile.User)
|
||||
.ThenInclude(user => user.Roles)
|
||||
.FirstOrDefaultAsync(profile => profile.ModeusId == personId);
|
||||
var subId = existingProfile?.User.MicrosoftId;
|
||||
if (string.IsNullOrWhiteSpace(subId))
|
||||
subId = await TryGetTeacherSubIdAsync(fullName);
|
||||
|
||||
User? ssoUser = null;
|
||||
if (!string.IsNullOrWhiteSpace(subId))
|
||||
{
|
||||
ssoUser = await _db.Users
|
||||
.Include(item => item.Roles)
|
||||
.Include(item => item.TeacherProfile)
|
||||
.FirstOrDefaultAsync(item => item.MicrosoftId == subId);
|
||||
}
|
||||
|
||||
if (existingProfile != null && ssoUser != null && existingProfile.UserId != ssoUser.Id)
|
||||
return await MergeTeacherPlaceholderAsync(existingProfile, ssoUser, fullName, subId);
|
||||
|
||||
if (existingProfile != null)
|
||||
{
|
||||
existingProfile.User.DisplayName = fullName;
|
||||
if (!string.IsNullOrWhiteSpace(subId))
|
||||
existingProfile.User.MicrosoftId = subId;
|
||||
existingProfile.User.UpdatedAt = DateTime.UtcNow;
|
||||
EnsureTeacherRole(existingProfile.User);
|
||||
return existingProfile.User;
|
||||
}
|
||||
|
||||
if (ssoUser != null)
|
||||
{
|
||||
ssoUser.DisplayName = fullName;
|
||||
ssoUser.UpdatedAt = DateTime.UtcNow;
|
||||
EnsureTeacherRole(ssoUser);
|
||||
EnsureTeacherProfile(ssoUser, personId);
|
||||
await _db.SaveChangesAsync();
|
||||
return ssoUser;
|
||||
}
|
||||
|
||||
var email = BuildModeusTeacherEmail(personId);
|
||||
var user = await _db.Users
|
||||
.Include(item => item.Roles)
|
||||
@@ -239,6 +266,7 @@ public class ScheduleSyncService : IScheduleSyncService
|
||||
{
|
||||
Email = email,
|
||||
DisplayName = fullName,
|
||||
MicrosoftId = subId,
|
||||
IsActive = true,
|
||||
TeacherProfile = new TeacherProfile { ModeusId = personId }
|
||||
};
|
||||
@@ -249,6 +277,8 @@ public class ScheduleSyncService : IScheduleSyncService
|
||||
}
|
||||
|
||||
user.DisplayName = fullName;
|
||||
if (!string.IsNullOrWhiteSpace(subId))
|
||||
user.MicrosoftId = subId;
|
||||
user.UpdatedAt = DateTime.UtcNow;
|
||||
if (user.TeacherProfile == null)
|
||||
user.TeacherProfile = new TeacherProfile { UserId = user.Id, ModeusId = personId };
|
||||
@@ -261,6 +291,76 @@ public class ScheduleSyncService : IScheduleSyncService
|
||||
return user;
|
||||
}
|
||||
|
||||
private async Task<string?> TryGetTeacherSubIdAsync(string fullName)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _modeus.GetSubIdByFullNameAsync(fullName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not resolve SSO sub id for teacher {TeacherFullName}. A placeholder teacher will be used until a future sync succeeds.", fullName);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<User> MergeTeacherPlaceholderAsync(
|
||||
TeacherProfile placeholderProfile,
|
||||
User targetUser,
|
||||
string fullName,
|
||||
string? subId)
|
||||
{
|
||||
var placeholderUser = placeholderProfile.User;
|
||||
|
||||
var lectures = await _db.Lectures
|
||||
.Where(lecture => lecture.TeacherId == placeholderUser.Id)
|
||||
.ToListAsync();
|
||||
foreach (var lecture in lectures)
|
||||
lecture.TeacherId = targetUser.Id;
|
||||
|
||||
targetUser.DisplayName = fullName;
|
||||
if (!string.IsNullOrWhiteSpace(subId))
|
||||
targetUser.MicrosoftId = subId;
|
||||
targetUser.UpdatedAt = DateTime.UtcNow;
|
||||
EnsureTeacherRole(targetUser);
|
||||
|
||||
if (targetUser.TeacherProfile == null)
|
||||
{
|
||||
placeholderProfile.UserId = targetUser.Id;
|
||||
placeholderProfile.User = targetUser;
|
||||
targetUser.TeacherProfile = placeholderProfile;
|
||||
placeholderUser.TeacherProfile = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
targetUser.TeacherProfile.ModeusId = placeholderProfile.ModeusId;
|
||||
_db.TeacherProfiles.Remove(placeholderProfile);
|
||||
}
|
||||
|
||||
if (await CanDeletePlaceholderUserAsync(placeholderUser.Id))
|
||||
_db.Users.Remove(placeholderUser);
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
return targetUser;
|
||||
}
|
||||
|
||||
private async Task<bool> CanDeletePlaceholderUserAsync(int userId) =>
|
||||
!await _db.StudentProfiles.AnyAsync(profile => profile.UserId == userId)
|
||||
&& !await _db.RefreshTokens.AnyAsync(token => token.UserId == userId)
|
||||
&& !await _db.LectureEnrollments.AnyAsync(enrollment => enrollment.UserId == userId)
|
||||
&& !await _db.Reviews.AnyAsync(review => review.UserId == userId)
|
||||
&& !await _db.UserAchievements.AnyAsync(achievement => achievement.UserId == userId)
|
||||
&& !await _db.CoinTransactions.AnyAsync(transaction => transaction.UserId == userId)
|
||||
&& !await _db.UserNotifications.AnyAsync(notification => notification.UserId == userId);
|
||||
|
||||
private static void EnsureTeacherProfile(User user, string modeusId)
|
||||
{
|
||||
if (user.TeacherProfile == null)
|
||||
user.TeacherProfile = new TeacherProfile { UserId = user.Id, ModeusId = modeusId };
|
||||
else
|
||||
user.TeacherProfile.ModeusId = modeusId;
|
||||
}
|
||||
|
||||
private static void EnsureTeacherRole(User user)
|
||||
{
|
||||
if (!user.Roles.Any(role => role.Role == UserRole.Teacher))
|
||||
|
||||
Reference in New Issue
Block a user