diff --git a/CalendarReminder.Api/CalendarReminder.Api.csproj b/CalendarReminder.Api/CalendarReminder.Api.csproj new file mode 100644 index 0000000..bd00813 --- /dev/null +++ b/CalendarReminder.Api/CalendarReminder.Api.csproj @@ -0,0 +1,25 @@ + + + + net9.0 + enable + enable + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/CalendarReminder.Api/CalendarReminder.Api.http b/CalendarReminder.Api/CalendarReminder.Api.http new file mode 100644 index 0000000..b227111 --- /dev/null +++ b/CalendarReminder.Api/CalendarReminder.Api.http @@ -0,0 +1,6 @@ +@CalendarReminder.Api_HostAddress = http://localhost:5115 + +GET {{CalendarReminder.Api_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/CalendarReminder.Api/Controllers/AuthController.cs b/CalendarReminder.Api/Controllers/AuthController.cs new file mode 100644 index 0000000..e4909b9 --- /dev/null +++ b/CalendarReminder.Api/Controllers/AuthController.cs @@ -0,0 +1,58 @@ +using CalendarReminder.Api.Data; +using CalendarReminder.Api.Models; +using CalendarReminder.Api.Services; +using CalendarReminder.Domain.Entities; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace CalendarReminder.Api.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class AuthController : ControllerBase +{ + private readonly AppDbContext _db; + private readonly TokenService _tokenService; + + public AuthController(AppDbContext db, TokenService tokenService) + { + _db = db; + _tokenService = tokenService; + } + + // POST api/auth/register + [HttpPost("register")] + public async Task Register(RegisterRequest req) + { + 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("Používateľské meno je obsadené."); + + var user = new User + { + Username = req.Username, + Email = req.Email, + PasswordHash = BCrypt.Net.BCrypt.HashPassword(req.Password) + }; + + _db.Users.Add(user); + await _db.SaveChangesAsync(); + + var token = _tokenService.GenerateToken(user); + return Ok(new AuthResponse(token, user.Username, user.Email)); + } + + // POST api/auth/login + [HttpPost("login")] + public async Task Login(LoginRequest req) + { + var user = await _db.Users.FirstOrDefaultAsync(u => u.Email == req.Email); + if (user is null || !BCrypt.Net.BCrypt.Verify(req.Password, user.PasswordHash)) + return Unauthorized("Nesprávny email alebo heslo."); + + var token = _tokenService.GenerateToken(user); + return Ok(new AuthResponse(token, user.Username, user.Email)); + } +} diff --git a/CalendarReminder.Api/Controllers/RemindersController.cs b/CalendarReminder.Api/Controllers/RemindersController.cs new file mode 100644 index 0000000..11cd225 --- /dev/null +++ b/CalendarReminder.Api/Controllers/RemindersController.cs @@ -0,0 +1,203 @@ +using CalendarReminder.Api.Data; +using CalendarReminder.Api.Services; +using CalendarReminder.Domain.Entities; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System.Security.Claims; + +namespace CalendarReminder.Api.Controllers; + +[Authorize] +[ApiController] +[Route("api/[controller]")] +public class RemindersController : ControllerBase +{ + private readonly AppDbContext _db; + private readonly EmailService _email; + + public RemindersController(AppDbContext db, EmailService email) + { + _db = db; + _email = email; + } + + private int UserId => int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); + + // GET api/reminders — moje + zdieľané so mnou + [HttpGet] + public async Task GetAll() + { + var myId = UserId; + + var reminders = await _db.Reminders + .Include(r => r.User) + .Include(r => r.Shares).ThenInclude(s => s.SharedWithUser) + .Where(r => r.UserId == myId || r.Shares.Any(s => s.SharedWithUserId == myId)) + .OrderBy(r => r.ReminderDate) + .Select(r => new + { + r.Id, r.Title, r.Description, r.ReminderDate, + r.IsCompleted, r.NotificationSent, r.UserId, r.CreatedAt, + Owner = r.User!.Username, + IsOwner = r.UserId == myId, + SharedWith = r.Shares.Select(s => new { s.SharedWithUser!.Id, s.SharedWithUser.Username }) + }) + .ToListAsync(); + + return Ok(reminders); + } + + // GET api/reminders/5 + [HttpGet("{id}")] + public async Task GetById(int id) + { + var myId = UserId; + var r = await _db.Reminders + .Include(r => r.Shares) + .FirstOrDefaultAsync(r => r.Id == id && + (r.UserId == myId || r.Shares.Any(s => s.SharedWithUserId == myId))); + return r is null ? NotFound() : Ok(r); + } + + // POST api/reminders + [HttpPost] + public async Task Create([FromBody] CreateReminderRequest req) + { + var reminder = new Reminder + { + Title = req.Title, + Description = req.Description, + ReminderDate = req.ReminderDate, + IsCompleted = false, + NotificationSent = false, + UserId = UserId, + CreatedAt = DateTime.UtcNow + }; + _db.Reminders.Add(reminder); + await _db.SaveChangesAsync(); + + // Zdieľaj s vybranými používateľmi + foreach (var uid in req.ShareWithUserIds ?? []) + { + _db.ReminderShares.Add(new ReminderShare { ReminderId = reminder.Id, SharedWithUserId = uid }); + } + if (req.ShareWithUserIds?.Count > 0) + await _db.SaveChangesAsync(); + + return CreatedAtAction(nameof(GetById), new { id = reminder.Id }, reminder); + } + + // PUT api/reminders/5 + [HttpPut("{id}")] + public async Task Update(int id, [FromBody] UpdateReminderRequest req) + { + var reminder = await _db.Reminders + .Include(r => r.Shares) + .FirstOrDefaultAsync(r => r.Id == id && r.UserId == UserId); + if (reminder is null) return NotFound(); + + bool dateChanged = reminder.ReminderDate != req.ReminderDate; + reminder.Title = req.Title; + reminder.Description = req.Description; + reminder.ReminderDate = req.ReminderDate; + reminder.IsCompleted = req.IsCompleted; + if (dateChanged) reminder.NotificationSent = false; + + // Aktualizuj zdieľanie + _db.ReminderShares.RemoveRange(reminder.Shares); + foreach (var uid in req.ShareWithUserIds ?? []) + _db.ReminderShares.Add(new ReminderShare { ReminderId = id, SharedWithUserId = uid }); + + await _db.SaveChangesAsync(); + return Ok(reminder); + } + + // DELETE api/reminders/5 + [HttpDelete("{id}")] + public async Task Delete(int id) + { + var reminder = await _db.Reminders + .FirstOrDefaultAsync(r => r.Id == id && r.UserId == UserId); + if (reminder is null) return NotFound(); + _db.Reminders.Remove(reminder); + await _db.SaveChangesAsync(); + return NoContent(); + } + + // POST api/reminders/5/send-email + [HttpPost("{id}/send-email")] + public async Task SendEmail(int id) + { + var myId = UserId; + var reminder = await _db.Reminders + .Include(r => r.User) + .Include(r => r.Shares).ThenInclude(s => s.SharedWithUser) + .FirstOrDefaultAsync(r => r.Id == id && r.UserId == myId); + + if (reminder is null) return NotFound(); + if (!reminder.Shares.Any()) return BadRequest("Pripomienka nie je zdieľaná so žiadnym používateľom."); + + var date = reminder.ReminderDate.ToLocalTime(); + var dateStr = date.ToString("dd.MM.yyyy"); + var timeStr = date.ToString("HH:mm"); + + var body = $""" + + + 📅 Calendar Reminder + + + {reminder.Title} + + + Dátum + {dateStr} + + + Čas + {timeStr} + + {(reminder.Description != null ? $""" + + Popis + {reminder.Description} + + """ : "")} + + Od + {reminder.User!.Username} + + + + + Calendar Reminder — automatická správa + + + """; + + var tasks = reminder.Shares + .Where(s => s.SharedWithUser != null) + .Select(s => _email.SendAsync( + s.SharedWithUser!.Email, + s.SharedWithUser.Username, + $"📅 Pripomienka: {reminder.Title} — {dateStr} {timeStr}", + body)); + + await Task.WhenAll(tasks); + return Ok(new { sent = reminder.Shares.Count }); + } +} + +public record CreateReminderRequest( + string Title, + string? Description, + DateTime ReminderDate, + List? ShareWithUserIds); + +public record UpdateReminderRequest( + string Title, + string? Description, + DateTime ReminderDate, + bool IsCompleted, + List? ShareWithUserIds); diff --git a/CalendarReminder.Api/Controllers/UsersController.cs b/CalendarReminder.Api/Controllers/UsersController.cs new file mode 100644 index 0000000..bcbd740 --- /dev/null +++ b/CalendarReminder.Api/Controllers/UsersController.cs @@ -0,0 +1,34 @@ +using CalendarReminder.Api.Data; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace CalendarReminder.Api.Controllers; + +[Authorize] +[ApiController] +[Route("api/[controller]")] +public class UsersController : ControllerBase +{ + private readonly AppDbContext _db; + + public UsersController(AppDbContext db) => _db = db; + + // GET api/users?search=jan + [HttpGet] + public async Task Search([FromQuery] string? search) + { + var myId = int.Parse(User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)!.Value); + + var query = _db.Users.Where(u => u.Id != myId); + if (!string.IsNullOrWhiteSpace(search)) + query = query.Where(u => u.Username.Contains(search) || u.Email.Contains(search)); + + var users = await query + .Select(u => new { u.Id, u.Username, u.Email }) + .Take(20) + .ToListAsync(); + + return Ok(users); + } +} diff --git a/CalendarReminder.Api/Data/AppDbContext.cs b/CalendarReminder.Api/Data/AppDbContext.cs new file mode 100644 index 0000000..e2649f5 --- /dev/null +++ b/CalendarReminder.Api/Data/AppDbContext.cs @@ -0,0 +1,50 @@ +using CalendarReminder.Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace CalendarReminder.Api.Data; + +public class AppDbContext : DbContext +{ + public AppDbContext(DbContextOptions options) : base(options) { } + + public DbSet Reminders => Set(); + public DbSet Users => Set(); + public DbSet ReminderShares => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.Title).IsRequired().HasMaxLength(200); + entity.Property(e => e.Description).HasMaxLength(1000); + entity.HasOne(e => e.User) + .WithMany(u => u.Reminders) + .HasForeignKey(e => e.UserId) + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.HasIndex(e => e.Email).IsUnique(); + entity.HasIndex(e => e.Username).IsUnique(); + entity.Property(e => e.Email).IsRequired().HasMaxLength(200); + entity.Property(e => e.Username).IsRequired().HasMaxLength(100); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.HasIndex(e => new { e.ReminderId, e.SharedWithUserId }).IsUnique(); + entity.HasOne(e => e.Reminder) + .WithMany(r => r.Shares) + .HasForeignKey(e => e.ReminderId) + .OnDelete(DeleteBehavior.Cascade); + entity.HasOne(e => e.SharedWithUser) + .WithMany() + .HasForeignKey(e => e.SharedWithUserId) + .OnDelete(DeleteBehavior.Cascade); + }); + } +} diff --git a/CalendarReminder.Api/Migrations/20260604161952_InitialCreate.Designer.cs b/CalendarReminder.Api/Migrations/20260604161952_InitialCreate.Designer.cs new file mode 100644 index 0000000..1fee2f5 --- /dev/null +++ b/CalendarReminder.Api/Migrations/20260604161952_InitialCreate.Designer.cs @@ -0,0 +1,121 @@ +// +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("20260604161952_InitialCreate")] + partial class InitialCreate + { + /// + 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.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.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("PasswordHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + 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.User", b => + { + b.Navigation("Reminders"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CalendarReminder.Api/Migrations/20260604161952_InitialCreate.cs b/CalendarReminder.Api/Migrations/20260604161952_InitialCreate.cs new file mode 100644 index 0000000..b9b459c --- /dev/null +++ b/CalendarReminder.Api/Migrations/20260604161952_InitialCreate.cs @@ -0,0 +1,94 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CalendarReminder.Api.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Username = table.Column(type: "varchar(100)", maxLength: 100, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Email = table.Column(type: "varchar(200)", maxLength: 200, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + PasswordHash = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + CreatedAt = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "Reminders", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Title = table.Column(type: "varchar(200)", maxLength: 200, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Description = table.Column(type: "varchar(1000)", maxLength: 1000, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + ReminderDate = table.Column(type: "datetime(6)", nullable: false), + IsCompleted = table.Column(type: "tinyint(1)", nullable: false), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + NotificationSent = table.Column(type: "tinyint(1)", nullable: false), + UserId = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Reminders", x => x.Id); + table.ForeignKey( + name: "FK_Reminders_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_Reminders_UserId", + table: "Reminders", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_Users_Email", + table: "Users", + column: "Email", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Users_Username", + table: "Users", + column: "Username", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Reminders"); + + migrationBuilder.DropTable( + name: "Users"); + } + } +} diff --git a/CalendarReminder.Api/Migrations/20260604182545_AddReminderShares.Designer.cs b/CalendarReminder.Api/Migrations/20260604182545_AddReminderShares.Designer.cs new file mode 100644 index 0000000..6885b58 --- /dev/null +++ b/CalendarReminder.Api/Migrations/20260604182545_AddReminderShares.Designer.cs @@ -0,0 +1,169 @@ +// +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("20260604182545_AddReminderShares")] + partial class AddReminderShares + { + /// + 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.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("PasswordHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + 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.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/20260604182545_AddReminderShares.cs b/CalendarReminder.Api/Migrations/20260604182545_AddReminderShares.cs new file mode 100644 index 0000000..f3babff --- /dev/null +++ b/CalendarReminder.Api/Migrations/20260604182545_AddReminderShares.cs @@ -0,0 +1,60 @@ +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CalendarReminder.Api.Migrations +{ + /// + public partial class AddReminderShares : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ReminderShares", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + ReminderId = table.Column(type: "int", nullable: false), + SharedWithUserId = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ReminderShares", x => x.Id); + table.ForeignKey( + name: "FK_ReminderShares_Reminders_ReminderId", + column: x => x.ReminderId, + principalTable: "Reminders", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ReminderShares_Users_SharedWithUserId", + column: x => x.SharedWithUserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_ReminderShares_ReminderId_SharedWithUserId", + table: "ReminderShares", + columns: new[] { "ReminderId", "SharedWithUserId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ReminderShares_SharedWithUserId", + table: "ReminderShares", + column: "SharedWithUserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ReminderShares"); + } + } +} diff --git a/CalendarReminder.Api/Migrations/AppDbContextModelSnapshot.cs b/CalendarReminder.Api/Migrations/AppDbContextModelSnapshot.cs new file mode 100644 index 0000000..6959c54 --- /dev/null +++ b/CalendarReminder.Api/Migrations/AppDbContextModelSnapshot.cs @@ -0,0 +1,166 @@ +// +using System; +using CalendarReminder.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CalendarReminder.Api.Migrations +{ + [DbContext(typeof(AppDbContext))] + partial class AppDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(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.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("PasswordHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + 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.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/Models/AuthModels.cs b/CalendarReminder.Api/Models/AuthModels.cs new file mode 100644 index 0000000..1d52e27 --- /dev/null +++ b/CalendarReminder.Api/Models/AuthModels.cs @@ -0,0 +1,5 @@ +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); diff --git a/CalendarReminder.Api/Program.cs b/CalendarReminder.Api/Program.cs new file mode 100644 index 0000000..1be72d5 --- /dev/null +++ b/CalendarReminder.Api/Program.cs @@ -0,0 +1,60 @@ +using CalendarReminder.Api.Data; +using CalendarReminder.Api.Services; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using System.Text; + +var builder = WebApplication.CreateBuilder(args); + +// Controllers + OpenAPI +builder.Services.AddControllers(); +builder.Services.AddOpenApi(); + +// MariaDB / EF Core +var connStr = builder.Configuration.GetConnectionString("Default"); +builder.Services.AddDbContext(options => + options.UseMySql(connStr, ServerVersion.AutoDetect(connStr))); + +// JWT autentifikácia +var jwtSecret = builder.Configuration["Jwt:Secret"]!; +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecret)), + ValidateIssuer = true, + ValidIssuer = builder.Configuration["Jwt:Issuer"], + ValidateAudience = true, + ValidAudience = builder.Configuration["Jwt:Audience"], + ValidateLifetime = true + }; + }); +builder.Services.AddAuthorization(); + +// Services +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddHostedService(); + +var app = builder.Build(); + +// Auto-migrate pri štarte +using (var scope = app.Services.CreateScope()) +{ + var db = scope.ServiceProvider.GetRequiredService(); + db.Database.Migrate(); +} + +if (app.Environment.IsDevelopment()) + app.MapOpenApi(); + +app.UseDefaultFiles(); // servuje index.html z wwwroot +app.UseStaticFiles(); // frontend súbory + +app.UseAuthentication(); +app.UseAuthorization(); +app.MapControllers(); +app.Run(); diff --git a/CalendarReminder.Api/Properties/launchSettings.json b/CalendarReminder.Api/Properties/launchSettings.json new file mode 100644 index 0000000..b20252d --- /dev/null +++ b/CalendarReminder.Api/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5115", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7160;http://localhost:5115", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/CalendarReminder.Api/Services/EmailService.cs b/CalendarReminder.Api/Services/EmailService.cs new file mode 100644 index 0000000..4a54fdf --- /dev/null +++ b/CalendarReminder.Api/Services/EmailService.cs @@ -0,0 +1,51 @@ +using MailKit.Net.Smtp; +using MimeKit; + +namespace CalendarReminder.Api.Services; + +public class EmailSettings +{ + public string Host { get; set; } = ""; + public int Port { get; set; } = 587; + public string Username { get; set; } = ""; + public string Password { get; set; } = ""; + public string From { get; set; } = ""; + public bool UseSsl { get; set; } = false; +} + +public class EmailService +{ + private readonly EmailSettings _settings; + private readonly ILogger _logger; + + public EmailService(IConfiguration config, ILogger logger) + { + _settings = config.GetSection("Email").Get() ?? new EmailSettings(); + _logger = logger; + } + + public async Task SendAsync(string toEmail, string toName, string subject, string body) + { + try + { + var message = new MimeMessage(); + message.From.Add(new MailboxAddress("CalendarReminder", _settings.From)); + message.To.Add(new MailboxAddress(toName, toEmail)); + message.Subject = subject; + message.Body = new TextPart("html") { Text = body }; + + using var client = new SmtpClient(); + await client.ConnectAsync(_settings.Host, _settings.Port, _settings.UseSsl); + if (!string.IsNullOrEmpty(_settings.Username)) + await client.AuthenticateAsync(_settings.Username, _settings.Password); + await client.SendAsync(message); + await client.DisconnectAsync(true); + + _logger.LogInformation("Email odoslaný na {Email}", toEmail); + } + catch (Exception ex) + { + _logger.LogError(ex, "Chyba pri odosielaní emailu na {Email}", toEmail); + } + } +} diff --git a/CalendarReminder.Api/Services/ReminderNotificationService.cs b/CalendarReminder.Api/Services/ReminderNotificationService.cs new file mode 100644 index 0000000..f17fa7a --- /dev/null +++ b/CalendarReminder.Api/Services/ReminderNotificationService.cs @@ -0,0 +1,71 @@ +using CalendarReminder.Api.Data; +using Microsoft.EntityFrameworkCore; + +namespace CalendarReminder.Api.Services; + +public class ReminderNotificationService : BackgroundService +{ + private readonly IServiceScopeFactory _scopeFactory; + private readonly ILogger _logger; + + public ReminderNotificationService(IServiceScopeFactory scopeFactory, + ILogger logger) + { + _scopeFactory = scopeFactory; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("Notification service spustený"); + + while (!stoppingToken.IsCancellationRequested) + { + await CheckRemindersAsync(); + await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); + } + } + + private async Task CheckRemindersAsync() + { + using var scope = _scopeFactory.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var emailService = scope.ServiceProvider.GetRequiredService(); + + var now = DateTime.UtcNow; + var windowEnd = now.AddMinutes(15); + + // Nájdi remindery, ktoré sú do 15 minút a notifikácia ešte nebola odoslaná + var upcoming = await db.Reminders + .Include(r => r.User) + .Where(r => !r.IsCompleted + && !r.NotificationSent + && r.ReminderDate >= now + && r.ReminderDate <= windowEnd) + .ToListAsync(); + + foreach (var reminder in upcoming) + { + var minutesLeft = (int)(reminder.ReminderDate - now).TotalMinutes; + var body = $""" + ⏰ Pripomienka: {reminder.Title} + Čas: {reminder.ReminderDate:dd.MM.yyyy HH:mm} + O: {minutesLeft} minút + {(reminder.Description != null ? $"Popis: {reminder.Description}" : "")} + + CalendarReminder + """; + + await emailService.SendAsync( + reminder.User!.Email, + reminder.User.Username, + $"⏰ Pripomienka: {reminder.Title}", + body); + + reminder.NotificationSent = true; + } + + if (upcoming.Count > 0) + await db.SaveChangesAsync(); + } +} diff --git a/CalendarReminder.Api/Services/TokenService.cs b/CalendarReminder.Api/Services/TokenService.cs new file mode 100644 index 0000000..16df870 --- /dev/null +++ b/CalendarReminder.Api/Services/TokenService.cs @@ -0,0 +1,39 @@ +using CalendarReminder.Domain.Entities; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; + +namespace CalendarReminder.Api.Services; + +public class TokenService +{ + private readonly IConfiguration _config; + + public TokenService(IConfiguration config) + { + _config = config; + } + + public string GenerateToken(User user) + { + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Secret"]!)); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), + new Claim(ClaimTypes.Name, user.Username), + new Claim(ClaimTypes.Email, user.Email) + }; + + var token = new JwtSecurityToken( + issuer: _config["Jwt:Issuer"], + audience: _config["Jwt:Audience"], + claims: claims, + expires: DateTime.UtcNow.AddDays(7), + signingCredentials: creds); + + return new JwtSecurityTokenHandler().WriteToken(token); + } +} diff --git a/CalendarReminder.Api/appsettings.Development.json b/CalendarReminder.Api/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/CalendarReminder.Api/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/CalendarReminder.Api/appsettings.json b/CalendarReminder.Api/appsettings.json new file mode 100644 index 0000000..7c787ae --- /dev/null +++ b/CalendarReminder.Api/appsettings.json @@ -0,0 +1,25 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "Default": "Server=192.168.1.68;Port=3306;Database=calendarreminder;User=mip;Password=mip@2013;" + }, + "Jwt": { + "Secret": "Hhb$bHfHsi@*lqxd1noC&DPK!$SL8fy9x", + "Issuer": "CalendarReminder", + "Audience": "CalendarReminderUsers" + }, + "Email": { + "Host": "smtp.gmail.com", + "Port": 465, + "Username": "nas@culak.sk", + "Password": "ggTdYzhRfFlK3RoiojOr", + "From": "nas@culak.sk", + "UseSsl": true + } +} diff --git a/CalendarReminder.Api/wwwroot/index.html b/CalendarReminder.Api/wwwroot/index.html new file mode 100644 index 0000000..e32f3f9 --- /dev/null +++ b/CalendarReminder.Api/wwwroot/index.html @@ -0,0 +1,931 @@ + + + + + + Calendar Reminder + + + + + + + + + 📅 + Calendar Reminder + + Správa udalostí s emailovými notifikáciami + + Prihlásiť sa + Registrácia + + + Email + Heslo + Prihlásiť sa + + + + Meno + Email + Heslo + Vytvoriť účet + + + + + + + + + + + + 📅 + Calendar Reminder + + + + ? + + Odhlásiť sa + + + + + + + + + + + ‹ + › + Dnes + + + + + + + Pon + Uto + Str + Štv + Pia + Sob + Ned + + + + + + + + + + Nová pripomienka + + + + + + + + + + + + Pridať pripomienku + + + + + Zoznam + + + Všetky + Tento týždeň + Dnes + Nadchádzajúce + Dokončené + + + + + + + + + + + + + ✏️ Upraviť pripomienku + ✉️ + + + Názov + Dátum a čas + Popis + + + Zrušiť + Uložiť + + + + + + + diff --git a/CalendarReminder.Domain/CalendarReminder.Domain.csproj b/CalendarReminder.Domain/CalendarReminder.Domain.csproj new file mode 100644 index 0000000..125f4c9 --- /dev/null +++ b/CalendarReminder.Domain/CalendarReminder.Domain.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/CalendarReminder.Domain/Class1.cs b/CalendarReminder.Domain/Class1.cs new file mode 100644 index 0000000..32de215 --- /dev/null +++ b/CalendarReminder.Domain/Class1.cs @@ -0,0 +1,6 @@ +namespace CalendarReminder.Domain; + +public class Class1 +{ + +} diff --git a/CalendarReminder.Domain/Entities/Reminder.cs b/CalendarReminder.Domain/Entities/Reminder.cs new file mode 100644 index 0000000..a5e9238 --- /dev/null +++ b/CalendarReminder.Domain/Entities/Reminder.cs @@ -0,0 +1,17 @@ +namespace CalendarReminder.Domain.Entities; + +public class Reminder +{ + public int Id { get; set; } + public string Title { get; set; } = ""; + public string? Description { get; set; } + public DateTime ReminderDate { get; set; } + public bool IsCompleted { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public bool NotificationSent { get; set; } + + // FK + public int UserId { get; set; } + public User? User { get; set; } + public ICollection Shares { get; set; } = []; +} diff --git a/CalendarReminder.Domain/Entities/ReminderShare.cs b/CalendarReminder.Domain/Entities/ReminderShare.cs new file mode 100644 index 0000000..db1eebf --- /dev/null +++ b/CalendarReminder.Domain/Entities/ReminderShare.cs @@ -0,0 +1,10 @@ +namespace CalendarReminder.Domain.Entities; + +public class ReminderShare +{ + public int Id { get; set; } + public int ReminderId { get; set; } + public Reminder? Reminder { get; set; } + public int SharedWithUserId { get; set; } + public User? SharedWithUser { get; set; } +} diff --git a/CalendarReminder.Domain/Entities/User.cs b/CalendarReminder.Domain/Entities/User.cs new file mode 100644 index 0000000..0106b95 --- /dev/null +++ b/CalendarReminder.Domain/Entities/User.cs @@ -0,0 +1,12 @@ +namespace CalendarReminder.Domain.Entities; + +public class User +{ + public int Id { get; set; } + public string Username { get; set; } = ""; + public string Email { get; set; } = ""; + public string PasswordHash { get; set; } = ""; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + public ICollection Reminders { get; set; } = []; +} diff --git a/CalendarReminder.sln b/CalendarReminder.sln new file mode 100644 index 0000000..1924f97 --- /dev/null +++ b/CalendarReminder.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalendarReminder.Api", "CalendarReminder.Api\CalendarReminder.Api.csproj", "{1C920E47-D7B4-4B98-907F-CB18D831BF28}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalendarReminder.Domain", "CalendarReminder.Domain\CalendarReminder.Domain.csproj", "{E1235840-D959-45E7-950C-AD7E61CD658C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1C920E47-D7B4-4B98-907F-CB18D831BF28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1C920E47-D7B4-4B98-907F-CB18D831BF28}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1C920E47-D7B4-4B98-907F-CB18D831BF28}.Debug|x64.ActiveCfg = Debug|Any CPU + {1C920E47-D7B4-4B98-907F-CB18D831BF28}.Debug|x64.Build.0 = Debug|Any CPU + {1C920E47-D7B4-4B98-907F-CB18D831BF28}.Debug|x86.ActiveCfg = Debug|Any CPU + {1C920E47-D7B4-4B98-907F-CB18D831BF28}.Debug|x86.Build.0 = Debug|Any CPU + {1C920E47-D7B4-4B98-907F-CB18D831BF28}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1C920E47-D7B4-4B98-907F-CB18D831BF28}.Release|Any CPU.Build.0 = Release|Any CPU + {1C920E47-D7B4-4B98-907F-CB18D831BF28}.Release|x64.ActiveCfg = Release|Any CPU + {1C920E47-D7B4-4B98-907F-CB18D831BF28}.Release|x64.Build.0 = Release|Any CPU + {1C920E47-D7B4-4B98-907F-CB18D831BF28}.Release|x86.ActiveCfg = Release|Any CPU + {1C920E47-D7B4-4B98-907F-CB18D831BF28}.Release|x86.Build.0 = Release|Any CPU + {E1235840-D959-45E7-950C-AD7E61CD658C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E1235840-D959-45E7-950C-AD7E61CD658C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E1235840-D959-45E7-950C-AD7E61CD658C}.Debug|x64.ActiveCfg = Debug|Any CPU + {E1235840-D959-45E7-950C-AD7E61CD658C}.Debug|x64.Build.0 = Debug|Any CPU + {E1235840-D959-45E7-950C-AD7E61CD658C}.Debug|x86.ActiveCfg = Debug|Any CPU + {E1235840-D959-45E7-950C-AD7E61CD658C}.Debug|x86.Build.0 = Debug|Any CPU + {E1235840-D959-45E7-950C-AD7E61CD658C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E1235840-D959-45E7-950C-AD7E61CD658C}.Release|Any CPU.Build.0 = Release|Any CPU + {E1235840-D959-45E7-950C-AD7E61CD658C}.Release|x64.ActiveCfg = Release|Any CPU + {E1235840-D959-45E7-950C-AD7E61CD658C}.Release|x64.Build.0 = Release|Any CPU + {E1235840-D959-45E7-950C-AD7E61CD658C}.Release|x86.ActiveCfg = Release|Any CPU + {E1235840-D959-45E7-950C-AD7E61CD658C}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..602224c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base +WORKDIR /app +EXPOSE 8080 + +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +WORKDIR /src +COPY . . +RUN dotnet publish CalendarReminder.Api/CalendarReminder.Api.csproj -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=build /app/publish . +ENTRYPOINT ["dotnet", "CalendarReminder.Api.dll"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..04a5b85 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,28 @@ +services: + app: + build: . + ports: + - "8080:8080" + environment: + - ConnectionStrings__Default=Server=db;Port=3306;Database=calendarreminder;User=appuser;Password=secret; + - ASPNETCORE_ENVIRONMENT=Production + depends_on: + db: + condition: service_healthy + restart: unless-stopped + + db: + image: mariadb:11 + environment: + MARIADB_ROOT_PASSWORD: rootpass + MARIADB_DATABASE: calendarreminder + MARIADB_USER: appuser + MARIADB_PASSWORD: secret + volumes: + - /mnt/tank/calendarreminder/mariadb:/var/lib/mysql + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped
Čas: {reminder.ReminderDate:dd.MM.yyyy HH:mm}
O: {minutesLeft} minút
Popis: {reminder.Description}
Správa udalostí s emailovými notifikáciami