From 31800ecedae517159b6419b0ff47525c8227113d Mon Sep 17 00:00:00 2001 From: roman Date: Thu, 11 Jun 2026 19:33:03 +0200 Subject: [PATCH] user administration --- .../Controllers/AdminController.cs | 176 +++++++ .../Controllers/AuthController.cs | 4 +- CalendarReminder.Api/Data/AppDbContext.cs | 12 + ...172823_AddFamilyAndAdminFields.Designer.cs | 243 ++++++++++ .../20260611172823_AddFamilyAndAdminFields.cs | 96 ++++ .../Migrations/AppDbContextModelSnapshot.cs | 47 ++ CalendarReminder.Api/Models/AuthModels.cs | 2 +- CalendarReminder.Api/Program.cs | 5 +- CalendarReminder.Api/Services/TokenService.cs | 4 +- CalendarReminder.Api/wwwroot/index.html | 438 +++++++++++++++++- CalendarReminder.Domain/Entities/Family.cs | 8 + CalendarReminder.Domain/Entities/User.cs | 5 + 12 files changed, 1031 insertions(+), 9 deletions(-) create mode 100644 CalendarReminder.Api/Controllers/AdminController.cs create mode 100644 CalendarReminder.Api/Migrations/20260611172823_AddFamilyAndAdminFields.Designer.cs create mode 100644 CalendarReminder.Api/Migrations/20260611172823_AddFamilyAndAdminFields.cs create mode 100644 CalendarReminder.Domain/Entities/Family.cs diff --git a/CalendarReminder.Api/Controllers/AdminController.cs b/CalendarReminder.Api/Controllers/AdminController.cs new file mode 100644 index 0000000..58fe469 --- /dev/null +++ b/CalendarReminder.Api/Controllers/AdminController.cs @@ -0,0 +1,176 @@ +using CalendarReminder.Api.Data; +using CalendarReminder.Api.Services; +using CalendarReminder.Domain.Entities; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace CalendarReminder.Api.Controllers; + +[Authorize] +[ApiController] +[Route("api/admin")] +public class AdminController : ControllerBase +{ + private readonly AppDbContext _db; + private readonly EmailService _email; + + public AdminController(AppDbContext db, EmailService email) + { + _db = db; + _email = email; + } + + private bool IsAdmin => User.FindFirst("isAdmin")?.Value == "true"; + + // ══════════════════════════════ + // RODINY + // ══════════════════════════════ + + [HttpGet("families")] + public async Task GetFamilies() + { + if (!IsAdmin) return Forbid(); + var families = await _db.Families + .Include(f => f.Users) + .Select(f => new { f.Id, f.Name, MemberCount = f.Users.Count }) + .ToListAsync(); + return Ok(families); + } + + [HttpPost("families")] + public async Task CreateFamily([FromBody] FamilyRequest req) + { + if (!IsAdmin) return Forbid(); + if (await _db.Families.AnyAsync(f => f.Name == req.Name)) + return BadRequest("Rodina s týmto názvom už existuje."); + var family = new Family { Name = req.Name }; + _db.Families.Add(family); + await _db.SaveChangesAsync(); + return Ok(new { family.Id, family.Name, MemberCount = 0 }); + } + + [HttpPut("families/{id}")] + public async Task UpdateFamily(int id, [FromBody] FamilyRequest req) + { + if (!IsAdmin) return Forbid(); + var family = await _db.Families.FindAsync(id); + if (family is null) return NotFound(); + if (await _db.Families.AnyAsync(f => f.Name == req.Name && f.Id != id)) + return BadRequest("Názov je obsadený."); + family.Name = req.Name; + await _db.SaveChangesAsync(); + return Ok(family); + } + + [HttpDelete("families/{id}")] + public async Task DeleteFamily(int id) + { + if (!IsAdmin) return Forbid(); + var family = await _db.Families.Include(f => f.Users).FirstOrDefaultAsync(f => f.Id == id); + if (family is null) return NotFound(); + if (family.Users.Any()) return BadRequest("Rodina má členov. Najprv presun alebo odstraň používateľov."); + _db.Families.Remove(family); + await _db.SaveChangesAsync(); + return NoContent(); + } + + // ══════════════════════════════ + // POUŽÍVATELIA + // ══════════════════════════════ + + [HttpGet("users")] + public async Task GetUsers() + { + if (!IsAdmin) return Forbid(); + var users = await _db.Users + .Include(u => u.Family) + .Select(u => new { + u.Id, u.Username, u.Email, u.IsAdmin, + u.MustChangePassword, u.CreatedAt, + FamilyId = u.FamilyId, + FamilyName = u.Family != null ? u.Family.Name : null + }) + .ToListAsync(); + return Ok(users); + } + + [HttpPost("users")] + public async Task CreateUser([FromBody] CreateUserRequest req) + { + if (!IsAdmin) return Forbid(); + if (await _db.Users.AnyAsync(u => u.Email == req.Email)) + return BadRequest("Email je už registrovaný."); + if (await _db.Users.AnyAsync(u => u.Username == req.Username)) + return BadRequest("Meno je obsadené."); + + var user = new User + { + Username = req.Username, + Email = req.Email, + FamilyId = req.FamilyId, + PasswordHash = BCrypt.Net.BCrypt.HashPassword("user"), + MustChangePassword = true + }; + _db.Users.Add(user); + await _db.SaveChangesAsync(); + + // Email notifikácia + await _email.SendAsync(req.Email, req.Username, + "👋 Vítaj v Calendar Reminder — bol ti vytvorený účet", + $""" +
+
+

👋 Bol ti vytvorený účet

+
+
+

Ahoj {req.Username}, bol ti vytvorený účet v aplikácii Calendar Reminder.

+ + + + +
Meno{req.Username}
Email{req.Email}
Heslouser
+

⚠️ Po prvom prihlásení si zmeň heslo cez „Zabudol som heslo".

+
+
Calendar Reminder
+
+ """); + + return Ok(new { user.Id, user.Username, user.Email, user.FamilyId, user.MustChangePassword }); + } + + [HttpPut("users/{id}")] + public async Task UpdateUser(int id, [FromBody] UpdateUserRequest req) + { + if (!IsAdmin) return Forbid(); + var user = await _db.Users.FindAsync(id); + if (user is null) return NotFound(); + if (await _db.Users.AnyAsync(u => u.Email == req.Email && u.Id != id)) + return BadRequest("Email je obsadený."); + if (await _db.Users.AnyAsync(u => u.Username == req.Username && u.Id != id)) + return BadRequest("Meno je obsadené."); + + user.Username = req.Username; + user.Email = req.Email; + user.FamilyId = req.FamilyId; + await _db.SaveChangesAsync(); + return Ok(new { user.Id, user.Username, user.Email, user.FamilyId }); + } + + [HttpDelete("users/{id}")] + public async Task DeleteUser(int id) + { + if (!IsAdmin) return Forbid(); + var myId = int.Parse(User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)!.Value); + if (id == myId) return BadRequest("Nemôžeš zmazať sám seba."); + var user = await _db.Users.FindAsync(id); + if (user is null) return NotFound(); + _db.Users.Remove(user); + await _db.SaveChangesAsync(); + return NoContent(); + } +} + +public record FamilyRequest(string Name); +public record CreateUserRequest(string Username, string Email, int? FamilyId); +public record UpdateUserRequest(string Username, string Email, int? FamilyId); diff --git a/CalendarReminder.Api/Controllers/AuthController.cs b/CalendarReminder.Api/Controllers/AuthController.cs index 3c655e5..cf5bd61 100644 --- a/CalendarReminder.Api/Controllers/AuthController.cs +++ b/CalendarReminder.Api/Controllers/AuthController.cs @@ -41,7 +41,7 @@ public class AuthController : ControllerBase await _db.SaveChangesAsync(); var token = _tokenService.GenerateToken(user); - return Ok(new AuthResponse(token, user.Username, user.Email)); + return Ok(new AuthResponse(token, user.Username, user.Email, user.IsAdmin, user.MustChangePassword)); } // POST api/auth/login @@ -56,7 +56,7 @@ public class AuthController : ControllerBase return Unauthorized("Nesprávny email/meno alebo heslo."); var token = _tokenService.GenerateToken(user); - return Ok(new AuthResponse(token, user.Username, user.Email)); + return Ok(new AuthResponse(token, user.Username, user.Email, user.IsAdmin, user.MustChangePassword)); } // POST api/auth/send-reset-code diff --git a/CalendarReminder.Api/Data/AppDbContext.cs b/CalendarReminder.Api/Data/AppDbContext.cs index 51dfdd2..7489c2e 100644 --- a/CalendarReminder.Api/Data/AppDbContext.cs +++ b/CalendarReminder.Api/Data/AppDbContext.cs @@ -9,6 +9,7 @@ public class AppDbContext : DbContext public DbSet Reminders => Set(); public DbSet Users => Set(); + public DbSet Families => Set(); public DbSet ReminderShares => Set(); public DbSet PasswordResetTokens => Set(); @@ -32,6 +33,17 @@ public class AppDbContext : DbContext entity.HasIndex(e => e.Username).IsUnique(); entity.Property(e => e.Email).IsRequired().HasMaxLength(200); entity.Property(e => e.Username).IsRequired().HasMaxLength(100); + entity.HasOne(e => e.Family) + .WithMany(f => f.Users) + .HasForeignKey(e => e.FamilyId) + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.HasIndex(e => e.Name).IsUnique(); + entity.Property(e => e.Name).IsRequired().HasMaxLength(100); }); modelBuilder.Entity(entity => diff --git a/CalendarReminder.Api/Migrations/20260611172823_AddFamilyAndAdminFields.Designer.cs b/CalendarReminder.Api/Migrations/20260611172823_AddFamilyAndAdminFields.Designer.cs new file mode 100644 index 0000000..85ba74d --- /dev/null +++ b/CalendarReminder.Api/Migrations/20260611172823_AddFamilyAndAdminFields.Designer.cs @@ -0,0 +1,243 @@ +// +using System; +using CalendarReminder.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CalendarReminder.Api.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260611172823_AddFamilyAndAdminFields")] + partial class AddFamilyAndAdminFields + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("CalendarReminder.Domain.Entities.Family", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Families"); + }); + + modelBuilder.Entity("CalendarReminder.Domain.Entities.PasswordResetToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Email") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("Used") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("PasswordResetTokens"); + }); + + modelBuilder.Entity("CalendarReminder.Domain.Entities.Reminder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("IsCompleted") + .HasColumnType("tinyint(1)"); + + b.Property("NotificationSent") + .HasColumnType("tinyint(1)"); + + b.Property("ReminderDate") + .HasColumnType("datetime(6)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Reminders"); + }); + + modelBuilder.Entity("CalendarReminder.Domain.Entities.ReminderShare", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ReminderId") + .HasColumnType("int"); + + b.Property("SharedWithUserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SharedWithUserId"); + + b.HasIndex("ReminderId", "SharedWithUserId") + .IsUnique(); + + b.ToTable("ReminderShares"); + }); + + modelBuilder.Entity("CalendarReminder.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("FamilyId") + .HasColumnType("int"); + + b.Property("IsAdmin") + .HasColumnType("tinyint(1)"); + + b.Property("MustChangePassword") + .HasColumnType("tinyint(1)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("FamilyId"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("CalendarReminder.Domain.Entities.Reminder", b => + { + b.HasOne("CalendarReminder.Domain.Entities.User", "User") + .WithMany("Reminders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CalendarReminder.Domain.Entities.ReminderShare", b => + { + b.HasOne("CalendarReminder.Domain.Entities.Reminder", "Reminder") + .WithMany("Shares") + .HasForeignKey("ReminderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CalendarReminder.Domain.Entities.User", "SharedWithUser") + .WithMany() + .HasForeignKey("SharedWithUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Reminder"); + + b.Navigation("SharedWithUser"); + }); + + modelBuilder.Entity("CalendarReminder.Domain.Entities.User", b => + { + b.HasOne("CalendarReminder.Domain.Entities.Family", "Family") + .WithMany("Users") + .HasForeignKey("FamilyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Family"); + }); + + modelBuilder.Entity("CalendarReminder.Domain.Entities.Family", b => + { + b.Navigation("Users"); + }); + + modelBuilder.Entity("CalendarReminder.Domain.Entities.Reminder", b => + { + b.Navigation("Shares"); + }); + + modelBuilder.Entity("CalendarReminder.Domain.Entities.User", b => + { + b.Navigation("Reminders"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CalendarReminder.Api/Migrations/20260611172823_AddFamilyAndAdminFields.cs b/CalendarReminder.Api/Migrations/20260611172823_AddFamilyAndAdminFields.cs new file mode 100644 index 0000000..318a1ff --- /dev/null +++ b/CalendarReminder.Api/Migrations/20260611172823_AddFamilyAndAdminFields.cs @@ -0,0 +1,96 @@ +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CalendarReminder.Api.Migrations +{ + /// + public partial class AddFamilyAndAdminFields : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "FamilyId", + table: "Users", + type: "int", + nullable: true); + + migrationBuilder.AddColumn( + name: "IsAdmin", + table: "Users", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "MustChangePassword", + table: "Users", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + + migrationBuilder.CreateTable( + name: "Families", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Name = table.Column(type: "varchar(100)", maxLength: 100, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_Families", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_Users_FamilyId", + table: "Users", + column: "FamilyId"); + + migrationBuilder.CreateIndex( + name: "IX_Families_Name", + table: "Families", + column: "Name", + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_Users_Families_FamilyId", + table: "Users", + column: "FamilyId", + principalTable: "Families", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Users_Families_FamilyId", + table: "Users"); + + migrationBuilder.DropTable( + name: "Families"); + + migrationBuilder.DropIndex( + name: "IX_Users_FamilyId", + table: "Users"); + + migrationBuilder.DropColumn( + name: "FamilyId", + table: "Users"); + + migrationBuilder.DropColumn( + name: "IsAdmin", + table: "Users"); + + migrationBuilder.DropColumn( + name: "MustChangePassword", + table: "Users"); + } + } +} diff --git a/CalendarReminder.Api/Migrations/AppDbContextModelSnapshot.cs b/CalendarReminder.Api/Migrations/AppDbContextModelSnapshot.cs index 098dad7..23533de 100644 --- a/CalendarReminder.Api/Migrations/AppDbContextModelSnapshot.cs +++ b/CalendarReminder.Api/Migrations/AppDbContextModelSnapshot.cs @@ -22,6 +22,27 @@ namespace CalendarReminder.Api.Migrations MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + modelBuilder.Entity("CalendarReminder.Domain.Entities.Family", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Families"); + }); + modelBuilder.Entity("CalendarReminder.Domain.Entities.PasswordResetToken", b => { b.Property("Id") @@ -128,6 +149,15 @@ namespace CalendarReminder.Api.Migrations .HasMaxLength(200) .HasColumnType("varchar(200)"); + b.Property("FamilyId") + .HasColumnType("int"); + + b.Property("IsAdmin") + .HasColumnType("tinyint(1)"); + + b.Property("MustChangePassword") + .HasColumnType("tinyint(1)"); + b.Property("PasswordHash") .IsRequired() .HasColumnType("longtext"); @@ -142,6 +172,8 @@ namespace CalendarReminder.Api.Migrations b.HasIndex("Email") .IsUnique(); + b.HasIndex("FamilyId"); + b.HasIndex("Username") .IsUnique(); @@ -178,6 +210,21 @@ namespace CalendarReminder.Api.Migrations b.Navigation("SharedWithUser"); }); + modelBuilder.Entity("CalendarReminder.Domain.Entities.User", b => + { + b.HasOne("CalendarReminder.Domain.Entities.Family", "Family") + .WithMany("Users") + .HasForeignKey("FamilyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Family"); + }); + + modelBuilder.Entity("CalendarReminder.Domain.Entities.Family", b => + { + b.Navigation("Users"); + }); + modelBuilder.Entity("CalendarReminder.Domain.Entities.Reminder", b => { b.Navigation("Shares"); diff --git a/CalendarReminder.Api/Models/AuthModels.cs b/CalendarReminder.Api/Models/AuthModels.cs index 1d52e27..f9daa53 100644 --- a/CalendarReminder.Api/Models/AuthModels.cs +++ b/CalendarReminder.Api/Models/AuthModels.cs @@ -2,4 +2,4 @@ namespace CalendarReminder.Api.Models; public record RegisterRequest(string Username, string Email, string Password); public record LoginRequest(string Email, string Password); -public record AuthResponse(string Token, string Username, string Email); +public record AuthResponse(string Token, string Username, string Email, bool IsAdmin, bool MustChangePassword); diff --git a/CalendarReminder.Api/Program.cs b/CalendarReminder.Api/Program.cs index 82bc970..344ca73 100644 --- a/CalendarReminder.Api/Program.cs +++ b/CalendarReminder.Api/Program.cs @@ -52,8 +52,9 @@ using (var scope = app.Services.CreateScope()) db.Users.Add(new CalendarReminder.Domain.Entities.User { Username = "admin", - Email = "admin@local", - PasswordHash = BCrypt.Net.BCrypt.HashPassword("admin") + Email = "nas@culak.sk", + PasswordHash = BCrypt.Net.BCrypt.HashPassword("Admin@2025"), + IsAdmin = true }); db.SaveChanges(); } diff --git a/CalendarReminder.Api/Services/TokenService.cs b/CalendarReminder.Api/Services/TokenService.cs index 16df870..222af4e 100644 --- a/CalendarReminder.Api/Services/TokenService.cs +++ b/CalendarReminder.Api/Services/TokenService.cs @@ -24,7 +24,9 @@ public class TokenService { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(ClaimTypes.Name, user.Username), - new Claim(ClaimTypes.Email, user.Email) + new Claim(ClaimTypes.Email, user.Email), + new Claim("isAdmin", user.IsAdmin.ToString().ToLower()), + new Claim("mustChangePassword", user.MustChangePassword.ToString().ToLower()) }; var token = new JwtSecurityToken( diff --git a/CalendarReminder.Api/wwwroot/index.html b/CalendarReminder.Api/wwwroot/index.html index f987fc1..c50dcca 100644 --- a/CalendarReminder.Api/wwwroot/index.html +++ b/CalendarReminder.Api/wwwroot/index.html @@ -102,6 +102,114 @@ } .btn-logout:hover { border-color: #f85149; color: #f85149; } + /* ── AVATAR DROPDOWN ── */ + .avatar-wrap { position: relative; } + .topbar-avatar { cursor: pointer; transition: box-shadow 0.15s; } + .topbar-avatar:hover { box-shadow: 0 0 0 2px #388bfd; } + .avatar-menu { + display: none; position: absolute; top: calc(100% + 8px); right: 0; + background: #1c1f2e; border: 1px solid #30363d; border-radius: 9px; + min-width: 180px; z-index: 500; + box-shadow: 0 8px 24px rgba(0,0,0,0.55); + overflow: hidden; + } + .avatar-menu.open { display: block; } + .avatar-menu-item { + display: flex; align-items: center; gap: 0.6rem; + padding: 0.62rem 1rem; font-size: 0.85rem; color: #c9d1d9; + cursor: pointer; transition: background 0.12s; border: none; + background: none; width: 100%; text-align: left; + } + .avatar-menu-item:hover { background: #30363d; } + .avatar-menu-item .mi-icon { font-size: 1rem; } + .avatar-menu-sep { border: none; border-top: 1px solid #30363d; margin: 3px 0; } + + /* ── ADMIN PANEL ── */ + .admin-overlay { + display: none; position: fixed; inset: 0; + background: rgba(0,0,0,0.7); z-index: 600; + align-items: flex-start; justify-content: center; + overflow-y: auto; padding: 2rem 1rem; + } + .admin-overlay.open { display: flex; } + .admin-panel { + background: #161b22; border: 1px solid #30363d; border-radius: 14px; + width: 100%; max-width: 820px; + box-shadow: 0 12px 48px rgba(0,0,0,0.7); + display: flex; flex-direction: column; + min-height: 0; + } + .admin-head { + display: flex; align-items: center; gap: 1rem; + padding: 1rem 1.5rem; border-bottom: 1px solid #30363d; flex-shrink: 0; + } + .admin-head h2 { font-size: 1rem; color: #c9d1d9; margin: 0; flex: 1; } + .btn-admin-close { + background: none; border: none; color: #6e7681; cursor: pointer; + font-size: 1.2rem; line-height: 1; padding: 2px; + transition: color 0.12s; + } + .btn-admin-close:hover { color: #f85149; } + .admin-tabs { + display: flex; gap: 0; border-bottom: 1px solid #30363d; flex-shrink: 0; + } + .admin-tab { + padding: 0.62rem 1.25rem; font-size: 0.85rem; color: #8b949e; + cursor: pointer; background: none; border: none; + border-bottom: 2px solid transparent; transition: all 0.12s; + font-weight: 500; + } + .admin-tab:hover { color: #c9d1d9; } + .admin-tab.active { color: #58a6ff; border-bottom-color: #58a6ff; } + .admin-body { padding: 1.25rem 1.5rem; flex: 1; } + .admin-section { display: none; } + .admin-section.active { display: block; } + .admin-toolbar { + display: flex; align-items: center; justify-content: space-between; + margin-bottom: 1rem; + } + .admin-toolbar h3 { font-size: 0.9rem; color: #8b949e; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; margin: 0; } + .btn-admin-add { + padding: 0.38rem 0.9rem; background: #238636; border: none; border-radius: 6px; + color: #fff; font-size: 0.82rem; font-weight: 600; cursor: pointer; transition: background 0.15s; + } + .btn-admin-add:hover { background: #2ea043; } + .admin-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; } + .admin-table th { + text-align: left; color: #6e7681; font-size: 0.74rem; text-transform: uppercase; + letter-spacing: 0.4px; padding: 0.4rem 0.75rem; border-bottom: 1px solid #21262d; + } + .admin-table td { padding: 0.55rem 0.75rem; border-bottom: 1px solid #21262d; color: #c9d1d9; vertical-align: middle; } + .admin-table tr:last-child td { border-bottom: none; } + .admin-table tr:hover td { background: #1c1f2e; } + .td-actions { display: flex; gap: 0.4rem; } + .btn-tbl { padding: 0.22rem 0.6rem; border-radius: 5px; font-size: 0.76rem; cursor: pointer; border: 1px solid; transition: all 0.12s; } + .btn-tbl-edit { border-color: #1f6feb55; color: #58a6ff; background: #1f6feb10; } + .btn-tbl-edit:hover { background: #1f6feb33; } + .btn-tbl-del { border-color: #f8514955; color: #f85149; background: #f8514910; } + .btn-tbl-del:hover { background: #f8514930; } + .badge-admin { padding: 1px 7px; border-radius: 8px; font-size: 0.72rem; font-weight: 700; background: #1f6feb22; color: #388bfd; } + .badge-mustchange { padding: 1px 7px; border-radius: 8px; font-size: 0.72rem; font-weight: 700; background: #e3b34122; color: #e3b341; } + + /* Mini form in admin */ + .admin-form { background: #0d1117; border: 1px solid #30363d; border-radius: 9px; padding: 1rem; margin-bottom: 1.25rem; } + .admin-form h4 { font-size: 0.8rem; color: #8b949e; text-transform: uppercase; letter-spacing: 0.4px; margin-bottom: 0.75rem; } + .admin-form .row { display: flex; gap: 0.6rem; flex-wrap: wrap; } + .admin-form .row > * { flex: 1 1 140px; } + .admin-form input, .admin-form select { + width: 100%; padding: 0.48rem 0.65rem; + background: #161b22; border: 1px solid #30363d; border-radius: 6px; + color: #e1e4e8; font-size: 0.85rem; outline: none; font-family: inherit; + transition: border-color 0.15s; + } + .admin-form input:focus, .admin-form select:focus { border-color: #58a6ff; } + .admin-form select option { background: #161b22; } + .admin-form label { display: block; font-size: 0.75rem; color: #8b949e; margin-bottom: 0.28rem; text-transform: uppercase; letter-spacing: 0.3px; } + .admin-form-actions { display: flex; gap: 0.6rem; margin-top: 0.75rem; justify-content: flex-end; } + .btn-admin-save { padding: 0.42rem 1rem; background: #1f6feb; border: none; border-radius: 6px; color: #fff; cursor: pointer; font-weight: 600; font-size: 0.84rem; } + .btn-admin-cancel { padding: 0.42rem 0.9rem; background: transparent; border: 1px solid #30363d; border-radius: 6px; color: #8b949e; cursor: pointer; font-size: 0.84rem; } + .admin-err { color: #f85149; font-size: 0.8rem; min-height: 1.1rem; margin-top: 0.35rem; } + /* ── APP BODY ── */ .app-body { flex: 1; @@ -404,9 +512,17 @@
-
?
- +
+
?
+
+ + + + + +
+
@@ -502,6 +618,103 @@ + +
+
+
+

⚙️ Administrácia

+ +
+
+ + + +
+
+ + +
+ +
+

Používatelia

+ +
+ + + +
MenoEmailRodinaRoly
+
+ + +
+ +
+

Rodiny

+ +
+ + + +
NázovPočet členov
+
+ + +
+

Nastavenia budú dostupné čoskoro.

+
+ +
+
+
+ + + + diff --git a/CalendarReminder.Domain/Entities/Family.cs b/CalendarReminder.Domain/Entities/Family.cs new file mode 100644 index 0000000..cc8bf0b --- /dev/null +++ b/CalendarReminder.Domain/Entities/Family.cs @@ -0,0 +1,8 @@ +namespace CalendarReminder.Domain.Entities; + +public class Family +{ + public int Id { get; set; } + public string Name { get; set; } = ""; + public ICollection Users { get; set; } = []; +} diff --git a/CalendarReminder.Domain/Entities/User.cs b/CalendarReminder.Domain/Entities/User.cs index 0106b95..807c796 100644 --- a/CalendarReminder.Domain/Entities/User.cs +++ b/CalendarReminder.Domain/Entities/User.cs @@ -7,6 +7,11 @@ public class User public string Email { get; set; } = ""; public string PasswordHash { get; set; } = ""; public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public bool IsAdmin { get; set; } + public bool MustChangePassword { get; set; } + + public int? FamilyId { get; set; } + public Family? Family { get; set; } public ICollection Reminders { get; set; } = []; }