Auth integration
This commit is contained in:
@@ -3,6 +3,7 @@ namespace timetracker.Data;
|
||||
public class AppSettings
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int UserId { get; set; }
|
||||
public double DailyTargetHours { get; set; } = 7.5;
|
||||
public int MinimumBreakMinutes { get; set; } = 30;
|
||||
public int VacationDaysPerYear { get; set; } = 30;
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace timetracker.Data;
|
||||
|
||||
public class AuthService(IDbContextFactory<TimetrackerDbContext> factory)
|
||||
{
|
||||
public async Task<User?> LoginAsync(string username, string password)
|
||||
{
|
||||
await using var db = await factory.CreateDbContextAsync();
|
||||
var user = await db.Users
|
||||
.FirstOrDefaultAsync(u => u.Username == username);
|
||||
if (user == null) return null;
|
||||
return VerifyPassword(password, user.PasswordHash, user.PasswordSalt) ? user : null;
|
||||
}
|
||||
|
||||
public async Task<(User? User, string? Error)> RegisterAsync(string username, string password)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(username) || username.Length < 3)
|
||||
return (null, "Benutzername muss mindestens 3 Zeichen lang sein.");
|
||||
if (string.IsNullOrWhiteSpace(password) || password.Length < 6)
|
||||
return (null, "Passwort muss mindestens 6 Zeichen lang sein.");
|
||||
|
||||
await using var db = await factory.CreateDbContextAsync();
|
||||
if (await db.Users.AnyAsync(u => u.Username == username))
|
||||
return (null, "Benutzername bereits vergeben.");
|
||||
|
||||
var (hash, salt) = HashPassword(password);
|
||||
var user = new User { Username = username, PasswordHash = hash, PasswordSalt = salt };
|
||||
db.Users.Add(user);
|
||||
await db.SaveChangesAsync();
|
||||
return (user, null);
|
||||
}
|
||||
|
||||
private static (string hash, string salt) HashPassword(string password)
|
||||
{
|
||||
var saltBytes = RandomNumberGenerator.GetBytes(32);
|
||||
var salt = Convert.ToBase64String(saltBytes);
|
||||
var hash = ComputeHash(password, salt);
|
||||
return (hash, salt);
|
||||
}
|
||||
|
||||
private static bool VerifyPassword(string password, string hash, string salt)
|
||||
=> ComputeHash(password, salt) == hash;
|
||||
|
||||
private static string ComputeHash(string password, string salt)
|
||||
{
|
||||
var hash = Rfc2898DeriveBytes.Pbkdf2(
|
||||
Encoding.UTF8.GetBytes(password),
|
||||
Convert.FromBase64String(salt),
|
||||
iterations: 200_000,
|
||||
HashAlgorithmName.SHA256,
|
||||
outputLength: 32);
|
||||
return Convert.ToBase64String(hash);
|
||||
}
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using timetracker.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace timetracker.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(TimetrackerDbContext))]
|
||||
[Migration("20260522081459_AddMultiUser")]
|
||||
partial class AddMultiUser
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.8");
|
||||
|
||||
modelBuilder.Entity("timetracker.Data.AppSettings", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<double>("DailyTargetHours")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<int>("MinimumBreakMinutes")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("VacationDaysPerYear")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("WorkFriday")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("WorkMonday")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("WorkSaturday")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("WorkSunday")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("WorkThursday")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("WorkTuesday")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("WorkWednesday")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("AppSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("timetracker.Data.BreakEntry", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<TimeOnly?>("EndTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<TimeOnly?>("StartTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("WorkDayId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("WorkDayId");
|
||||
|
||||
b.ToTable("BreakEntries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("timetracker.Data.PublicHoliday", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateOnly>("Date")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PublicHolidays");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("timetracker.Data.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordSalt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("timetracker.Data.VacationDay", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateOnly>("Date")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("VacationDays");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("timetracker.Data.WorkDay", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateOnly>("Date")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<TimeOnly?>("EndTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<TimeOnly?>("StartTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("WorkDays");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("timetracker.Data.BreakEntry", b =>
|
||||
{
|
||||
b.HasOne("timetracker.Data.WorkDay", "WorkDay")
|
||||
.WithMany("Breaks")
|
||||
.HasForeignKey("WorkDayId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("WorkDay");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("timetracker.Data.WorkDay", b =>
|
||||
{
|
||||
b.Navigation("Breaks");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace timetracker.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddMultiUser : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "UserId",
|
||||
table: "WorkDays",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "UserId",
|
||||
table: "VacationDays",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "UserId",
|
||||
table: "AppSettings",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Users",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Username = table.Column<string>(type: "TEXT", nullable: false),
|
||||
PasswordHash = table.Column<string>(type: "TEXT", nullable: false),
|
||||
PasswordSalt = table.Column<string>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Users", x => x.Id);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Users");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "UserId",
|
||||
table: "WorkDays");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "UserId",
|
||||
table: "VacationDays");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "UserId",
|
||||
table: "AppSettings");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,9 @@ namespace timetracker.Data.Migrations
|
||||
b.Property<int>("MinimumBreakMinutes")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("VacationDaysPerYear")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -58,24 +61,6 @@ namespace timetracker.Data.Migrations
|
||||
b.ToTable("AppSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("timetracker.Data.PublicHoliday", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateOnly>("Date")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PublicHolidays");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("timetracker.Data.BreakEntry", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -98,6 +83,47 @@ namespace timetracker.Data.Migrations
|
||||
b.ToTable("BreakEntries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("timetracker.Data.PublicHoliday", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateOnly>("Date")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PublicHolidays");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("timetracker.Data.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordSalt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("timetracker.Data.VacationDay", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -110,6 +136,9 @@ namespace timetracker.Data.Migrations
|
||||
b.Property<string>("Note")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("VacationDays");
|
||||
@@ -130,6 +159,9 @@ namespace timetracker.Data.Migrations
|
||||
b.Property<TimeOnly?>("StartTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("WorkDays");
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace timetracker.Data;
|
||||
|
||||
public class TimetrackerDbContext(DbContextOptions<TimetrackerDbContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<User> Users => Set<User>();
|
||||
public DbSet<WorkDay> WorkDays => Set<WorkDay>();
|
||||
public DbSet<BreakEntry> BreakEntries => Set<BreakEntry>();
|
||||
public DbSet<AppSettings> AppSettings => Set<AppSettings>();
|
||||
|
||||
+16
-16
@@ -4,12 +4,12 @@ namespace timetracker.Data;
|
||||
|
||||
public class TimetrackerService(IDbContextFactory<TimetrackerDbContext> factory)
|
||||
{
|
||||
public async Task<List<WorkDay>> GetWeekAsync(DateOnly monday)
|
||||
public async Task<List<WorkDay>> GetWeekAsync(int userId, DateOnly monday)
|
||||
{
|
||||
await using var db = await factory.CreateDbContextAsync();
|
||||
return await db.WorkDays
|
||||
.Include(w => w.Breaks)
|
||||
.Where(w => w.Date >= monday && w.Date < monday.AddDays(7))
|
||||
.Where(w => w.UserId == userId && w.Date >= monday && w.Date < monday.AddDays(7))
|
||||
.OrderBy(w => w.Date)
|
||||
.ToListAsync();
|
||||
}
|
||||
@@ -19,7 +19,7 @@ public class TimetrackerService(IDbContextFactory<TimetrackerDbContext> factory)
|
||||
await using var db = await factory.CreateDbContextAsync();
|
||||
var existing = await db.WorkDays
|
||||
.Include(w => w.Breaks)
|
||||
.FirstOrDefaultAsync(w => w.Date == workDay.Date);
|
||||
.FirstOrDefaultAsync(w => w.UserId == workDay.UserId && w.Date == workDay.Date);
|
||||
|
||||
if (existing == null)
|
||||
{
|
||||
@@ -45,17 +45,17 @@ public class TimetrackerService(IDbContextFactory<TimetrackerDbContext> factory)
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<AppSettings> GetSettingsAsync()
|
||||
public async Task<AppSettings> GetSettingsAsync(int userId)
|
||||
{
|
||||
await using var db = await factory.CreateDbContextAsync();
|
||||
return await db.AppSettings.FindAsync(1) ?? new AppSettings { Id = 1 };
|
||||
return await db.AppSettings.FirstOrDefaultAsync(s => s.UserId == userId)
|
||||
?? new AppSettings { UserId = userId };
|
||||
}
|
||||
|
||||
public async Task SaveSettingsAsync(AppSettings settings)
|
||||
{
|
||||
await using var db = await factory.CreateDbContextAsync();
|
||||
settings.Id = 1;
|
||||
var existing = await db.AppSettings.FindAsync(1);
|
||||
var existing = await db.AppSettings.FirstOrDefaultAsync(s => s.UserId == settings.UserId);
|
||||
if (existing == null)
|
||||
db.AppSettings.Add(settings);
|
||||
else
|
||||
@@ -75,11 +75,11 @@ public class TimetrackerService(IDbContextFactory<TimetrackerDbContext> factory)
|
||||
}
|
||||
|
||||
// ── Urlaub ────────────────────────────────────────────────────────────
|
||||
public async Task<List<VacationDay>> GetVacationDaysAsync(int year)
|
||||
public async Task<List<VacationDay>> GetVacationDaysAsync(int userId, int year)
|
||||
{
|
||||
await using var db = await factory.CreateDbContextAsync();
|
||||
return await db.VacationDays
|
||||
.Where(v => v.Date.Year == year)
|
||||
.Where(v => v.UserId == userId && v.Date.Year == year)
|
||||
.OrderBy(v => v.Date)
|
||||
.ToListAsync();
|
||||
}
|
||||
@@ -87,7 +87,7 @@ public class TimetrackerService(IDbContextFactory<TimetrackerDbContext> factory)
|
||||
public async Task AddVacationDayAsync(VacationDay vacationDay)
|
||||
{
|
||||
await using var db = await factory.CreateDbContextAsync();
|
||||
var exists = await db.VacationDays.AnyAsync(v => v.Date == vacationDay.Date);
|
||||
var exists = await db.VacationDays.AnyAsync(v => v.UserId == vacationDay.UserId && v.Date == vacationDay.Date);
|
||||
if (!exists)
|
||||
{
|
||||
vacationDay.Id = 0;
|
||||
@@ -96,10 +96,10 @@ public class TimetrackerService(IDbContextFactory<TimetrackerDbContext> factory)
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RemoveVacationDayAsync(int id)
|
||||
public async Task RemoveVacationDayAsync(int userId, int id)
|
||||
{
|
||||
await using var db = await factory.CreateDbContextAsync();
|
||||
var v = await db.VacationDays.FindAsync(id);
|
||||
var v = await db.VacationDays.FirstOrDefaultAsync(v => v.Id == id && v.UserId == userId);
|
||||
if (v != null)
|
||||
{
|
||||
db.VacationDays.Remove(v);
|
||||
@@ -108,12 +108,12 @@ public class TimetrackerService(IDbContextFactory<TimetrackerDbContext> factory)
|
||||
}
|
||||
|
||||
// ── Gleitzeitkonto ───────────────────────────────────────────────────
|
||||
public async Task<TimeSpan> GetTotalOvertimeAsync(AppSettings settings)
|
||||
public async Task<TimeSpan> GetTotalOvertimeAsync(int userId, AppSettings settings)
|
||||
{
|
||||
await using var db = await factory.CreateDbContextAsync();
|
||||
var allDays = await db.WorkDays
|
||||
.Include(w => w.Breaks)
|
||||
.Where(w => w.StartTime != null && w.EndTime != null)
|
||||
.Where(w => w.UserId == userId && w.StartTime != null && w.EndTime != null)
|
||||
.ToListAsync();
|
||||
|
||||
var total = TimeSpan.Zero;
|
||||
@@ -132,14 +132,14 @@ public class TimetrackerService(IDbContextFactory<TimetrackerDbContext> factory)
|
||||
}
|
||||
|
||||
// ── Monatsübersicht ───────────────────────────────────────────────────
|
||||
public async Task<List<WorkDay>> GetMonthAsync(int year, int month)
|
||||
public async Task<List<WorkDay>> GetMonthAsync(int userId, int year, int month)
|
||||
{
|
||||
await using var db = await factory.CreateDbContextAsync();
|
||||
var from = new DateOnly(year, month, 1);
|
||||
var to = from.AddMonths(1);
|
||||
return await db.WorkDays
|
||||
.Include(w => w.Breaks)
|
||||
.Where(w => w.Date >= from && w.Date < to)
|
||||
.Where(w => w.UserId == userId && w.Date >= from && w.Date < to)
|
||||
.OrderBy(w => w.Date)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace timetracker.Data;
|
||||
|
||||
public class User
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Username { get; set; } = "";
|
||||
public string PasswordHash { get; set; } = "";
|
||||
public string PasswordSalt { get; set; } = "";
|
||||
}
|
||||
@@ -3,6 +3,7 @@ namespace timetracker.Data;
|
||||
public class VacationDay
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int UserId { get; set; }
|
||||
public DateOnly Date { get; set; }
|
||||
public string? Note { get; set; }
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ namespace timetracker.Data;
|
||||
public class WorkDay
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int UserId { get; set; }
|
||||
public DateOnly Date { get; set; }
|
||||
public TimeOnly? StartTime { get; set; }
|
||||
public TimeOnly? EndTime { get; set; }
|
||||
|
||||
Reference in New Issue
Block a user