login changes
This commit is contained in:
@@ -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
|
||||
@@ -361,3 +361,8 @@ MigrationBackup/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
# Secrets — NIKDY do gitu!
|
||||
.env
|
||||
|
||||
# Local dev settings (obsahuju heslá)
|
||||
appsettings.Development.json
|
||||
|
||||
@@ -13,20 +13,21 @@ 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<IActionResult> 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é.");
|
||||
|
||||
@@ -36,7 +37,6 @@ public class AuthController : ControllerBase
|
||||
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<IActionResult> 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<IActionResult> 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",
|
||||
$"""
|
||||
<div style="font-family:Segoe UI,sans-serif;max-width:480px;margin:auto;background:#161b22;color:#e1e4e8;border-radius:10px;overflow:hidden">
|
||||
<div style="background:#1f6feb;padding:1.2rem 1.5rem">
|
||||
<h2 style="margin:0;color:#fff;font-size:1.1rem">🔑 Obnova hesla</h2>
|
||||
</div>
|
||||
<div style="padding:1.5rem">
|
||||
<p style="margin:0 0 1rem">Tvoj kód na obnovu hesla:</p>
|
||||
<div style="background:#0d1117;border:1px solid #30363d;border-radius:8px;padding:1rem;text-align:center;font-size:2rem;font-weight:700;letter-spacing:0.4rem;color:#58a6ff">{code}</div>
|
||||
<p style="margin:1rem 0 0;color:#8b949e;font-size:0.85rem">Kód je platný 30 minút. Ak si o reset nepožiadal, ignoruj tento email.</p>
|
||||
</div>
|
||||
</div>
|
||||
""");
|
||||
|
||||
return Ok("Kód bol odoslaný na email.");
|
||||
}
|
||||
|
||||
// POST api/auth/reset-password
|
||||
[HttpPost("reset-password")]
|
||||
public async Task<IActionResult> 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);
|
||||
|
||||
@@ -10,6 +10,7 @@ public class AppDbContext : DbContext
|
||||
public DbSet<Reminder> Reminders => Set<Reminder>();
|
||||
public DbSet<User> Users => Set<User>();
|
||||
public DbSet<ReminderShare> ReminderShares => Set<ReminderShare>();
|
||||
public DbSet<PasswordResetToken> PasswordResetTokens => Set<PasswordResetToken>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
// <auto-generated />
|
||||
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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<DateTime>("ExpiresAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<bool>("Used")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PasswordResetTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CalendarReminder.Domain.Entities.Reminder", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("varchar(1000)");
|
||||
|
||||
b.Property<bool>("IsCompleted")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool>("NotificationSent")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<DateTime>("ReminderDate")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Reminders");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CalendarReminder.Domain.Entities.ReminderShare", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("ReminderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace CalendarReminder.Api.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddPasswordResetTokens : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PasswordResetTokens",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
Email = table.Column<string>(type: "longtext", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Code = table.Column<string>(type: "longtext", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
ExpiresAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
Used = table.Column<bool>(type: "tinyint(1)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PasswordResetTokens", x => x.Id);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "PasswordResetTokens");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,33 @@ namespace CalendarReminder.Api.Migrations
|
||||
|
||||
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("CalendarReminder.Domain.Entities.PasswordResetToken", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<DateTime>("ExpiresAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<bool>("Used")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PasswordResetTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CalendarReminder.Domain.Entities.Reminder", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
||||
@@ -41,11 +41,22 @@ builder.Services.AddHostedService<ReminderNotificationService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Auto-migrate pri štarte
|
||||
// Auto-migrate + seed admin
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
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())
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 @@
|
||||
<h1>Calendar Reminder</h1>
|
||||
</div>
|
||||
<p>Správa udalostí s emailovými notifikáciami</p>
|
||||
<div class="tabs">
|
||||
<button class="tab-btn active" onclick="showTab('login')">Prihlásiť sa</button>
|
||||
<button class="tab-btn" onclick="showTab('register')">Registrácia</button>
|
||||
</div>
|
||||
|
||||
<!-- LOGIN -->
|
||||
<div id="login-form">
|
||||
<div class="fg"><label>Email</label><input type="email" id="l-email" placeholder="vas@email.com" /></div>
|
||||
<div class="fg"><label>Email alebo meno</label><input type="text" id="l-email" placeholder="admin" /></div>
|
||||
<div class="fg"><label>Heslo</label><input type="password" id="l-pass" placeholder="••••••••" onkeydown="if(event.key==='Enter')doLogin()" /></div>
|
||||
<button class="btn-primary" onclick="doLogin()">Prihlásiť sa</button>
|
||||
<button class="forgot-link" onclick="showReset()">Zabudol som heslo</button>
|
||||
<div class="err" id="l-err"></div>
|
||||
</div>
|
||||
<div id="register-form" style="display:none">
|
||||
<div class="fg"><label>Meno</label><input type="text" id="r-name" placeholder="Ján Novák" /></div>
|
||||
<div class="fg"><label>Email</label><input type="email" id="r-email" placeholder="vas@email.com" /></div>
|
||||
<div class="fg"><label>Heslo</label><input type="password" id="r-pass" placeholder="min. 6 znakov" onkeydown="if(event.key==='Enter')doRegister()" /></div>
|
||||
<button class="btn-primary" onclick="doRegister()">Vytvoriť účet</button>
|
||||
|
||||
<!-- RESET HESLA -->
|
||||
<div id="reset-form" style="display:none">
|
||||
<button class="back-link" onclick="showLogin()">← Späť na prihlásenie</button>
|
||||
<div class="reset-title">Obnova hesla</div>
|
||||
<div class="reset-subtitle" id="reset-subtitle">Zadaj email a nové heslo.</div>
|
||||
|
||||
<div class="send-code-row">
|
||||
<input type="email" id="r-email" placeholder="Email *" />
|
||||
<button class="btn-send-code" id="btn-send-code" onclick="sendResetCode()">Zaslať kód</button>
|
||||
</div>
|
||||
|
||||
<!-- Kód — skrytý, zobrazí sa po aktivácii mailovej služby -->
|
||||
<div class="code-section" id="code-section">
|
||||
<div class="fg"><label>Kód z emailu (8 znakov)</label><input type="text" id="r-code" placeholder="XXXXXXXX" maxlength="8" style="text-transform:uppercase;letter-spacing:0.3rem;font-weight:700" /></div>
|
||||
</div>
|
||||
|
||||
<div class="fg"><label>Nové heslo</label><input type="password" id="r-newpass" placeholder="min. 6 znakov" /></div>
|
||||
<div class="fg"><label>Potvrď heslo</label><input type="password" id="r-newpass2" placeholder="zopakuj heslo" onkeydown="if(event.key==='Enter')doResetPassword()" /></div>
|
||||
<button class="btn-primary" onclick="doResetPassword()">Zmeniť heslo</button>
|
||||
<div class="err" id="r-err"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
+14
-8
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user