From 43011f0a774f7878a6ef441bd82e42b0f7a1a012 Mon Sep 17 00:00:00 2001 From: roman Date: Wed, 10 Jun 2026 20:39:52 +0200 Subject: [PATCH] login changes --- .env.example | 19 ++ .gitignore | 7 +- .../Controllers/AuthController.cs | 93 ++++++++- CalendarReminder.Api/Data/AppDbContext.cs | 1 + ...0183536_AddPasswordResetTokens.Designer.cs | 196 ++++++++++++++++++ .../20260610183536_AddPasswordResetTokens.cs | 42 ++++ .../Migrations/AppDbContextModelSnapshot.cs | 27 +++ CalendarReminder.Api/Program.cs | 13 +- .../appsettings.Development.json | 15 +- CalendarReminder.Api/appsettings.json | 14 +- CalendarReminder.Api/wwwroot/index.html | 121 +++++++++-- .../Entities/PasswordResetToken.cs | 10 + docker-compose.yml | 22 +- 13 files changed, 528 insertions(+), 52 deletions(-) create mode 100644 .env.example create mode 100644 CalendarReminder.Api/Migrations/20260610183536_AddPasswordResetTokens.Designer.cs create mode 100644 CalendarReminder.Api/Migrations/20260610183536_AddPasswordResetTokens.cs create mode 100644 CalendarReminder.Domain/Entities/PasswordResetToken.cs diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..3a60c9c --- /dev/null +++ b/.env.example @@ -0,0 +1,19 @@ +# Skopíruj tento súbor ako .env a vyplň hodnoty +# cp .env.example .env + +# Databáza +DB_SERVER=192.168.1.68 +DB_PORT=3306 +DB_NAME=calendarreminder +DB_USER= +DB_PASSWORD= + +# JWT — minimálne 32 znakov, náhodný reťazec +JWT_SECRET= + +# Email SMTP +EMAIL_HOST=smtp.gmail.com +EMAIL_PORT=587 +EMAIL_USER= +EMAIL_PASS= +EMAIL_SSL=false diff --git a/.gitignore b/.gitignore index 9491a2f..0c5989f 100644 --- a/.gitignore +++ b/.gitignore @@ -360,4 +360,9 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file +FodyWeavers.xsd +# Secrets — NIKDY do gitu! +.env + +# Local dev settings (obsahuju heslá) +appsettings.Development.json diff --git a/CalendarReminder.Api/Controllers/AuthController.cs b/CalendarReminder.Api/Controllers/AuthController.cs index e4909b9..3c655e5 100644 --- a/CalendarReminder.Api/Controllers/AuthController.cs +++ b/CalendarReminder.Api/Controllers/AuthController.cs @@ -13,30 +13,30 @@ public class AuthController : ControllerBase { private readonly AppDbContext _db; private readonly TokenService _tokenService; + private readonly EmailService _emailService; - public AuthController(AppDbContext db, TokenService tokenService) + public AuthController(AppDbContext db, TokenService tokenService, EmailService emailService) { _db = db; _tokenService = tokenService; + _emailService = emailService; } - // POST api/auth/register + // POST api/auth/register (interný endpoint — nie je vystavený vo frontende) [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, + Email = req.Email, PasswordHash = BCrypt.Net.BCrypt.HashPassword(req.Password) }; - _db.Users.Add(user); await _db.SaveChangesAsync(); @@ -48,11 +48,90 @@ public class AuthController : ControllerBase [HttpPost("login")] public async Task Login(LoginRequest req) { - var user = await _db.Users.FirstOrDefaultAsync(u => u.Email == req.Email); + // Podpor prihlásenie cez email aj username + var user = await _db.Users.FirstOrDefaultAsync(u => + u.Email == req.Email || u.Username == req.Email); + if (user is null || !BCrypt.Net.BCrypt.Verify(req.Password, user.PasswordHash)) - return Unauthorized("Nesprávny email alebo heslo."); + return Unauthorized("Nesprávny email/meno alebo heslo."); var token = _tokenService.GenerateToken(user); return Ok(new AuthResponse(token, user.Username, user.Email)); } + + // POST api/auth/send-reset-code + // Vygeneruje 8-znakový kód a odošle ho emailom (pre budúce použitie) + [HttpPost("send-reset-code")] + public async Task SendResetCode([FromBody] SendResetCodeRequest req) + { + var user = await _db.Users.FirstOrDefaultAsync(u => u.Email == req.Email); + if (user is null) + return BadRequest("Email neexistuje."); + + // Zneplatni staré tokeny + var old = _db.PasswordResetTokens.Where(t => t.Email == req.Email && !t.Used); + _db.PasswordResetTokens.RemoveRange(old); + + // Vygeneruj nový 8-znakový kód + const string chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; + var rng = new Random(); + var code = new string(Enumerable.Range(0, 8).Select(_ => chars[rng.Next(chars.Length)]).ToArray()); + + _db.PasswordResetTokens.Add(new PasswordResetToken + { + Email = req.Email, + Code = code, + ExpiresAt = DateTime.UtcNow.AddMinutes(30), + Used = false + }); + await _db.SaveChangesAsync(); + + await _emailService.SendAsync(req.Email, user.Username, + "🔑 Obnova hesla — Calendar Reminder", + $""" +
+
+

🔑 Obnova hesla

+
+
+

Tvoj kód na obnovu hesla:

+
{code}
+

Kód je platný 30 minút. Ak si o reset nepožiadal, ignoruj tento email.

+
+
+ """); + + return Ok("Kód bol odoslaný na email."); + } + + // POST api/auth/reset-password + [HttpPost("reset-password")] + public async Task ResetPassword([FromBody] ResetPasswordRequest req) + { + var user = await _db.Users.FirstOrDefaultAsync(u => u.Email == req.Email); + if (user is null) + return BadRequest("Email neexistuje."); + + // Ak je kód zadaný — over ho + if (!string.IsNullOrEmpty(req.Code)) + { + var resetToken = await _db.PasswordResetTokens + .Where(t => t.Email == req.Email && !t.Used && t.ExpiresAt > DateTime.UtcNow) + .OrderByDescending(t => t.ExpiresAt) + .FirstOrDefaultAsync(); + + if (resetToken is null || resetToken.Code != req.Code.ToUpper()) + return BadRequest("Nesprávny alebo expirovaný kód."); + + resetToken.Used = true; + } + + user.PasswordHash = BCrypt.Net.BCrypt.HashPassword(req.NewPassword); + await _db.SaveChangesAsync(); + + return Ok("Heslo bolo úspešne zmenené."); + } } + +public record SendResetCodeRequest(string Email); +public record ResetPasswordRequest(string Email, string NewPassword, string? Code); diff --git a/CalendarReminder.Api/Data/AppDbContext.cs b/CalendarReminder.Api/Data/AppDbContext.cs index e2649f5..51dfdd2 100644 --- a/CalendarReminder.Api/Data/AppDbContext.cs +++ b/CalendarReminder.Api/Data/AppDbContext.cs @@ -10,6 +10,7 @@ public class AppDbContext : DbContext public DbSet Reminders => Set(); public DbSet Users => Set(); public DbSet ReminderShares => Set(); + public DbSet PasswordResetTokens => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/CalendarReminder.Api/Migrations/20260610183536_AddPasswordResetTokens.Designer.cs b/CalendarReminder.Api/Migrations/20260610183536_AddPasswordResetTokens.Designer.cs new file mode 100644 index 0000000..c0ec1ed --- /dev/null +++ b/CalendarReminder.Api/Migrations/20260610183536_AddPasswordResetTokens.Designer.cs @@ -0,0 +1,196 @@ +// +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("20260610183536_AddPasswordResetTokens")] + partial class AddPasswordResetTokens + { + /// + 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.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("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/20260610183536_AddPasswordResetTokens.cs b/CalendarReminder.Api/Migrations/20260610183536_AddPasswordResetTokens.cs new file mode 100644 index 0000000..819c66a --- /dev/null +++ b/CalendarReminder.Api/Migrations/20260610183536_AddPasswordResetTokens.cs @@ -0,0 +1,42 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CalendarReminder.Api.Migrations +{ + /// + public partial class AddPasswordResetTokens : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "PasswordResetTokens", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Email = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Code = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + ExpiresAt = table.Column(type: "datetime(6)", nullable: false), + Used = table.Column(type: "tinyint(1)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PasswordResetTokens", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PasswordResetTokens"); + } + } +} diff --git a/CalendarReminder.Api/Migrations/AppDbContextModelSnapshot.cs b/CalendarReminder.Api/Migrations/AppDbContextModelSnapshot.cs index 6959c54..098dad7 100644 --- a/CalendarReminder.Api/Migrations/AppDbContextModelSnapshot.cs +++ b/CalendarReminder.Api/Migrations/AppDbContextModelSnapshot.cs @@ -22,6 +22,33 @@ namespace CalendarReminder.Api.Migrations MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + 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") diff --git a/CalendarReminder.Api/Program.cs b/CalendarReminder.Api/Program.cs index 1be72d5..82bc970 100644 --- a/CalendarReminder.Api/Program.cs +++ b/CalendarReminder.Api/Program.cs @@ -41,11 +41,22 @@ builder.Services.AddHostedService(); var app = builder.Build(); -// Auto-migrate pri štarte +// Auto-migrate + seed admin using (var scope = app.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); db.Database.Migrate(); + + if (!db.Users.Any()) + { + db.Users.Add(new CalendarReminder.Domain.Entities.User + { + Username = "admin", + Email = "admin@local", + PasswordHash = BCrypt.Net.BCrypt.HashPassword("admin") + }); + db.SaveChanges(); + } } if (app.Environment.IsDevelopment()) diff --git a/CalendarReminder.Api/appsettings.Development.json b/CalendarReminder.Api/appsettings.Development.json index 0c208ae..91527c0 100644 --- a/CalendarReminder.Api/appsettings.Development.json +++ b/CalendarReminder.Api/appsettings.Development.json @@ -1,8 +1,13 @@ { - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } + "ConnectionStrings": { + "Default": "Server=192.168.1.68;Port=3306;Database=calendarreminder;User=mip;Password=mip@2013;" + }, + "Jwt": { + "Secret": "Hhb$bHfHsi@*lqxd1noC&DPK!$SL8fy9x" + }, + "Email": { + "Username": "nas@culak.sk", + "Password": "qyoq taqy vwpb tyiq", + "From": "nas@culak.sk" } } diff --git a/CalendarReminder.Api/appsettings.json b/CalendarReminder.Api/appsettings.json index 7c787ae..582ca2b 100644 --- a/CalendarReminder.Api/appsettings.json +++ b/CalendarReminder.Api/appsettings.json @@ -7,19 +7,19 @@ }, "AllowedHosts": "*", "ConnectionStrings": { - "Default": "Server=192.168.1.68;Port=3306;Database=calendarreminder;User=mip;Password=mip@2013;" + "Default": "" }, "Jwt": { - "Secret": "Hhb$bHfHsi@*lqxd1noC&DPK!$SL8fy9x", + "Secret": "", "Issuer": "CalendarReminder", "Audience": "CalendarReminderUsers" }, "Email": { "Host": "smtp.gmail.com", - "Port": 465, - "Username": "nas@culak.sk", - "Password": "ggTdYzhRfFlK3RoiojOr", - "From": "nas@culak.sk", - "UseSsl": true + "Port": 587, + "Username": "", + "Password": "", + "From": "", + "UseSsl": false } } diff --git a/CalendarReminder.Api/wwwroot/index.html b/CalendarReminder.Api/wwwroot/index.html index e32f3f9..28d7757 100644 --- a/CalendarReminder.Api/wwwroot/index.html +++ b/CalendarReminder.Api/wwwroot/index.html @@ -30,9 +30,18 @@ .auth-logo .logo-icon { font-size: 1.8rem; } .auth-logo h1 { font-size: 1.45rem; color: #58a6ff; font-weight: 700; letter-spacing: -0.3px; } .auth-card > p { color: #8b949e; font-size: 0.88rem; margin-bottom: 1.75rem; padding-left: 2.4rem; } - .tabs { display: flex; gap: 0.5rem; margin-bottom: 1.5rem; } - .tab-btn { flex: 1; padding: 0.5rem; border: 1px solid #30363d; border-radius: 7px; background: transparent; color: #8b949e; cursor: pointer; font-size: 0.9rem; transition: all 0.15s; } - .tab-btn.active { background: #1f6feb; border-color: #1f6feb; color: #fff; font-weight: 600; } + .forgot-link { font-size: 0.8rem; color: #58a6ff; cursor: pointer; text-align: right; margin-top: -0.5rem; margin-bottom: 1rem; display: block; background: none; border: none; padding: 0; } + .forgot-link:hover { text-decoration: underline; } + .back-link { font-size: 0.8rem; color: #8b949e; cursor: pointer; background: none; border: none; padding: 0; margin-bottom: 1.25rem; display: flex; align-items: center; gap: 0.3rem; } + .back-link:hover { color: #c9d1d9; } + .reset-title { font-size: 0.95rem; font-weight: 600; color: #c9d1d9; margin-bottom: 0.25rem; } + .reset-subtitle { font-size: 0.82rem; color: #8b949e; margin-bottom: 1.25rem; } + .send-code-row { display: flex; gap: 0.5rem; margin-bottom: 0.9rem; } + .send-code-row input { flex: 1; } + .btn-send-code { padding: 0.6rem 0.85rem; background: #1f6feb22; border: 1px solid #1f6feb55; border-radius: 7px; color: #58a6ff; cursor: pointer; font-size: 0.82rem; white-space: nowrap; transition: all 0.15s; } + .btn-send-code:hover { background: #1f6feb44; } + .btn-send-code:disabled { opacity: 0.4; cursor: not-allowed; } + .code-section { display: none; } .fg { margin-bottom: 0.9rem; } .fg label { display: block; font-size: 0.8rem; color: #8b949e; margin-bottom: 0.35rem; text-transform: uppercase; letter-spacing: 0.4px; } .fg input { width: 100%; padding: 0.6rem 0.85rem; background: #0d1117; border: 1px solid #30363d; border-radius: 7px; color: #e1e4e8; font-size: 0.95rem; outline: none; transition: border-color 0.15s; } @@ -349,23 +358,38 @@

Calendar Reminder

Správa udalostí s emailovými notifikáciami

-
- - -
+ +
-
+
+
- @@ -499,28 +523,79 @@ calMonth = now0.getMonth(); if (token) showApp(); // ═══ AUTH ═══ -function showTab(t) { - document.querySelectorAll('.tab-btn').forEach((b,i) => b.classList.toggle('active', i===(t==='login'?0:1))); - document.getElementById('login-form').style.display = t==='login' ? 'block':'none'; - document.getElementById('register-form').style.display = t==='register' ? 'block':'none'; +function showLogin() { + document.getElementById('login-form').style.display = 'block'; + document.getElementById('reset-form').style.display = 'none'; + document.getElementById('l-err').textContent = ''; +} + +function showReset() { + document.getElementById('login-form').style.display = 'none'; + document.getElementById('reset-form').style.display = 'block'; + document.getElementById('r-err').textContent = ''; + document.getElementById('reset-subtitle').textContent = 'Zadaj email a nové heslo.'; } async function doLogin() { + const err = document.getElementById('l-err'); + err.textContent = ''; const res = await fetch(`${API}/auth/login`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ email: v('l-email'), password: v('l-pass') }) }); if (res.ok) { const d = await res.json(); saveAuth(d); showApp(); } - else document.getElementById('l-err').textContent = await res.text(); + else err.textContent = await res.text(); } -async function doRegister() { - const res = await fetch(`${API}/auth/register`, { +// Zaslanie kódu — aktivuje sa keď bude mailová služba funkčná +const RESET_WITH_CODE = true; + +async function sendResetCode() { + const email = v('r-email').trim(); + if (!email) { document.getElementById('r-err').textContent = 'Zadaj email.'; return; } + const btn = document.getElementById('btn-send-code'); + btn.disabled = true; + const res = await fetch(`${API}/auth/send-reset-code`, { method:'POST', headers:{'Content-Type':'application/json'}, - body: JSON.stringify({ username: v('r-name'), email: v('r-email'), password: v('r-pass') }) + body: JSON.stringify({ email }) }); - if (res.ok) { const d = await res.json(); saveAuth(d); showApp(); } - else document.getElementById('r-err').textContent = await res.text(); + btn.disabled = false; + if (res.ok) { + document.getElementById('code-section').style.display = 'block'; + document.getElementById('reset-subtitle').textContent = 'Kód bol odoslaný na email. Zadaj ho nižšie.'; + document.getElementById('r-err').style.color = '#3fb950'; + document.getElementById('r-err').textContent = '✓ Kód odoslaný'; + } else { + document.getElementById('r-err').style.color = '#f85149'; + document.getElementById('r-err').textContent = await res.text(); + } +} + +async function doResetPassword() { + const err = document.getElementById('r-err'); + err.style.color = '#f85149'; + err.textContent = ''; + const email = v('r-email').trim(); + const pass = v('r-newpass'); + const pass2 = v('r-newpass2'); + const code = v('r-code').trim().toUpperCase(); + + if (!email) { err.textContent = 'Zadaj email.'; return; } + if (pass.length < 6) { err.textContent = 'Heslo musí mať aspoň 6 znakov.'; return; } + if (pass !== pass2) { err.textContent = 'Heslá sa nezhodujú.'; return; } + if (RESET_WITH_CODE && !code) { err.textContent = 'Zadaj kód z emailu.'; return; } + + const res = await fetch(`${API}/auth/reset-password`, { + method:'POST', headers:{'Content-Type':'application/json'}, + body: JSON.stringify({ email, newPassword: pass, code: RESET_WITH_CODE ? code : null }) + }); + if (res.ok) { + err.style.color = '#3fb950'; + err.textContent = '✓ Heslo bolo zmenené. Môžeš sa prihlásiť.'; + setTimeout(showLogin, 2000); + } else { + err.textContent = await res.text(); + } } function saveAuth(d) { diff --git a/CalendarReminder.Domain/Entities/PasswordResetToken.cs b/CalendarReminder.Domain/Entities/PasswordResetToken.cs new file mode 100644 index 0000000..2820a2f --- /dev/null +++ b/CalendarReminder.Domain/Entities/PasswordResetToken.cs @@ -0,0 +1,10 @@ +namespace CalendarReminder.Domain.Entities; + +public class PasswordResetToken +{ + public int Id { get; set; } + public string Email { get; set; } = ""; + public string Code { get; set; } = ""; // 8 znakov + public DateTime ExpiresAt { get; set; } + public bool Used { get; set; } +} diff --git a/docker-compose.yml b/docker-compose.yml index 04a5b85..62bac06 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,20 +4,26 @@ services: 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 + - ConnectionStrings__Default=Server=${DB_SERVER};Port=${DB_PORT};Database=${DB_NAME};User=${DB_USER};Password=${DB_PASSWORD}; + - Jwt__Secret=${JWT_SECRET} + - Jwt__Issuer=CalendarReminder + - Jwt__Audience=CalendarReminderUsers + - Email__Host=${EMAIL_HOST} + - Email__Port=${EMAIL_PORT} + - Email__Username=${EMAIL_USER} + - Email__Password=${EMAIL_PASS} + - Email__From=${EMAIL_USER} + - Email__UseSsl=${EMAIL_SSL} restart: unless-stopped db: image: mariadb:11 environment: - MARIADB_ROOT_PASSWORD: rootpass - MARIADB_DATABASE: calendarreminder - MARIADB_USER: appuser - MARIADB_PASSWORD: secret + - MARIADB_ROOT_PASSWORD=${DB_PASSWORD} + - MARIADB_DATABASE=${DB_NAME} + - MARIADB_USER=${DB_USER} + - MARIADB_PASSWORD=${DB_PASSWORD} volumes: - /mnt/tank/calendarreminder/mariadb:/var/lib/mysql healthcheck: