Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,28 @@ jobs:
$result = Join-Path $exe.DirectoryName 'diagnostics/result.txt'
if ($p.ExitCode -ne 0 -or (Get-Content $result) -ne 'OK') { Get-Content (Join-Path $exe.DirectoryName 'self-test-error.txt') -ErrorAction SilentlyContinue; exit 1 }

firebase:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
- uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: '21'
- uses: actions/setup-node@v5
with:
node-version: '24'
- name: Firebase Emulator tests
env:
FIREBASE_AUTH_EMULATOR_HOST: 127.0.0.1:9099
FIRESTORE_EMULATOR_HOST: 127.0.0.1:8080
run: >
npx --yes firebase-tools@14 emulators:exec --only auth,firestore --project demo-bistudent
"dotnet test BiStudent.Tests -- --filter-class BiStudent.Tests.FirebaseEmulatorTests --filter-class BiStudent.Tests.FirebaseSyncScenarioTests"

docker:
runs-on: ubuntu-24.04
needs: build-test
Expand Down
1 change: 1 addition & 0 deletions BiStudent.Client.Tests/PagesSmokeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ [new AtRiskStudentDto(101, "Абдиев Артур", "23-ПО-103-4р-1", 62.5,
server.On(HttpMethod.Get, "api/report", _ => FakeServer.Json(report));
server.On(HttpMethod.Get, "api/settings/automation", _ => FakeServer.Json(new AutomationSettingsDto(6, 20, 3)));
server.On(HttpMethod.Get, "api/recognition/status", _ => FakeServer.Json(new EnrollResultDto(true, "Распознавание готово.")));
server.On(HttpMethod.Get, "api/sync/status", _ => FakeServer.Json(new SyncStatusDto(true, "Ok", DateTimeOffset.Now, 0, null)));
server.On(HttpMethod.Get, "api/schedule/day", _ => FakeServer.Json(new List<LessonDto> { lesson, later }));
server.On(HttpMethod.Get, "api/schedule/week", _ => FakeServer.Json(new List<LessonDto> { lesson, later }));
server.On(HttpMethod.Get, "api/schedule/parity", _ => FakeServer.Json(WeekParity.Odd));
Expand Down
2 changes: 2 additions & 0 deletions BiStudent.Core/Contracts/Contracts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public sealed record TokenResponse(string AccessToken, long ExpiresIn, string? R
public sealed record TeacherDto(int Id, string FullName, string Login, bool IsAdmin = false, bool MustChangePassword = false, int PendingFaceSubmissions = 0);
public sealed record ChangePasswordRequest(string CurrentPassword, string NewPassword);

public sealed record SyncStatusDto(bool Enabled, string State, DateTimeOffset? LastSuccessAt, int Pending, string? Error);

public sealed record TeacherAccountDto(int Id, string Login, string FullName, bool IsAdmin, bool IsActive, bool MustChangePassword, DateTime CreatedAt, DateTime? LastLoginAt);
public sealed record SaveTeacherRequest(string Login, string FullName, bool IsAdmin, bool IsActive);
public sealed record TemporaryPasswordDto(string Login, string Password);
Expand Down
12 changes: 12 additions & 0 deletions BiStudent.Server/Data/AppDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options) : DbCon
public DbSet<EnrollmentCode> EnrollmentCodes => Set<EnrollmentCode>();
public DbSet<FaceSubmission> FaceSubmissions => Set<FaceSubmission>();
public DbSet<FaceSubmissionImage> FaceSubmissionImages => Set<FaceSubmissionImage>();
public DbSet<SyncLink> SyncLinks => Set<SyncLink>();
public DbSet<SyncOutboxEntry> SyncOutbox => Set<SyncOutboxEntry>();
public DbSet<SyncAlias> SyncAliases => Set<SyncAlias>();

/// Идёт применение изменений из Firestore: не ставить их в очередь и не добавлять членство автоматически.
public bool ApplyingRemote { get; set; }

public IQueryable<GroupMembership> MembershipsOn(DateOnly date) =>
GroupMemberships.Where(x => x.From <= date && (x.To == null || x.To >= date));
Expand All @@ -30,6 +36,7 @@ public IQueryable<GroupMembership> MembershipsDuring(DateOnly from, DateOnly to)

private void AddInitialMemberships()
{
if (ApplyingRemote) return;
foreach (var entry in ChangeTracker.Entries<Student>().Where(x => x.State == EntityState.Added))
{
var student = entry.Entity;
Expand Down Expand Up @@ -61,6 +68,11 @@ public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, Cance
protected override void OnModelCreating(ModelBuilder model)
{
model.Entity<Teacher>().HasIndex(x => x.Login).IsUnique();
model.Entity<Teacher>().HasIndex(x => x.FirebaseUid).IsUnique();
model.Entity<SyncLink>().HasIndex(x => new { x.Table, x.LocalId }).IsUnique();
model.Entity<SyncLink>().HasIndex(x => new { x.Table, x.CloudId }).IsUnique();
model.Entity<SyncAlias>().HasIndex(x => new { x.Table, x.CloudId }).IsUnique();
model.Entity<SyncOutboxEntry>().ToTable("SyncOutbox").HasIndex(x => new { x.Table, x.LocalId });
model.Entity<Group>().HasIndex(x => x.Name).IsUnique();
model.Entity<AppSetting>().HasKey(x => x.Key);
model.Entity<SlotCancellation>().HasIndex(x => new { x.SlotId, x.Date }).IsUnique();
Expand Down
4 changes: 4 additions & 0 deletions BiStudent.Server/Data/Entities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ public sealed class Teacher
public string SecurityStamp { get; set; } = Guid.NewGuid().ToString("N");
public DateTime CreatedAt { get; set; } = DateTime.Now;
public DateTime? LastLoginAt { get; set; }
// Учётная запись Firebase Authentication, если она уже связана с этим преподавателем.
public string? FirebaseUid { get; set; }
// Секрет для первой привязки старой учётки к Firebase; хранится только локально, в облаке — его SHA-256.
public string? ClaimToken { get; set; }
}

public sealed class Group
Expand Down
Loading