From e4d61fba243a80eb842ce3f14e89a5144cd62d7a Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 1 Feb 2026 20:32:07 +0100 Subject: [PATCH] proxy for web to registration setup --- .../Controllers/AdminController.cs | 29 ++ .../Controllers/ContentController.cs | 27 ++ .../Controllers/RegistrationController.cs | 14 + .../Registration.API/appsettings.json | 3 + .../Models/EventContent.cs | 13 + .../Registration.Domain/Models/Participant.cs | 3 +- .../Repositories/MemberRepository.cs | 43 +++ .../lan-frontend/app/admin/dashboard/page.tsx | 201 +++++++++++ src/Web/lan-frontend/app/admin/login/page.tsx | 77 +++++ src/Web/lan-frontend/app/page.tsx | 167 ++++++--- src/Web/lan-frontend/app/register/page.tsx | 322 ++++++++++++++++++ src/Web/lan-frontend/next.config.ts | 17 +- 12 files changed, 858 insertions(+), 58 deletions(-) create mode 100644 src/Registration/Registration.API/Controllers/AdminController.cs create mode 100644 src/Registration/Registration.API/Controllers/ContentController.cs create mode 100644 src/Registration/Registration.Domain/Models/EventContent.cs create mode 100644 src/Web/lan-frontend/app/admin/dashboard/page.tsx create mode 100644 src/Web/lan-frontend/app/admin/login/page.tsx create mode 100644 src/Web/lan-frontend/app/register/page.tsx diff --git a/src/Registration/Registration.API/Controllers/AdminController.cs b/src/Registration/Registration.API/Controllers/AdminController.cs new file mode 100644 index 0000000..b6bb548 --- /dev/null +++ b/src/Registration/Registration.API/Controllers/AdminController.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Registration.API.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class AdminController(IConfiguration configuration) : ControllerBase + { + private readonly IConfiguration _configuration = configuration; + + [HttpPost("login")] + public IActionResult Login([FromBody] LoginRequest request) + { + var adminPassword = _configuration["Admin:Password"] ?? "admin123"; + if (request.Username == "admin" && request.Password == adminPassword) + { + // In a real app, we would return a JWT token here + return Ok(new { Success = true, Token = "fake-jwt-token" }); + } + return Unauthorized(new { Success = false, Message = "Invalid credentials" }); + } + + public class LoginRequest + { + public string Username { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + } + } +} diff --git a/src/Registration/Registration.API/Controllers/ContentController.cs b/src/Registration/Registration.API/Controllers/ContentController.cs new file mode 100644 index 0000000..ac96985 --- /dev/null +++ b/src/Registration/Registration.API/Controllers/ContentController.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Mvc; +using Registration.Domain.Models; +using Registration.Infra.Repositories; + +namespace Registration.API.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class ContentController(IMemberRepository memberRepository) : ControllerBase + { + private readonly IMemberRepository _memberRepository = memberRepository; + + [HttpGet] + public async Task GetContent() + { + var content = await _memberRepository.GetEventContent(); + return Ok(content); + } + + [HttpPost] + public async Task UpdateContent([FromBody] EventContent content) + { + await _memberRepository.UpdateEventContent(content); + return Ok(); + } + } +} diff --git a/src/Registration/Registration.API/Controllers/RegistrationController.cs b/src/Registration/Registration.API/Controllers/RegistrationController.cs index 26de92f..44e3d1a 100644 --- a/src/Registration/Registration.API/Controllers/RegistrationController.cs +++ b/src/Registration/Registration.API/Controllers/RegistrationController.cs @@ -9,6 +9,20 @@ namespace Registration.API.Controllers { private readonly IMemberRepository _memberRepository = memberRepository; + [HttpGet("register/{ssn}")] + public IActionResult ValidateSsn(string ssn) + { + // Should talk to the auth api to validate the ssn properly. + if (ssn.Length == 10 && long.TryParse(ssn, out _)) + { + return Ok(); + } + else + { + return NoContent(); + } + } + [HttpPost("register/{ssn}")] public async Task RegisterMember(string ssn) { diff --git a/src/Registration/Registration.API/appsettings.json b/src/Registration/Registration.API/appsettings.json index 7b18068..1a4b525 100644 --- a/src/Registration/Registration.API/appsettings.json +++ b/src/Registration/Registration.API/appsettings.json @@ -5,6 +5,9 @@ "Security": { "SsnPepper": "VBYTES_LAN_2026_SECRET_PEPPER" }, + "Admin": { + "Password": "admin" + }, "Logging": { "LogLevel": { "Default": "Information", diff --git a/src/Registration/Registration.Domain/Models/EventContent.cs b/src/Registration/Registration.Domain/Models/EventContent.cs new file mode 100644 index 0000000..5a4b9c3 --- /dev/null +++ b/src/Registration/Registration.Domain/Models/EventContent.cs @@ -0,0 +1,13 @@ +namespace Registration.Domain.Models; + +public class EventContent +{ + public string Title { get; set; } = string.Empty; + public string SubTitle { get; set; } = string.Empty; + public string EventDate { get; set; } = string.Empty; + public string EventTime { get; set; } = string.Empty; + public string LocationName { get; set; } = string.Empty; + public string LocationAddress { get; set; } = string.Empty; + public string WhatToBring { get; set; } = string.Empty; + public string RulesAndGdpr { get; set; } = string.Empty; +} diff --git a/src/Registration/Registration.Domain/Models/Participant.cs b/src/Registration/Registration.Domain/Models/Participant.cs index b75c92d..1c551b4 100644 --- a/src/Registration/Registration.Domain/Models/Participant.cs +++ b/src/Registration/Registration.Domain/Models/Participant.cs @@ -2,6 +2,7 @@ namespace Registration.Domain.Models; public class Participant { + public required bool IsMember { get; set; } public required string FirstName { get; set; } public required string SurName { get; set; } public required string Grade { get; set; } @@ -15,5 +16,3 @@ public class Participant public string? Friends { get; set; } public string? SpecialDiet { get; set; } } - - diff --git a/src/Registration/Registration.Infra/Repositories/MemberRepository.cs b/src/Registration/Registration.Infra/Repositories/MemberRepository.cs index eed8439..66c85a3 100644 --- a/src/Registration/Registration.Infra/Repositories/MemberRepository.cs +++ b/src/Registration/Registration.Infra/Repositories/MemberRepository.cs @@ -3,6 +3,7 @@ using System.Security.Cryptography; using System.Text; using Dapper; using Npgsql; +using Registration.Domain.Models; namespace Registration.Infra.Repositories; @@ -12,6 +13,8 @@ public interface IMemberRepository Task GetIsRegistered(string ssn); Task AddRegistration(string ssn); Task ClearRegistrations(); + Task GetEventContent(); + Task UpdateEventContent(EventContent content); } public class MemberRepository(IConfiguration configuration) : IMemberRepository @@ -41,6 +44,22 @@ public class MemberRepository(IConfiguration configuration) : IMemberRepository ssn_index VARCHAR(255) NOT NULL UNIQUE ); CREATE INDEX IF NOT EXISTS idx_members_ssn_index ON members(ssn_index); + + CREATE TABLE IF NOT EXISTS event_content ( + id INT PRIMARY KEY, + title TEXT, + sub_title TEXT, + event_date TEXT, + event_time TEXT, + location_name TEXT, + location_address TEXT, + what_to_bring TEXT, + rules_and_gdpr TEXT + ); + + INSERT INTO event_content (id, title, sub_title, event_date, event_time, location_name, location_address, what_to_bring, rules_and_gdpr) + SELECT 1, '', '', '', '', '', '', '', '' + WHERE NOT EXISTS (SELECT 1 FROM event_content WHERE id = 1); "; await connection.ExecuteAsync(sql); @@ -75,5 +94,29 @@ public class MemberRepository(IConfiguration configuration) : IMemberRepository var sql = "DELETE FROM members"; await connection.ExecuteAsync(sql); } + + public async Task GetEventContent() + { + using var connection = CreateConnection(); + var content = await connection.QueryFirstOrDefaultAsync("SELECT title, sub_title as SubTitle, event_date as EventDate, event_time as EventTime, location_name as LocationName, location_address as LocationAddress, what_to_bring as WhatToBring, rules_and_gdpr as RulesAndGdpr FROM event_content WHERE id = 1"); + return content ?? new EventContent(); + } + + public async Task UpdateEventContent(EventContent content) + { + using var connection = CreateConnection(); + var sql = @" + UPDATE event_content SET + title = @Title, + sub_title = @SubTitle, + event_date = @EventDate, + event_time = @EventTime, + location_name = @LocationName, + location_address = @LocationAddress, + what_to_bring = @WhatToBring, + rules_and_gdpr = @RulesAndGdpr + WHERE id = 1"; + await connection.ExecuteAsync(sql, content); + } } diff --git a/src/Web/lan-frontend/app/admin/dashboard/page.tsx b/src/Web/lan-frontend/app/admin/dashboard/page.tsx new file mode 100644 index 0000000..4b0a051 --- /dev/null +++ b/src/Web/lan-frontend/app/admin/dashboard/page.tsx @@ -0,0 +1,201 @@ +"use client"; + +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; + +interface EventContent { + title: string; + subTitle: string; + eventDate: string; + eventTime: string; + locationName: string; + locationAddress: string; + whatToBring: string; + rulesAndGdpr: string; +} + +export default function AdminDashboard() { + const [content, setContent] = useState(null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [message, setMessage] = useState({ type: "", text: "" }); + const router = useRouter(); + + useEffect(() => { + const token = localStorage.getItem("adminToken"); + if (!token) { + router.push("/admin/login"); + return; + } + + fetch("/api/Content") + .then((res) => res.json()) + .then((data) => { + setContent(data); + setLoading(false); + }) + .catch((err) => { + console.error("Failed to fetch content", err); + setLoading(false); + }); + }, [router]); + + const handleLogout = () => { + localStorage.removeItem("adminToken"); + router.push("/admin/login"); + }; + + const handleSave = async (e: React.FormEvent) => { + e.preventDefault(); + if (!content) return; + setSaving(true); + setMessage({ type: "", text: "" }); + + try { + const response = await fetch("/api/Content", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(content), + }); + + if (response.ok) { + setMessage({ type: "success", text: "Content saved successfully!" }); + } else { + setMessage({ type: "error", text: "Failed to save content." }); + } + } catch (err) { + setMessage({ type: "error", text: "An error occurred while saving." }); + } finally { + setSaving(false); + } + }; + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setContent((prev) => prev ? { ...prev, [name]: value } : null); + }; + + if (loading) return
Loading...
; + if (!content) return
Error loading content.
; + + return ( +
+
+
+

Admin Dashboard

+
+ View Site + +
+
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +