Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
639baafe4a | ||
|
|
43011f0a77 |
@@ -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
|
# Fody - auto-generated XML schema
|
||||||
FodyWeavers.xsd
|
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 AppDbContext _db;
|
||||||
private readonly TokenService _tokenService;
|
private readonly TokenService _tokenService;
|
||||||
|
private readonly EmailService _emailService;
|
||||||
|
|
||||||
public AuthController(AppDbContext db, TokenService tokenService)
|
public AuthController(AppDbContext db, TokenService tokenService, EmailService emailService)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
_tokenService = tokenService;
|
_tokenService = tokenService;
|
||||||
|
_emailService = emailService;
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST api/auth/register
|
// POST api/auth/register (interný endpoint — nie je vystavený vo frontende)
|
||||||
[HttpPost("register")]
|
[HttpPost("register")]
|
||||||
public async Task<IActionResult> Register(RegisterRequest req)
|
public async Task<IActionResult> Register(RegisterRequest req)
|
||||||
{
|
{
|
||||||
if (await _db.Users.AnyAsync(u => u.Email == req.Email))
|
if (await _db.Users.AnyAsync(u => u.Email == req.Email))
|
||||||
return BadRequest("Email je už registrovaný.");
|
return BadRequest("Email je už registrovaný.");
|
||||||
|
|
||||||
if (await _db.Users.AnyAsync(u => u.Username == req.Username))
|
if (await _db.Users.AnyAsync(u => u.Username == req.Username))
|
||||||
return BadRequest("Používateľské meno je obsadené.");
|
return BadRequest("Používateľské meno je obsadené.");
|
||||||
|
|
||||||
@@ -36,7 +37,6 @@ public class AuthController : ControllerBase
|
|||||||
Email = req.Email,
|
Email = req.Email,
|
||||||
PasswordHash = BCrypt.Net.BCrypt.HashPassword(req.Password)
|
PasswordHash = BCrypt.Net.BCrypt.HashPassword(req.Password)
|
||||||
};
|
};
|
||||||
|
|
||||||
_db.Users.Add(user);
|
_db.Users.Add(user);
|
||||||
await _db.SaveChangesAsync();
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
@@ -48,11 +48,90 @@ public class AuthController : ControllerBase
|
|||||||
[HttpPost("login")]
|
[HttpPost("login")]
|
||||||
public async Task<IActionResult> Login(LoginRequest req)
|
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))
|
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);
|
var token = _tokenService.GenerateToken(user);
|
||||||
return Ok(new AuthResponse(token, user.Username, user.Email));
|
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<Reminder> Reminders => Set<Reminder>();
|
||||||
public DbSet<User> Users => Set<User>();
|
public DbSet<User> Users => Set<User>();
|
||||||
public DbSet<ReminderShare> ReminderShares => Set<ReminderShare>();
|
public DbSet<ReminderShare> ReminderShares => Set<ReminderShare>();
|
||||||
|
public DbSet<PasswordResetToken> PasswordResetTokens => Set<PasswordResetToken>();
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
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);
|
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 =>
|
modelBuilder.Entity("CalendarReminder.Domain.Entities.Reminder", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
|
|||||||
@@ -41,11 +41,22 @@ builder.Services.AddHostedService<ReminderNotificationService>();
|
|||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Auto-migrate pri štarte
|
// Auto-migrate + seed admin
|
||||||
using (var scope = app.Services.CreateScope())
|
using (var scope = app.Services.CreateScope())
|
||||||
{
|
{
|
||||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
db.Database.Migrate();
|
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())
|
if (app.Environment.IsDevelopment())
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
{
|
{
|
||||||
"Logging": {
|
"ConnectionStrings": {
|
||||||
"LogLevel": {
|
"Default": "Server=192.168.1.68;Port=3306;Database=calendarreminder;User=mip;Password=mip@2013;"
|
||||||
"Default": "Information",
|
},
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"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": "*",
|
"AllowedHosts": "*",
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"Default": "Server=192.168.1.68;Port=3306;Database=calendarreminder;User=mip;Password=mip@2013;"
|
"Default": ""
|
||||||
},
|
},
|
||||||
"Jwt": {
|
"Jwt": {
|
||||||
"Secret": "Hhb$bHfHsi@*lqxd1noC&DPK!$SL8fy9x",
|
"Secret": "",
|
||||||
"Issuer": "CalendarReminder",
|
"Issuer": "CalendarReminder",
|
||||||
"Audience": "CalendarReminderUsers"
|
"Audience": "CalendarReminderUsers"
|
||||||
},
|
},
|
||||||
"Email": {
|
"Email": {
|
||||||
"Host": "smtp.gmail.com",
|
"Host": "smtp.gmail.com",
|
||||||
"Port": 465,
|
"Port": 587,
|
||||||
"Username": "nas@culak.sk",
|
"Username": "",
|
||||||
"Password": "ggTdYzhRfFlK3RoiojOr",
|
"Password": "",
|
||||||
"From": "nas@culak.sk",
|
"From": "",
|
||||||
"UseSsl": true
|
"UseSsl": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,9 +30,18 @@
|
|||||||
.auth-logo .logo-icon { font-size: 1.8rem; }
|
.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-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; }
|
.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; }
|
.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; }
|
||||||
.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; }
|
.forgot-link:hover { text-decoration: underline; }
|
||||||
.tab-btn.active { background: #1f6feb; border-color: #1f6feb; color: #fff; font-weight: 600; }
|
.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 { 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 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; }
|
.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>
|
<h1>Calendar Reminder</h1>
|
||||||
</div>
|
</div>
|
||||||
<p>Správa udalostí s emailovými notifikáciami</p>
|
<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>
|
<!-- LOGIN -->
|
||||||
<button class="tab-btn" onclick="showTab('register')">Registrácia</button>
|
|
||||||
</div>
|
|
||||||
<div id="login-form">
|
<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>
|
<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>
|
<button class="btn-primary" onclick="doLogin()">Prihlásiť sa</button>
|
||||||
<div class="err" id="l-err"></div>
|
<div class="err" id="l-err"></div>
|
||||||
</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>
|
<!-- RESET HESLA -->
|
||||||
<div class="fg"><label>Email</label><input type="email" id="r-email" placeholder="vas@email.com" /></div>
|
<div id="reset-form" style="display:none">
|
||||||
<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="back-link" onclick="showLogin()">← Späť na prihlásenie</button>
|
||||||
<button class="btn-primary" onclick="doRegister()">Vytvoriť účet</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 class="err" id="r-err"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -499,28 +523,90 @@ calMonth = now0.getMonth();
|
|||||||
if (token) showApp();
|
if (token) showApp();
|
||||||
|
|
||||||
// ═══ AUTH ═══
|
// ═══ AUTH ═══
|
||||||
function showTab(t) {
|
function showLogin() {
|
||||||
document.querySelectorAll('.tab-btn').forEach((b,i) => b.classList.toggle('active', i===(t==='login'?0:1)));
|
document.getElementById('login-form').style.display = 'block';
|
||||||
document.getElementById('login-form').style.display = t==='login' ? 'block':'none';
|
document.getElementById('reset-form').style.display = 'none';
|
||||||
document.getElementById('register-form').style.display = t==='register' ? 'block':'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() {
|
async function doLogin() {
|
||||||
|
const err = document.getElementById('l-err');
|
||||||
|
err.textContent = '';
|
||||||
const res = await fetch(`${API}/auth/login`, {
|
const res = await fetch(`${API}/auth/login`, {
|
||||||
method:'POST', headers:{'Content-Type':'application/json'},
|
method:'POST', headers:{'Content-Type':'application/json'},
|
||||||
body: JSON.stringify({ email: v('l-email'), password: v('l-pass') })
|
body: JSON.stringify({ email: v('l-email'), password: v('l-pass') })
|
||||||
});
|
});
|
||||||
if (res.ok) { const d = await res.json(); saveAuth(d); showApp(); }
|
if (res.ok) {
|
||||||
else document.getElementById('l-err').textContent = await res.text();
|
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() {
|
// Zaslanie kódu — aktivuje sa keď bude mailová služba funkčná
|
||||||
const res = await fetch(`${API}/auth/register`, {
|
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'},
|
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(); }
|
btn.disabled = false;
|
||||||
else document.getElementById('r-err').textContent = await res.text();
|
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) {
|
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; }
|
||||||
|
}
|
||||||
@@ -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
@@ -4,20 +4,26 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
environment:
|
environment:
|
||||||
- ConnectionStrings__Default=Server=db;Port=3306;Database=calendarreminder;User=appuser;Password=secret;
|
|
||||||
- ASPNETCORE_ENVIRONMENT=Production
|
- ASPNETCORE_ENVIRONMENT=Production
|
||||||
depends_on:
|
- ConnectionStrings__Default=Server=${DB_SERVER};Port=${DB_PORT};Database=${DB_NAME};User=${DB_USER};Password=${DB_PASSWORD};
|
||||||
db:
|
- Jwt__Secret=${JWT_SECRET}
|
||||||
condition: service_healthy
|
- 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
|
restart: unless-stopped
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: mariadb:11
|
image: mariadb:11
|
||||||
environment:
|
environment:
|
||||||
MARIADB_ROOT_PASSWORD: rootpass
|
- MARIADB_ROOT_PASSWORD=${DB_PASSWORD}
|
||||||
MARIADB_DATABASE: calendarreminder
|
- MARIADB_DATABASE=${DB_NAME}
|
||||||
MARIADB_USER: appuser
|
- MARIADB_USER=${DB_USER}
|
||||||
MARIADB_PASSWORD: secret
|
- MARIADB_PASSWORD=${DB_PASSWORD}
|
||||||
volumes:
|
volumes:
|
||||||
- /mnt/tank/calendarreminder/mariadb:/var/lib/mysql
|
- /mnt/tank/calendarreminder/mariadb:/var/lib/mysql
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|||||||
Reference in New Issue
Block a user