33 Commits

Author SHA1 Message Date
fddbc1e2ce Merge remote-tracking branch 'origin/identity/develop' into identity/develop 2026-02-03 20:30:00 +01:00
e21e6aa96d Fix formatting in PlunkEmailSender by adding a newline for improved readability 2026-02-03 20:29:16 +01:00
Jonas
2ee1f68c3f Merge pull request #4 from kobolol/claude/create-readme-awk06 2026-02-03 06:53:52 +01:00
Claude
5ff0df8685 Add comprehensive README with project documentation
Include project description, tech stack, structure, setup instructions,
available scripts, database configuration, and API endpoints.

https://claude.ai/code/session_01UFj67Xe3r5qqRaq4UMRhpa
2026-02-03 05:42:21 +00:00
ce26a30693 Refactored AgeGroup namespace and services, added RegistrationKey functionality. 2026-01-25 21:30:05 +01:00
91dd8d1603 Add AuthController and configure authentication/identity services 2026-01-25 15:29:28 +01:00
d1a72876d2 Implement Plunk email sending service with HttpClient integration and DTO definition. 2026-01-25 13:43:25 +01:00
Jonas
ba2a455a2b Delete API/app.db 2026-01-25 13:41:54 +01:00
44b39b4393 Add initial implementation of PlunkEmailSender and update .gitignore.
- Removed `appsettings.Development.json` and excluded it from version control.
- Updated `API.csproj` to remove unnecessary folder reference.
2026-01-25 13:17:14 +01:00
facc84157c Add footer with links and update UI/UX consistency 2026-01-25 13:00:25 +01:00
e4862f1878 - Enhance 404 page design and add link to home
- Update routes to include "Login" page
- Add dynamic website title updates based on current route
- Add "Login" button to layout linking to Login page
2026-01-23 22:59:32 +01:00
e0ecdad408 - Enable UseAuthentication middleware.
- Fix variable typo from `postgreConnection` to `postgresConnection`.
- Update `.gitignore` to exclude SQLite database files.
2026-01-23 21:52:20 +01:00
534ec5f36f Add authentication and identity support using ASP.NET Core Identity 2026-01-23 21:28:16 +01:00
cec2b65c29 Set WebRootPath, enable static file serving, and upgrade to .NET 9.0 2026-01-23 20:49:51 +01:00
23505060c5 Update Vite build output and remove test file
Added Vite build output directories to .gitignore and configured Vite to output to ../wwwroot. Removed obsolete App.spec.ts test file from GUI/src/__tests__.
2025-12-31 16:14:20 +01:00
Jonas
c63da03a3e Füge Datenbank-WAL- und SHM-Dateien zur .gitignore hinzu 2025-12-08 16:40:25 +00:00
Jonas
94fcf83241 Füge Datenbank- und S3-Dienste hinzu; entferne nicht mehr benötigte WAL- und SHM-Dateien 2025-12-08 16:37:49 +00:00
Jonas
6cc45313f5 Update database files to reflect recent changes 2025-12-08 16:34:40 +00:00
aacd8b7d96 Change AgeGroup ID type from int to string and add migration
Refactored AgeGroupController, AgeGroupService, and IAgeGroupService to use string IDs instead of int. Added initial Entity Framework migration and database files to support AltersGruppe with string primary key.
2025-12-07 20:32:49 +01:00
9128b199e9 Switch AltersGruppe ID to ULID and add config
Changed AltersGruppe model ID from int to string and now generates ULID automatically. Added AltersGruppeConfiguration for EF Core mapping. Updated ApplicationDbContext to apply configurations from assembly. Removed obsolete migration files and database artifacts. Added Ulid package dependency.
2025-12-07 19:48:47 +01:00
3125d657dd Simple Database + Endpoints 2025-12-07 12:07:10 +01:00
35eee78efc Enhance Home page with new sections and image presets
Added new event images and introduced HomeEntrieWithImagePreset component for consistent image-text layouts. Updated Home.vue to include new sections for club values and events (Nikolausturnier, Wettkämpfe, Kinoabende) with tab navigation. Refactored carousel and improved HomeEntrie for better mobile/desktop title handling. Renamed CarouseWithTitle.vue to CarouselItemWithTitle.vue for clarity.
2025-11-23 20:53:33 +01:00
c2d84ad5e0 Fix Auto Theme 2025-11-22 22:57:05 +01:00
99af5fe13e Home Page
+ Added Information
+ First and Second Entrie
2025-11-22 22:50:05 +01:00
a118026b04 Main Site Work
+ Updatet Router
+ "Hauptseite" mit Großem Bild
2025-11-22 17:04:45 +01:00
30eeaabc5a Update app title and enhance app bar UI
Changed the HTML title to 'Judoteam - Stadtlohn'. Improved the app bar in Layout.vue by replacing search and heart icons with account and theme toggle buttons, each wrapped in tooltips for better user guidance. Added rounded styling to navigation drawer items.
2025-11-09 21:01:59 +01:00
4ce2e05dc8 Add persistent navigation drawer to layout
Introduces a navigation drawer that can be toggled and persists its state in localStorage. The drawer displays route items and adapts its location and permanence based on device type. Also refactors initial drawer visibility and updates the app bar navigation icon to toggle the drawer.
2025-11-09 20:15:07 +01:00
2b139495a3 feat: update layout and routing structure with theme toggle functionality 2025-11-09 13:11:59 +01:00
7c227d45bb fix 2025-11-08 13:09:16 +01:00
e12795bacd chore: remove unnecessary Visual Studio database files 2025-10-21 20:38:44 +02:00
75393940b4 feat: add app bar with theme toggle and remove weather forecast demo code 2025-10-19 21:28:11 +02:00
74cfa283a1 Add basic routing and layout structure
Replaced App.vue with Layout.vue to serve as the main layout. Added Home and 404NotFound route components and configured router to handle home and fallback routes. Updated main.ts to use the new layout component.
2025-10-04 16:43:09 +02:00
9ccfd2e526 Remove GUI README documentation
Deleted the GUI/README.md file, removing setup and usage instructions for the Vue 3 project. This may be part of a cleanup or migration effort.
2025-10-04 16:26:50 +02:00
70 changed files with 2364 additions and 142 deletions

22
.gitignore vendored
View File

@@ -67,10 +67,30 @@ yarn-error.log*
# Editor / IDE # Editor / IDE
.vscode/ .vscode/
.vs/
*.suo *.suo
*.user *.user
*.userosscache *.userosscache
*.sln.docstates *.sln.docstates
# JetBrains IDEs # JetBrains IDEs
.idea/ .idea/
# Claude Code
.claude
# Database (Sql Lite)
API/app.db-wal
API/app.db-shm
# Vite build output
GUI/dist/
wwwroot/
# SQLite Datenbank-Dateien
*.db
*.db-shm
*.db-wal
# AppSettings Development files
appsettings.Development.json

Binary file not shown.

View File

@@ -1,12 +1,49 @@
{ {
"Version": 1, "Version": 1,
"WorkspaceRootPath": "D:\\Programmieren\\CSharp\\JudoWeb\\", "WorkspaceRootPath": "D:\\Programmieren\\CSharp\\JudoWeb\\",
"Documents": [], "Documents": [
{
"AbsoluteMoniker": "D:0:0:{98166726-DC3A-4D5B-889A-8B4428E28656}|API\\API.csproj|d:\\programmieren\\csharp\\judoweb\\api\\program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{98166726-DC3A-4D5B-889A-8B4428E28656}|API\\API.csproj|solutionrelative:api\\program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
}
],
"DocumentGroupContainers": [ "DocumentGroupContainers": [
{ {
"Orientation": 0, "Orientation": 0,
"VerticalTabListWidth": 256, "VerticalTabListWidth": 256,
"DocumentGroups": [] "DocumentGroups": [
{
"DockedWidth": 200,
"SelectedChildIndex": 2,
"Children": [
{
"$type": "Bookmark",
"Name": "ST:0:0:{0174dea2-fdbe-4ef1-8f99-c0beae78880f}"
},
{
"$type": "Bookmark",
"Name": "ST:0:0:{aa2115a1-9712-457b-9047-dbb71ca2cdd2}"
},
{
"$type": "Document",
"DocumentIndex": 0,
"Title": "Program.cs",
"DocumentMoniker": "D:\\Programmieren\\CSharp\\JudoWeb\\API\\Program.cs",
"RelativeDocumentMoniker": "API\\Program.cs",
"ToolTip": "D:\\Programmieren\\CSharp\\JudoWeb\\API\\Program.cs",
"RelativeToolTip": "API\\Program.cs",
"ViewState": "AgIAAAAAAAAAAAAAAAAAABkAAAAAAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2025-10-05T16:26:39.39Z",
"EditorCaption": ""
},
{
"$type": "Bookmark",
"Name": "ST:0:0:{cce594b6-0c39-4442-ba28-10c64ac7e89f}"
}
]
}
]
} }
] ]
} }

View File

@@ -1,12 +1,49 @@
{ {
"Version": 1, "Version": 1,
"WorkspaceRootPath": "D:\\Programmieren\\CSharp\\JudoWeb\\", "WorkspaceRootPath": "D:\\Programmieren\\CSharp\\JudoWeb\\",
"Documents": [], "Documents": [
{
"AbsoluteMoniker": "D:0:0:{98166726-DC3A-4D5B-889A-8B4428E28656}|API\\API.csproj|d:\\programmieren\\csharp\\judoweb\\api\\program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{98166726-DC3A-4D5B-889A-8B4428E28656}|API\\API.csproj|solutionrelative:api\\program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
}
],
"DocumentGroupContainers": [ "DocumentGroupContainers": [
{ {
"Orientation": 0, "Orientation": 0,
"VerticalTabListWidth": 256, "VerticalTabListWidth": 256,
"DocumentGroups": [] "DocumentGroups": [
{
"DockedWidth": 200,
"SelectedChildIndex": 2,
"Children": [
{
"$type": "Bookmark",
"Name": "ST:0:0:{0174dea2-fdbe-4ef1-8f99-c0beae78880f}"
},
{
"$type": "Bookmark",
"Name": "ST:0:0:{aa2115a1-9712-457b-9047-dbb71ca2cdd2}"
},
{
"$type": "Document",
"DocumentIndex": 0,
"Title": "Program.cs",
"DocumentMoniker": "D:\\Programmieren\\CSharp\\JudoWeb\\API\\Program.cs",
"RelativeDocumentMoniker": "API\\Program.cs",
"ToolTip": "D:\\Programmieren\\CSharp\\JudoWeb\\API\\Program.cs",
"RelativeToolTip": "API\\Program.cs",
"ViewState": "AgIAAAAAAAAAAAAAAAAAABkAAAAAAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2025-10-05T16:26:39.39Z",
"EditorCaption": ""
},
{
"$type": "Bookmark",
"Name": "ST:0:0:{cce594b6-0c39-4442-ba28-10c64ac7e89f}"
}
]
}
]
} }
] ]
} }

View File

@@ -1,13 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Remove="Models\Ingoing\Altersgruppen\AltersGruppeIngoing\**" />
<Compile Remove="Services\Interfaces\**" />
<Content Remove="Models\Ingoing\Altersgruppen\AltersGruppeIngoing\**" />
<Content Remove="Services\Interfaces\**" />
<EmbeddedResource Remove="Models\Ingoing\Altersgruppen\AltersGruppeIngoing\**" />
<EmbeddedResource Remove="Services\Interfaces\**" />
<None Remove="Models\Ingoing\Altersgruppen\AltersGruppeIngoing\**" />
<None Remove="Services\Interfaces\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.11" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="Ulid" Version="1.4.1" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,73 @@
using API.Models.Internal.Altersgruppen;
using API.Repository.AgeGroupRepo;
using Microsoft.AspNetCore.Mvc;
namespace API.Controllers
{
[ApiController]
[Route("api/ageGroups/")]
public class AgeGroupController : ControllerBase
{
private IAgeGroupService _ageGroupService;
public AgeGroupController(IAgeGroupService ageGroupService)
{
_ageGroupService = ageGroupService;
}
[HttpGet()]
public async Task<IActionResult> GetAll()
{
var allAgeGroups = await _ageGroupService.GetAllAsync();
return Ok(allAgeGroups);
}
[HttpGet("{id}")]
public async Task<IActionResult> GetOne([FromRoute] string id)
{
var group = await _ageGroupService.GetAsync(id);
if (group == null)
{
return NotFound();
}
return Ok(group);
}
[HttpPost()]
public async Task<IActionResult> Create([FromBody] AltersGruppeIngoing groupDto)
{
var group = await _ageGroupService.CreateAsync(groupDto.ToInternalFromIngoing());
return CreatedAtAction(nameof(GetOne), new { Id = group.Id }, group);
}
[HttpPut("{id}")]
public async Task<IActionResult> Update([FromRoute] string id, [FromBody] AltersGruppeIngoing groupDto)
{
var group = await _ageGroupService.UpdateAsync(id, groupDto.ToInternalFromIngoing());
if(group == null)
{
return NotFound();
}
return Ok(group);
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete([FromRoute] string Id)
{
var group = await _ageGroupService.DeleteAsync(Id);
if (group == null)
{
return NotFound();
}
return NoContent();
}
}
}

View File

@@ -0,0 +1,16 @@
using API.Models.Internal.User;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
namespace API.Controllers;
[ApiController]
[Route("api/account")]
public class AuthController(UserManager<User> userManager, SignInManager<User> signInManager, IEmailSender emailSender)
: ControllerBase
{
private readonly UserManager<User> _userManager = userManager;
private readonly SignInManager<User> _signInManager = signInManager;
private readonly IEmailSender _emailSender = emailSender;
}

View File

@@ -0,0 +1,21 @@
using API.Repository.RegistrationKeyRepo;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
namespace API.Controllers;
[ApiController]
[Route("api/registrationKey")]
public class RegistrationKeyController : ControllerBase
{
private IRegistrationKeyService _keyService;
private readonly RoleManager<IdentityRole> _roleManager;
public RegistrationKeyController(IRegistrationKeyService keyService, RoleManager<IdentityRole> roleManager)
{
_keyService = keyService;
_roleManager = roleManager;
}
}

View File

@@ -1,33 +0,0 @@
using Microsoft.AspNetCore.Mvc;
namespace API.Controllers
{
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}
}

View File

@@ -0,0 +1,24 @@
using API.Models.Internal.Altersgruppen;
using API.Models.Internal.User;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace API.Database
{
public class ApplicationDbContext : IdentityDbContext<User>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
}
public DbSet<AltersGruppe> Altersgruppen { get; set; }
public DbSet<RegistrationKey> RegistrationKeys { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
}
}
}

View File

@@ -0,0 +1,34 @@
using API.Models.Internal.Altersgruppen;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace API.Database.Configurations
{
public class AltersGruppeConfiguration : IEntityTypeConfiguration<AltersGruppe>
{
public void Configure(EntityTypeBuilder<AltersGruppe> entity)
{
// Primary Key
entity.HasKey(e => e.Id);
// Id-Konfiguration
entity.Property(e => e.Id)
.HasMaxLength(26)
.IsRequired()
.ValueGeneratedNever();
// Name
entity.Property(e => e.Name)
.HasMaxLength(100)
.IsRequired();
// StartingAge
entity.Property(e => e.StartingAge)
.IsRequired();
// EndingAge
entity.Property(e => e.EndingAge)
.IsRequired();
}
}
}

View File

@@ -0,0 +1,13 @@
using API.Models.Internal.User;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace API.Database.Configurations;
public class UserConfiguration : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> entity)
{
entity.Property(e => e.Ininitals).HasMaxLength(5);
}
}

View File

@@ -0,0 +1,46 @@
// <auto-generated />
using API.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace API.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20251207192702_Init")]
partial class Init
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.11");
modelBuilder.Entity("API.Models.Internal.Altersgruppen.AltersGruppe", b =>
{
b.Property<string>("Id")
.HasMaxLength(26)
.HasColumnType("TEXT");
b.Property<int>("EndingAge")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<int>("StartingAge")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("Altersgruppen");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,35 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations
{
/// <inheritdoc />
public partial class Init : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Altersgruppen",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", maxLength: 26, nullable: false),
Name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
StartingAge = table.Column<int>(type: "INTEGER", nullable: false),
EndingAge = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Altersgruppen", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Altersgruppen");
}
}
}

View File

@@ -0,0 +1,294 @@
// <auto-generated />
using System;
using API.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace API.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20260123202637_InitialIdentity")]
partial class InitialIdentity
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.11");
modelBuilder.Entity("API.Models.Internal.Altersgruppen.AltersGruppe", b =>
{
b.Property<string>("Id")
.HasMaxLength(26)
.HasColumnType("TEXT");
b.Property<int>("EndingAge")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<int>("StartingAge")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("Altersgruppen");
});
modelBuilder.Entity("API.Models.Internal.User.User", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("Ininitals")
.HasMaxLength(5)
.HasColumnType("TEXT");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("ProviderKey")
.HasColumnType("TEXT");
b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.HasColumnType("TEXT");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("API.Models.Internal.User.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("API.Models.Internal.User.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Models.Internal.User.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("API.Models.Internal.User.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,223 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations
{
/// <inheritdoc />
public partial class InitialIdentity : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AspNetRoles",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
NormalizedName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
ConcurrencyStamp = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetUsers",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
Ininitals = table.Column<string>(type: "TEXT", maxLength: 5, nullable: true),
UserName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
NormalizedUserName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
Email = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
NormalizedEmail = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
EmailConfirmed = table.Column<bool>(type: "INTEGER", nullable: false),
PasswordHash = table.Column<string>(type: "TEXT", nullable: true),
SecurityStamp = table.Column<string>(type: "TEXT", nullable: true),
ConcurrencyStamp = table.Column<string>(type: "TEXT", nullable: true),
PhoneNumber = table.Column<string>(type: "TEXT", nullable: true),
PhoneNumberConfirmed = table.Column<bool>(type: "INTEGER", nullable: false),
TwoFactorEnabled = table.Column<bool>(type: "INTEGER", nullable: false),
LockoutEnd = table.Column<DateTimeOffset>(type: "TEXT", nullable: true),
LockoutEnabled = table.Column<bool>(type: "INTEGER", nullable: false),
AccessFailedCount = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetRoleClaims",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
RoleId = table.Column<string>(type: "TEXT", nullable: false),
ClaimType = table.Column<string>(type: "TEXT", nullable: true),
ClaimValue = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
table.ForeignKey(
name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
column: x => x.RoleId,
principalTable: "AspNetRoles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserClaims",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
UserId = table.Column<string>(type: "TEXT", nullable: false),
ClaimType = table.Column<string>(type: "TEXT", nullable: true),
ClaimValue = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
table.ForeignKey(
name: "FK_AspNetUserClaims_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserLogins",
columns: table => new
{
LoginProvider = table.Column<string>(type: "TEXT", nullable: false),
ProviderKey = table.Column<string>(type: "TEXT", nullable: false),
ProviderDisplayName = table.Column<string>(type: "TEXT", nullable: true),
UserId = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
table.ForeignKey(
name: "FK_AspNetUserLogins_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserRoles",
columns: table => new
{
UserId = table.Column<string>(type: "TEXT", nullable: false),
RoleId = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
table.ForeignKey(
name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
column: x => x.RoleId,
principalTable: "AspNetRoles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_AspNetUserRoles_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserTokens",
columns: table => new
{
UserId = table.Column<string>(type: "TEXT", nullable: false),
LoginProvider = table.Column<string>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: false),
Value = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
table.ForeignKey(
name: "FK_AspNetUserTokens_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_AspNetRoleClaims_RoleId",
table: "AspNetRoleClaims",
column: "RoleId");
migrationBuilder.CreateIndex(
name: "RoleNameIndex",
table: "AspNetRoles",
column: "NormalizedName",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_AspNetUserClaims_UserId",
table: "AspNetUserClaims",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_AspNetUserLogins_UserId",
table: "AspNetUserLogins",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_AspNetUserRoles_RoleId",
table: "AspNetUserRoles",
column: "RoleId");
migrationBuilder.CreateIndex(
name: "EmailIndex",
table: "AspNetUsers",
column: "NormalizedEmail");
migrationBuilder.CreateIndex(
name: "UserNameIndex",
table: "AspNetUsers",
column: "NormalizedUserName",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AspNetRoleClaims");
migrationBuilder.DropTable(
name: "AspNetUserClaims");
migrationBuilder.DropTable(
name: "AspNetUserLogins");
migrationBuilder.DropTable(
name: "AspNetUserRoles");
migrationBuilder.DropTable(
name: "AspNetUserTokens");
migrationBuilder.DropTable(
name: "AspNetRoles");
migrationBuilder.DropTable(
name: "AspNetUsers");
}
}
}

View File

@@ -0,0 +1,291 @@
// <auto-generated />
using System;
using API.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace API.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.11");
modelBuilder.Entity("API.Models.Internal.Altersgruppen.AltersGruppe", b =>
{
b.Property<string>("Id")
.HasMaxLength(26)
.HasColumnType("TEXT");
b.Property<int>("EndingAge")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<int>("StartingAge")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("Altersgruppen");
});
modelBuilder.Entity("API.Models.Internal.User.User", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("Ininitals")
.HasMaxLength(5)
.HasColumnType("TEXT");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("ProviderKey")
.HasColumnType("TEXT");
b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.HasColumnType("TEXT");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("API.Models.Internal.User.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("API.Models.Internal.User.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Models.Internal.User.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("API.Models.Internal.User.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,35 @@
using System.ComponentModel.DataAnnotations;
namespace API.Models.Internal.Altersgruppen
{
public class AltersGruppe
{
public string Id { get; set; } = Ulid.NewUlid().ToString();
public required string Name { get; set; }
public int StartingAge { get; set; }
public int EndingAge { get; set; }
}
public class AltersGruppeIngoing
{
[Required]
public string Name { get; set; }
[Required]
public int StartingAge { get; set; }
[Required]
public int EndingAge { get; set; }
}
public static class AltersgruppeMapper
{
public static AltersGruppe ToInternalFromIngoing(this AltersGruppeIngoing group)
{
return new AltersGruppe
{
Name = group.Name,
StartingAge = group.StartingAge,
EndingAge = group.EndingAge,
};
}
}
}

View File

@@ -0,0 +1,24 @@
namespace API.Models.Internal.User;
public class RegistrationKey
{
public string Id { get; set; } = Ulid.NewUlid().ToString();
public string? LinkedRole { get; set; }
public DateTime Created { get; set; } = DateTime.UtcNow;
}
public class RegistrationKeyIngoing
{
public string? LinkedRole { get; set; }
}
public static class RegistrationKeyMapper
{
public static RegistrationKey ToInternalFromIngoing(this RegistrationKeyIngoing key)
{
return new RegistrationKey
{
LinkedRole = key.LinkedRole,
};
}
}

View File

@@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Identity;
namespace API.Models.Internal.User;
public class User : IdentityUser
{
public string? Ininitals { get; set; }
}

View File

@@ -0,0 +1,9 @@
namespace API.Models.Outgoing;
public class PlunkEmailDto
{
public required string To { get; set; }
public required string Subject { get; set; }
public required string Body { get; set; }
public string? Name { get; set; }
}

View File

@@ -1,12 +1,58 @@
var builder = WebApplication.CreateBuilder(args); using API.Database;
using API.Models.Internal.User;
using API.Repository.AgeGroupRepo;
using API.Services;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.EntityFrameworkCore;
// Add services to the container. var webRoot = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), "..", "wwwroot"));
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
WebRootPath = webRoot
});
builder.Services.AddControllers(); builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(); builder.Services.AddSwaggerGen();
// Authentication
builder.Services.AddAuthorization();
builder.Services.AddAuthentication()
.AddCookie(IdentityConstants.ApplicationScheme);
builder.Services.AddIdentityCore<User>(options =>
{
options.SignIn.RequireConfirmedAccount = true;
options.User.RequireUniqueEmail = true;
}).AddEntityFrameworkStores<ApplicationDbContext>().AddRoles<IdentityRole>().AddDefaultTokenProviders();
// Database
var postgresConnection = builder.Configuration.GetConnectionString("PostgresConnection");
if (!string.IsNullOrEmpty(postgresConnection))
{
// Nutze PostgresSQL
builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseNpgsql(postgresConnection));
}
else
{
builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlite("Data Source=app.db"));
}
// Adding Email Service (Plunk)
builder.Services.AddHttpClient<IEmailSender, PlunkEmailSender>();
// Adding Database Services
builder.Services.AddScoped<IAgeGroupService, AgeGroupService>();
// Adding S3 Services
// Adding (latler) Redis Services
// Add Database Services
var app = builder.Build(); var app = builder.Build();
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.
@@ -16,10 +62,22 @@ if (app.Environment.IsDevelopment())
app.UseSwaggerUI(); app.UseSwaggerUI();
} }
app.UseHttpsRedirection(); using(var scope = app.Services.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
dbContext.Database.Migrate();
}
// Deliver frontend
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.MapControllers(); app.MapControllers();
// Frontend Fallback
app.MapFallbackToFile("index.html");
app.Run(); app.Run();

View File

@@ -0,0 +1,68 @@
using API.Database;
using API.Models.Internal.Altersgruppen;
using Microsoft.EntityFrameworkCore;
namespace API.Repository.AgeGroupRepo
{
public class AgeGroupService : IAgeGroupService
{
private ApplicationDbContext _context;
public AgeGroupService(ApplicationDbContext context)
{
_context = context;
}
public async Task<AltersGruppe> CreateAsync(AltersGruppe altersGruppe)
{
await _context.Altersgruppen.AddAsync(altersGruppe);
await _context.SaveChangesAsync();
return altersGruppe;
}
public async Task<AltersGruppe?> DeleteAsync(string id)
{
var group = await _context.Altersgruppen.FirstOrDefaultAsync(x => x.Id == id);
if (group == null)
{
return null;
}
_context.Altersgruppen.Remove(group);
_context.SaveChanges();
return group;
}
public async Task<List<AltersGruppe>> GetAllAsync()
{
var allGroups = await _context.Altersgruppen.ToListAsync();
return allGroups;
}
public async Task<AltersGruppe?> GetAsync(string id)
{
return await _context.Altersgruppen.FindAsync(id);
}
public async Task<AltersGruppe?> UpdateAsync(string id, AltersGruppe altersGruppe)
{
var existingGroup = await _context.Altersgruppen.FirstOrDefaultAsync(x => x.Id == id);
if (existingGroup == null)
{
return null;
}
existingGroup.Name = altersGruppe.Name;
existingGroup.StartingAge = altersGruppe.StartingAge;
existingGroup.EndingAge = altersGruppe.EndingAge;
await _context.SaveChangesAsync();
return existingGroup;
}
}
}

View File

@@ -0,0 +1,14 @@
using API.Models.Internal.Altersgruppen;
namespace API.Repository.AgeGroupRepo
{
public interface IAgeGroupService
{
public Task<List<AltersGruppe>> GetAllAsync();
public Task<AltersGruppe?> GetAsync(string id);
public Task<AltersGruppe> CreateAsync(AltersGruppe altersGruppe);
public Task<AltersGruppe?> DeleteAsync(string id);
public Task<AltersGruppe?> UpdateAsync(string id, AltersGruppe altersGruppe);
}
}

View File

@@ -0,0 +1,13 @@
using API.Models.Internal.User;
namespace API.Repository.RegistrationKeyRepo;
public interface IRegistrationKeyService
{
public Task<List<RegistrationKey>> GetAllAsync();
public Task<RegistrationKey?> GetAsync(string id);
public Task<RegistrationKey> CreateAsync(RegistrationKeyIngoing key);
public Task<RegistrationKey?> DeleteAsync(string id);
public Task<int> DeleteOldRegistrationKeysAsync(int x);
}

View File

@@ -0,0 +1,76 @@
using API.Database;
using API.Models.Internal.User;
using Microsoft.EntityFrameworkCore;
namespace API.Repository.RegistrationKeyRepo;
public class RegistrationKeyService : IRegistrationKeyService
{
private ApplicationDbContext _context;
public RegistrationKeyService(ApplicationDbContext context)
{
_context = context;
}
public async Task<List<RegistrationKey>> GetAllAsync()
{
var allKeys = await _context.RegistrationKeys.ToListAsync();
return allKeys;
}
public async Task<RegistrationKey?> GetAsync(string id)
{
return await _context.RegistrationKeys.FindAsync(id);
}
public async Task<RegistrationKey> CreateAsync(RegistrationKeyIngoing key)
{
var internalKey = key.ToInternalFromIngoing();
await _context.RegistrationKeys.AddAsync(internalKey);
await _context.SaveChangesAsync();
return internalKey;
}
public async Task<RegistrationKey?> DeleteAsync(string id)
{
var key = await _context.RegistrationKeys.FirstOrDefaultAsync(x => x.Id == id);
if (key == null)
{
return null;
}
_context.RegistrationKeys.Remove(key);
await _context.SaveChangesAsync();
return key;
}
public async Task<int> DeleteOldRegistrationKeysAsync(int x)
{
if (x <= 0)
{
return 0;
}
var cutoff = DateTime.UtcNow.AddDays(-x);
var oldKeys = await _context.RegistrationKeys
.Where(k => k.Created < cutoff)
.ToListAsync();
if (oldKeys.Count == 0)
{
return 0;
}
_context.RegistrationKeys.RemoveRange(oldKeys);
await _context.SaveChangesAsync();
return oldKeys.Count;
}
}

View File

@@ -0,0 +1,36 @@
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using API.Models.Outgoing;
using Microsoft.AspNetCore.Identity.UI.Services;
namespace API.Services;
public class PlunkEmailSender(HttpClient httpClient, string plunkSecretKey) : IEmailSender
{
public async Task SendEmailAsync(string email, string subject, string htmlMessage)
{
var requestBody = new PlunkEmailDto()
{
To = email,
Subject = subject,
Body = htmlMessage
};
var jsonContent = new StringContent(
JsonSerializer.Serialize(requestBody),
Encoding.UTF8,
"application/json"
);
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", plunkSecretKey);
var response = await httpClient.PostAsync("https://api.useplunk.com/v1/send", jsonContent);
if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadAsStringAsync();
throw new Exception($"Fehler beim Senden der E-Mail über Plunk: {error}");
}
}
}

View File

@@ -1,13 +0,0 @@
namespace API
{
public class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string? Summary { get; set; }
}
}

View File

@@ -1,8 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/prettierrc", "$schema": "https://json.schemastore.org/prettierrc",
"semi": false, "semi": true,
"singleQuote": true, "singleQuote": true,
"printWidth": 100 "printWidth": 100
} }

View File

@@ -1,48 +0,0 @@
# ./
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Recommended Browser Setup
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
- Firefox:
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```
### Run Unit Tests with [Vitest](https://vitest.dev/)
```sh
npm run test:unit
```

View File

@@ -4,10 +4,10 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title> <title>Judoteam - Stadtlohn</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>
</html> </html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 MiB

View File

@@ -1,11 +0,0 @@
<script setup lang="ts"></script>
<template>
<h1>You did it!</h1>
<p>
Visit <a href="https://vuejs.org/" target="_blank" rel="noopener">vuejs.org</a> to read the
documentation
</p>
</template>
<style scoped></style>

131
GUI/src/Layout.vue Normal file
View File

@@ -0,0 +1,131 @@
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue';
import { useTheme } from 'vuetify';
import { Visibility, routes } from './plugins/routesLayout';
import { useRoute } from 'vue-router';
const theme = useTheme();
const route = useRoute();
const showDrawer = ref(true);
function changeTheme() {
theme.toggle();
localStorage.setItem('theme', theme.name.value);
}
function toggleDrawer() {
showDrawer.value = !showDrawer.value;
localStorage.setItem('drawer', showDrawer.value ? 'Y' : 'N');
}
onMounted(() => {
theme.change(
localStorage.getItem('theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'),
);
showDrawer.value = localStorage.getItem('drawer')?.startsWith('Y') || false;
});
function changeWebsiteTitle(path: string) {
// Pfad als Parameter
let currentPageInfo = routes.find((x) => x.path === path);
if (currentPageInfo) {
document.title = 'Judoteam - Stadtlohn | ' + currentPageInfo.name;
} else {
document.title = 'Judoteam - Stadtlohn';
}
}
// Beobachte die Route auf Änderungen
watch(
() => route.path,
(newPath) => {
changeWebsiteTitle(newPath);
},
{ immediate: true },
);
</script>
<template>
<v-app>
<v-app-bar image="/static/images/appBarIcon.png">
<template v-slot:image>
<v-img
:gradient="
theme.name.value === 'dark'
? 'rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)'
: 'rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5)'
"
/>
</template>
<template v-slot:prepend>
<v-app-bar-nav-icon
v-tooltip="!showDrawer ? 'Menü öffnen' : 'Menü schließen'"
@click="toggleDrawer()"
></v-app-bar-nav-icon>
</template>
<v-app-bar-title class="title" @click="$router.push({ name: 'Home' })"
><span class="pointer">Judoteam - Stadtlohn</span></v-app-bar-title
>
<v-tooltip v-if="!$vuetify.display.mobile">
<template #activator="{ props }">
<v-btn icon v-bind="props" to="/login">
<v-icon>mdi-account</v-icon>
</v-btn>
</template>
Account
</v-tooltip>
<v-tooltip>
<template #activator="{ props }">
<v-btn icon @click="changeTheme()" v-bind="props">
<v-icon>mdi-brightness-6</v-icon>
</v-btn>
</template>
{{ theme.name.value === 'dark' ? 'Hellen Modus aktivieren' : 'Dunklen Modus aktivieren' }}
</v-tooltip>
</v-app-bar>
<v-navigation-drawer
v-model="showDrawer"
:location="$vuetify.display.mobile ? 'bottom' : undefined"
:permanent="!$vuetify.display.mobile"
>
<v-list>
<v-list-item
v-for="item in routes.filter((x) => x.visible === Visibility.Public)"
:key="item.path"
:to="item.path"
:active="route.path === item.path"
link
:prepend-icon="item.icon"
:title="item.name"
class="rounded-lg mr-1 ml-1"
>
</v-list-item>
</v-list>
</v-navigation-drawer>
<v-main>
<router-view></router-view>
<v-footer
class="d-flex align-center justify-center ga-2 flex-wrap flex-grow-1 py-3"
>
<v-btn
v-for="link in routes.filter((x) => x.visible === Visibility.Footer)"
:key="link.path"
:to="link.path"
:text="link.name"
variant="text"
rounded
></v-btn>
<div class="flex-1-0-100 text-center mt-2">
{{ new Date().getFullYear() }} <strong>Judoteam - Stadtlohn</strong>
</div>
</v-footer>
</v-main>
</v-app>
</template>
<style scoped></style>

View File

@@ -1,11 +0,0 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import App from '../App.vue'
describe('App', () => {
it('mounts renders properly', () => {
const wrapper = mount(App)
expect(wrapper.text()).toContain('You did it!')
})
})

View File

@@ -0,0 +1,29 @@
<script setup lang="ts">
const props = defineProps({
image: String,
text: String,
noCover: Boolean,
})
</script>
<template>
<v-carousel-item :src="props.image" :cover="!props.noCover">
<template #default>
<div class="text-center">
<p class="text-h CarouselWithTitleText">{{ text }}</p>
</div>
</template>
</v-carousel-item>
</template>
<style scoped>
.CarouselWithTitleText{
background-color: rgb(0, 0, 0, 0.65);
color: white;
display: inline-block;
padding: 2px;
padding-right: 10px;
padding-left: 10px;
border-radius: 0px 0px var(--default-radius) var(--default-radius);
}
</style>

View File

@@ -0,0 +1,132 @@
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps({
title: String,
noMarginTop: Boolean,
extraBesideText: {
type: Boolean,
default: false,
},
extraWidthPercent: {
type: Number,
default: 30,
},
titleInSplitRight: {
type: Boolean,
default: false,
},
});
const normalizedExtraWidth = computed(() => {
const width = Number.isFinite(props.extraWidthPercent) ? props.extraWidthPercent : 30;
return Math.min(100, Math.max(0, width));
});
const textWidthPercent = computed(() => 100 - normalizedExtraWidth.value);
const extraWidthStyle = computed(() => ({
flexBasis: `${normalizedExtraWidth.value}%`,
maxWidth: `${normalizedExtraWidth.value}%`,
}));
const textWidthStyle = computed(() => ({
flexBasis: `${textWidthPercent.value}%`,
maxWidth: `${textWidthPercent.value}%`,
}));
</script>
<template>
<div :class="{ 'mt-6': !noMarginTop }">
<template v-if="props.extraBesideText">
<h3 v-if="!props.titleInSplitRight" class="text-h4 font-weight-bold">
{{ props.title }}
</h3>
<div class="entry-content entry-content--split">
<h3
v-if="props.titleInSplitRight"
class="text-h4 font-weight-bold entry-content__title--mobile"
>
{{ props.title }}
</h3>
<div class="entry-content__extra" :style="extraWidthStyle">
<slot name="extra" />
</div>
<div class="entry-content__text" :style="textWidthStyle">
<h3
v-if="props.titleInSplitRight"
class="text-h4 font-weight-bold entry-content__title--desktop"
>
{{ props.title }}
</h3>
<p class="text-subtitle-1">
<slot name="text" />
</p>
</div>
</div>
</template>
<template v-else>
<h3 class="text-h4 font-weight-bold">{{ props.title }}</h3>
<p class="text-subtitle-1">
<slot name="text" />
</p>
<slot name="extra" />
</template>
</div>
</template>
<style scoped>
.entry-content {
display: flex;
gap: 1.5rem;
align-items: flex-start;
}
.entry-content--split {
align-items: center;
}
.entry-content__extra,
.entry-content__text {
min-width: 0;
}
.entry-content__extra {
flex: 0 0 auto;
}
.entry-content__text {
flex: 1 1 auto;
}
/* Desktop/Mobile title visibility helpers */
.entry-content__title--mobile {
display: none;
}
.entry-content__title--desktop {
display: block;
}
@media (max-width: 600px) {
.entry-content--split {
flex-direction: column;
align-items: stretch;
}
.entry-content__extra,
.entry-content__text {
flex-basis: auto !important;
max-width: 100% !important;
width: 100%;
}
.entry-content__title--mobile {
display: block;
}
.entry-content__title--desktop {
display: none;
}
}
</style>

View File

@@ -0,0 +1,30 @@
<script setup lang="ts">
import HomeEntrie from '@/components/HomeEntrie.vue';
const props = defineProps({
title: String,
image: String,
});
</script>
<template>
<home-entrie
:title="props.title"
extra-beside-text
:extra-width-percent="50"
title-in-split-right
>
<template #text>
<slot name="text"></slot>
</template>
<template #extra>
<v-img class="rounded-lg" :src="props.image" height="400" cover> </v-img>
</template>
</home-entrie>
</template>
<style scoped>
.paddingIn {
padding-left: 75px;
padding-right: 75px;
}
</style>

View File

@@ -0,0 +1,43 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useDisplay } from 'vuetify';
export interface TabItem {
value: string;
label: string;
}
defineProps<{
items: TabItem[];
tabColor?: string;
}>();
const display = useDisplay();
const tab = ref('1');
</script>
<template>
<!-- Desktop: Tabs mit einzelner Anzeige -->
<template v-if="!display.mobile.value">
<v-tabs :color="tabColor ?? '#b62b2b'" v-model="tab">
<v-tab v-for="item in items" :key="item.value" :value="item.value">
{{ item.label }}
</v-tab>
</v-tabs>
<v-divider></v-divider>
<v-tabs-window v-model="tab">
<v-tabs-window-item v-for="item in items" :key="item.value" :value="item.value">
<slot :name="item.value"></slot>
</v-tabs-window-item>
</v-tabs-window>
</template>
<!-- Mobile: Alle Einträge untereinander -->
<template v-else>
<template v-for="item in items" :key="item.value">
<slot :name="item.value"></slot>
</template>
</template>
</template>

View File

@@ -0,0 +1,13 @@
:root{
--default-radius: 7px;
--red-color: #b62b2b
}
.title {
font-weight: 600;
}
.pointer{
cursor: pointer;
user-select: none;
}

View File

@@ -1,5 +1,5 @@
import { createApp } from 'vue' import { createApp } from 'vue'
import App from './App.vue' import App from './Layout.vue'
import router from './router' import router from './router'
import vuetify from './plugins/vuetify' import vuetify from './plugins/vuetify'

View File

View File

@@ -0,0 +1,66 @@
import Home from '@/routes/Home.vue'
import NotFound from '@/routes/404NotFound.vue'
import type { RouteRecordRaw } from 'vue-router'
import Login from '@/routes/authentication/Login.vue';
export enum Visibility {
Hidden,
Authenticated,
Unauthenticated,
Authorized,
Public,
Footer,
}
export interface LayoutRoute {
path: string,
name: string,
description: string,
icon: string,
disableFooter?: boolean,
visible: Visibility,
meta?: RouteRecordRaw
}
export const routes: LayoutRoute[] = [
{
path: "/",
name: "Startseite",
description: "Übersicht der Anwendung",
icon: "mdi-home",
visible: Visibility.Public,
meta: {
name: 'Home',
path: '/',
component: Home
}
},
{
path: "/login",
name: "Login",
description: "Logge dich ein",
icon: "mdi-login",
visible: Visibility.Hidden,
meta: {
path: "/login",
name: 'Login',
component: Login
}
},
{
path: "/impressum",
name: "Impressum",
description: "Impressum der Anwendung",
icon: "mdi-file-document",
visible: Visibility.Footer
},
{
path: "/notFound",
name: "Nicht Gefunden",
description: "Diese Seite wurde nicht gefunden",
icon: "mdi-information-outline",
visible: Visibility.Hidden,
meta: { path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound },
}
]

View File

@@ -1,8 +1,9 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router';
import { routes } from '@/plugins/routesLayout'
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
routes: [], routes: routes.filter(x => x.meta !== undefined).map(x => x.meta) as RouteRecordRaw[]
}) })
export default router export default router

View File

@@ -0,0 +1,34 @@
<script setup lang="ts"></script>
<template>
<v-container fluid class="fill-height d-flex flex-column justify-center align-center">
<h1 class="error-title text-h1 font-weight-bold">404</h1>
<p class="error-message text-h5">Page not found</p>
<router-link
to="/"
class="text-blue text-decoration-none font-weight-medium mt-5"
>
Zurück zur Startseite
</router-link>
</v-container>
</template>
<style scoped>
.error-title {
font-size: 3rem;
font-weight: bold;
margin-bottom: 1rem;
letter-spacing: 2px;
color: #fff;
}
.error-message {
font-size: 1.25rem;
opacity: 0.85;
color: #fff;
}
a:hover {
text-decoration: underline !important;
}
</style>

142
GUI/src/routes/Home.vue Normal file
View File

@@ -0,0 +1,142 @@
<script setup lang="ts">
import CarouselItemWithTitle from '@/components/CarouselItemWithTitle.vue';
import HomeEntrie from '@/components/HomeEntrie.vue';
import HomeEntrieWithImagePreset from '@/components/HomeEntrieWithImagePreset.vue';
import ResponsiveTabsOrList from '@/components/ResponsiveTabsOrList.vue';
const tabItems = [
{ value: '1', label: 'Nikolausturnier' },
{ value: '2', label: 'Wettkämpfe' },
{ value: '3', label: 'Kinoabende' },
];
</script>
<template>
<v-container fluid>
<v-img class="rounded-lg" height="40vh" src="/static/images/startPage.png" cover />
<main>
<v-container
id="main"
:style="{
maxWidth: $vuetify.display.smAndDown ? '100vw' : '70vw',
}"
fluid
>
<home-entrie title="Herzlich Willkommen!">
<template #text>
<b>Schön, dass du den Weg zu uns gefunden hast!</b> Egal, ob du schon lange dabei bist
oder das erste Mal von Judo hörst wir freuen uns, dich bald (wieder) in unserem
<b>Judozentrum</b>
(Dojo) begrüßen zu dürfen.
</template>
</home-entrie>
<home-entrie
title="Was ist Judo?"
extra-beside-text
:extra-width-percent="50"
title-in-split-right
>
<template #text>
Judo ist eine Sportart, welche den ganzen Körper fordert, fördert und beansprucht. Es
werden also <b>sämtliche Muskelgruppen, sowie das Köpfchen bei uns trainiert. </b>
<b>Judo bedeutet übersetzt der sanfte Weg.</b> Daraus lässt sich ja schon erahnen,
dass Judo eine Kampfsportart ist bei der das "Schlagen" oder "Treten" des Gegners
verboten ist. Beim Judo beschränkt man sich rein auf das <b>Werfen</b>,
<b>fixieren am Boden</b>, <b>Hebeln</b> und <b>Hebeln</b> (nur für die älteren) des
Partners/Gegners.
</template>
<template #extra>
<v-carousel
show-arrows="hover"
hide-delimiter-background
class="rounded-lg"
cycle
interval="8000"
>
<carousel-item-with-title text="Werfen" image="/static/images/whatsJudo/throw.jpg" />
<carousel-item-with-title
text="fixieren am Boden"
image="/static/images/whatsJudo/hold.jpg"
/>
<carousel-item-with-title
text="Trainieren sämtlicher Muskelgruppen"
image="/static/images/whatsJudo/stabil.jpg"
/>
</v-carousel>
</template>
</home-entrie>
<home-entrie
title="Unsere Werte beim Judo"
extra-beside-text
:extra-width-percent="50"
title-in-split-right
>
<template #text>
Zu dem Spielen beim Judo die Werte wie <b>Respekt</b>, <b>Disziplin</b> und
<b>Selbstbehauptung</b> eine große Rolle und so werden auch diese Werte bei uns
gefördert.
</template>
<template #extra>
<v-timeline align="start" side="end">
<v-timeline-item dot-color="blue">
<h6 class="text-h6">Respekt</h6>
</v-timeline-item>
<v-timeline-item dot-color="green">
<h6 class="text-h6">Selbstbehauptung</h6>
</v-timeline-item>
<v-timeline-item dot-color="orange">
<h6 class="text-h6">Disziplin</h6>
</v-timeline-item>
</v-timeline>
</template>
</home-entrie>
<home-entrie title="Und sonst so?"></home-entrie>
<responsive-tabs-or-list :items="tabItems">
<template #1>
<home-entrie-with-image-preset
title="Nikolausturnier"
image="/static/images/nikolausturnier/total.png"
>
<template #text>
Ein Highlight im Jahreskalender ist unser alljährliches Nikolausturnier, bei dem
Judoka aller Altersgruppen zusammenkommen, um ihre Fähigkeiten zu messen und
gemeinsam Spaß zu haben.
</template>
</home-entrie-with-image-preset>
</template>
<template #2>
<home-entrie-with-image-preset
title="Wettkämpfe"
image="/static/images/wettkampf/total.jpg"
>
<template #text>
Für diejenigen, die sich gerne messen wollen, bieten wir die Teilnahme an
Wettkämpfen auf verschiedenen Ebenen an von lokalen Turnieren bis hin zu
nationalen Meisterschaften.
</template>
</home-entrie-with-image-preset>
</template>
<template #3>
<home-entrie-with-image-preset
title="Kinoabende"
image="/static/images/kinoabend/total.jpg"
>
<template #text>
Bei unseren Kinoabenden schauen wir in lockerer Runde ausgewählte Filme nicht nur
judobezogene, sondern auch beliebte Klassiker und aktuelle Hits. Gemeinsam
entspannen, Snacks teilen und über Filme und Sport plaudern.
</template>
</home-entrie-with-image-preset>
</template>
</responsive-tabs-or-list>
</v-container>
</main>
</v-container>
</template>
<style scoped></style>

View File

@@ -0,0 +1,9 @@
<script setup lang="ts"></script>
<template>
<v-container class="">
<h1>Potenzieller Login</h1>
</v-container>
</template>
<style scoped></style>

View File

@@ -15,4 +15,7 @@ export default defineConfig({
'@': fileURLToPath(new URL('./src', import.meta.url)) '@': fileURLToPath(new URL('./src', import.meta.url))
}, },
}, },
build: {
outDir: "../wwwroot"
}
}) })

190
README.md
View File

@@ -1 +1,189 @@
# JudoWeb # JudoWeb
Eine moderne Webanwendung für das Judoteam Stadtlohn - mit Vereinspräsentation und Verwaltungssystem.
## Projektbeschreibung
JudoWeb ist eine Full-Stack-Webanwendung, die zwei Hauptzwecke erfüllt:
- **Öffentliche Vereinswebsite**: Präsentation des Judoclubs mit Informationen zu Judo, Vereinswerten (Respekt, Disziplin, Selbstbehauptung) und Veranstaltungen
- **Verwaltungssystem**: Benutzerverwaltung, Registrierung und Altersgruppenmanagement
### Features
- Responsive Homepage mit Bildkarussell (Würfe, Haltetechniken, Stabilitätstraining)
- Veranstaltungsübersicht (Nikolausturnier, Wettkämpfe, Kinoabende)
- Benutzerauthentifizierung mit Registrierungsschlüsseln
- Altersgruppenmanagement
- Dark/Light Theme-Umschaltung
- Mobile-optimiertes Design
## Technologie-Stack
### Backend
- **.NET 9.0** mit ASP.NET Core Web API
- **Entity Framework Core 9** - ORM
- **ASP.NET Core Identity** - Authentifizierung
- **Swagger/Swashbuckle** - API-Dokumentation
### Frontend
- **Vue.js 3** mit TypeScript
- **Vite** - Build-Tool und Entwicklungsserver
- **Vuetify 3** - Material Design Komponenten
- **Vue Router** - Client-seitiges Routing
### Datenbank
- **SQLite** (Entwicklung)
- **PostgreSQL** (Produktion)
### Zusätzliche Services
- **Plunk** - E-Mail-Versand
## Projektstruktur
```
JudoWeb/
├── API/ # Backend (.NET)
│ ├── Controllers/ # API-Endpunkte
│ │ ├── AgeGroupController.cs
│ │ ├── AuthController.cs
│ │ └── RegistrationKeyController.cs
│ ├── Models/
│ │ ├── Internal/ # Domain-Modelle
│ │ └── Outgoing/ # DTOs
│ ├── Database/ # EF Core Kontext
│ ├── Repository/ # Datenzugriff
│ ├── Services/ # Business-Logik
│ └── Migrations/ # Datenbank-Migrationen
├── GUI/ # Frontend (Vue.js)
│ ├── src/
│ │ ├── routes/ # Seiten-Komponenten
│ │ ├── components/ # Wiederverwendbare Komponenten
│ │ ├── plugins/ # Vue-Plugins
│ │ └── router/ # Router-Konfiguration
│ └── public/static/ # Statische Assets
└── JudoWeb.sln # Visual Studio Solution
```
## Installation & Setup
### Voraussetzungen
- [.NET 9.0 SDK](https://dotnet.microsoft.com/download)
- [Node.js](https://nodejs.org/) (LTS-Version empfohlen)
- Optional: [Visual Studio 2022](https://visualstudio.microsoft.com/) oder [VS Code](https://code.visualstudio.com/)
### Frontend einrichten
```bash
cd GUI
npm install
```
### Backend einrichten
```bash
cd API
dotnet restore
```
## Entwicklung starten
### Frontend (mit Hot-Reload)
```bash
cd GUI
npm run dev
```
Der Entwicklungsserver startet unter `http://localhost:5173`
### Backend
```bash
cd API
dotnet run
```
Die API ist verfügbar unter:
- HTTP: `http://localhost:5246`
- HTTPS: `https://localhost:7248`
- Swagger UI: `https://localhost:7248/swagger`
## Verfügbare Scripts
### Frontend (GUI/)
| Befehl | Beschreibung |
|--------|--------------|
| `npm run dev` | Startet Entwicklungsserver |
| `npm run build` | Erstellt Produktions-Build |
| `npm run test:unit` | Führt Unit-Tests aus |
| `npm run type-check` | TypeScript-Typprüfung |
| `npm run format` | Code-Formatierung mit Prettier |
### Backend (API/)
| Befehl | Beschreibung |
|--------|--------------|
| `dotnet run` | Startet Entwicklungsserver |
| `dotnet build` | Kompiliert das Projekt |
| `dotnet publish -c Release` | Erstellt Produktions-Build |
| `dotnet ef database update` | Wendet Migrationen an |
## Datenbank-Konfiguration
Die Anwendung wählt automatisch die Datenbank basierend auf der Konfiguration:
1. **PostgreSQL** (Produktion): Wenn `PostgresConnection` in `appsettings.json` oder als Umgebungsvariable gesetzt ist
2. **SQLite** (Entwicklung): Standardmäßig wird `app.db` verwendet
Datenbank-Migrationen werden beim Anwendungsstart automatisch ausgeführt.
### Beispiel PostgreSQL-Konfiguration
```json
{
"ConnectionStrings": {
"PostgresConnection": "Host=localhost;Database=judoweb;Username=user;Password=pass"
}
}
```
## API-Endpunkte
| Endpunkt | Beschreibung |
|----------|--------------|
| `GET /api/ageGroups` | Alle Altersgruppen abrufen |
| `POST /api/ageGroups` | Neue Altersgruppe erstellen |
| `PUT /api/ageGroups/{id}` | Altersgruppe aktualisieren |
| `DELETE /api/ageGroups/{id}` | Altersgruppe löschen |
| `POST /api/account/register` | Benutzer registrieren |
| `POST /api/account/login` | Benutzer anmelden |
| `GET /api/registrationKey` | Registrierungsschlüssel abrufen |
Vollständige API-Dokumentation ist über Swagger UI verfügbar.
## Produktion
### Frontend bauen
```bash
cd GUI
npm run build
```
Das Build wird in `../wwwroot/` ausgegeben und vom Backend als statische Dateien serviert.
### Backend publizieren
```bash
cd API
dotnet publish -c Release -o ./publish
```
## Lizenz
Dieses Projekt ist proprietär und gehört dem Judoteam Stadtlohn.