Compare commits

..
2 Commits
Author SHA1 Message Date
roman 639baafe4a admin fix 2026-06-10 23:00:58 +02:00
roman 43011f0a77 login changes 2026-06-10 20:39:52 +02:00
14 changed files with 559 additions and 53 deletions
+19
View File
@@ -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
+5
View File
@@ -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)
{
@@ -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")
+12 -1
View File
@@ -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 -7
View File
@@ -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
}
}
+110 -24
View File
@@ -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="forgot-link" onclick="showReset()">Zabudol som heslo</button>
<button class="btn-primary" onclick="doLogin()">Prihlásiť sa</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,90 @@ 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();
if (res.ok) {
const d = await res.json();
saveAuth(d);
// Reset všetkých auth inputov
['l-email','l-pass','r-email','r-newpass','r-newpass2','r-code'].forEach(id => {
const el = document.getElementById(id);
if (el) el.value = '';
});
document.getElementById('l-err').textContent = '';
document.getElementById('r-err').textContent = '';
showLogin();
showApp();
} 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; }
}
+19
View File
@@ -0,0 +1,19 @@
-- ============================================================
-- CalendarReminder — vytvorenie admin používateľa
-- Heslo: Admin@2025
-- Spusti: mysql -h 192.168.1.68 -u mip -p"mip@2013" calendarreminder < create-admin.sql
-- ============================================================
INSERT INTO Users (Username, Email, PasswordHash, CreatedAt)
VALUES (
'admin',
'nas@culak.sk',
'$2a$11$8yPi0lcuKsJH6/Ncvm2A6O4Nw9gZblkCZ23PxbibW5MVfG06syp5S',
UTC_TIMESTAMP()
)
ON DUPLICATE KEY UPDATE
Email = VALUES(Email),
PasswordHash = VALUES(PasswordHash);
-- Over výsledok
SELECT id, Username, Email, CreatedAt FROM Users WHERE Username = 'admin';
+14 -8
View File
@@ -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: