Add change-password API and dynamic 404 redirect
Introduce ChangePasswordRequest DTO and a new ChangePassword endpoint in AuthController that validates input, changes the user's password via UserManager, updates the security stamp, signs out the user to invalidate sessions, and returns localized messages. Add a simple authorized AppUserController stub (GET /auth/user). Update the 404 view to resolve auth status via fetchCurrentUser, show a dynamic CTA/icon (Dashboard vs Home), auto-redirect after a short delay with proper timer cleanup, and adjust navigation behavior. Update codexInfo.md to document the 404 behavior change.
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace API.Contracts.Auth
|
||||||
|
{
|
||||||
|
public class ChangePasswordRequest
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public string OldPassword { get; set; } = string.Empty;
|
||||||
|
[Required]
|
||||||
|
public string NewPassword { get; set; } = string.Empty;
|
||||||
|
[Required]
|
||||||
|
public string NewPasswordConfirm { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace API.Controllers.Auth
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
[Route("auth/user")]
|
||||||
|
public class AppUserController : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpGet]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<IActionResult> GetAppUsers()
|
||||||
|
{
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -67,5 +67,51 @@ namespace API.Controllers.Auth
|
|||||||
MustChangePassword = user.MustChangePassword
|
MustChangePassword = user.MustChangePassword
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("password")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<IActionResult> ChangePassword([FromBody] ChangePasswordRequest pwChangeDto)
|
||||||
|
{
|
||||||
|
var user = await userManager.GetUserAsync(User);
|
||||||
|
if (user is null)
|
||||||
|
return Unauthorized();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(pwChangeDto.NewPassword) ||
|
||||||
|
string.IsNullOrWhiteSpace(pwChangeDto.OldPassword) ||
|
||||||
|
string.IsNullOrWhiteSpace(pwChangeDto.NewPasswordConfirm))
|
||||||
|
{
|
||||||
|
return BadRequest(new { message = "Alle Passwörter müssen einen Wert enthalten." });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pwChangeDto.NewPassword != pwChangeDto.NewPasswordConfirm)
|
||||||
|
{
|
||||||
|
return BadRequest(new { message = "Die neuen Passwörter stimmen nicht überein." });
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await userManager.ChangePasswordAsync(
|
||||||
|
user,
|
||||||
|
pwChangeDto.OldPassword,
|
||||||
|
pwChangeDto.NewPassword
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result.Succeeded)
|
||||||
|
{
|
||||||
|
return BadRequest(new
|
||||||
|
{
|
||||||
|
message = "Passwort konnte nicht geändert werden.",
|
||||||
|
errors = result.Errors.Select(e => e.Description)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var stampResult = await userManager.UpdateSecurityStampAsync(user);
|
||||||
|
if (!stampResult.Succeeded)
|
||||||
|
{
|
||||||
|
return StatusCode(500, new { message = "Passwort geändert, aber Sessions konnten nicht invalidiert werden." });
|
||||||
|
}
|
||||||
|
|
||||||
|
await signInManager.SignOutAsync();
|
||||||
|
|
||||||
|
return Ok(new { message = "Passwort geändert. Du wurdest auf allen Geräten abgemeldet." });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,74 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
import notFoundImage from '@/assets/images/404NotFound.png'
|
import notFoundImage from '@/assets/images/404NotFound.png'
|
||||||
|
import { fetchCurrentUser } from '@/services/authSession'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const isAuthenticated = ref(false)
|
||||||
|
let autoRedirectTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
const redirectRouteName = computed(() => (isAuthenticated.value ? 'Dashboard' : 'Home'))
|
||||||
|
const redirectButtonLabel = computed(() =>
|
||||||
|
isAuthenticated.value ? 'Zum Dashboard' : 'Zur Startseite',
|
||||||
|
)
|
||||||
|
const redirectButtonIcon = computed(() =>
|
||||||
|
isAuthenticated.value ? 'mdi-view-dashboard-outline' : 'mdi-home-outline',
|
||||||
|
)
|
||||||
|
const autoRedirectTargetLabel = computed(() => (isAuthenticated.value ? 'Dashboard' : 'Startseite'))
|
||||||
|
|
||||||
|
function clearAutoRedirectTimer() {
|
||||||
|
if (autoRedirectTimer === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTimeout(autoRedirectTimer)
|
||||||
|
autoRedirectTimer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleAutoRedirect() {
|
||||||
|
clearAutoRedirectTimer()
|
||||||
|
|
||||||
|
autoRedirectTimer = setTimeout(() => {
|
||||||
|
void router.replace({ name: redirectRouteName.value })
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveRedirectTarget() {
|
||||||
|
try {
|
||||||
|
const currentUser = await fetchCurrentUser()
|
||||||
|
isAuthenticated.value = currentUser !== null
|
||||||
|
} catch {
|
||||||
|
isAuthenticated.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleAutoRedirect()
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateToPrimaryTarget() {
|
||||||
|
clearAutoRedirectTimer()
|
||||||
|
void router.replace({ name: redirectRouteName.value })
|
||||||
|
}
|
||||||
|
|
||||||
function navigateBack() {
|
function navigateBack() {
|
||||||
|
clearAutoRedirectTimer()
|
||||||
|
|
||||||
if (window.history.length > 1) {
|
if (window.history.length > 1) {
|
||||||
router.back()
|
router.back()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
router.push({ name: 'Dashboard' })
|
void router.push({ name: redirectRouteName.value })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void resolveRedirectTarget()
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
clearAutoRedirectTimer()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -29,12 +85,17 @@ function navigateBack() {
|
|||||||
<h1>Seite nicht gefunden</h1>
|
<h1>Seite nicht gefunden</h1>
|
||||||
<p class="not-found-text">
|
<p class="not-found-text">
|
||||||
Der Link ist ungültig oder die Seite wurde verschoben. Du kannst direkt zur
|
Der Link ist ungültig oder die Seite wurde verschoben. Du kannst direkt zur
|
||||||
Dashboard-Seite zurück oder die vorherige Ansicht öffnen.
|
{{ autoRedirectTargetLabel }} weitergehen oder die vorherige Ansicht öffnen.
|
||||||
|
Du wirst automatisch dorthin weitergeleitet.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="not-found-actions hoard-action-row">
|
<div class="not-found-actions hoard-action-row">
|
||||||
<v-btn color="primary" prepend-icon="mdi-view-dashboard-outline" :to="{ name: 'Dashboard' }">
|
<v-btn
|
||||||
Zum Dashboard
|
color="primary"
|
||||||
|
:prepend-icon="redirectButtonIcon"
|
||||||
|
@click="navigateToPrimaryTarget"
|
||||||
|
>
|
||||||
|
{{ redirectButtonLabel }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn variant="outlined" prepend-icon="mdi-arrow-left" @click="navigateBack">Zurück</v-btn>
|
<v-btn variant="outlined" prepend-icon="mdi-arrow-left" @click="navigateBack">Zurück</v-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+2
-1
@@ -110,7 +110,7 @@ Ich baue alleine neben meiner Ausbildung eine einfache self-hosted Web-App für
|
|||||||
- Fehler werden jetzt global über einen Banner-Stack am unteren Seitenrand angezeigt (routeübergreifend, stapelbar, manuell schließbar).
|
- Fehler werden jetzt global über einen Banner-Stack am unteren Seitenrand angezeigt (routeübergreifend, stapelbar, manuell schließbar).
|
||||||
- Der globale Banner-Stack im Layout nutzt einen kontrollierten `z-index` und opake Hintergründe, damit Fehlermeldungen im Vordergrund bleiben und kaum durchscheinende Inhalte zeigen.
|
- Der globale Banner-Stack im Layout nutzt einen kontrollierten `z-index` und opake Hintergründe, damit Fehlermeldungen im Vordergrund bleiben und kaum durchscheinende Inhalte zeigen.
|
||||||
- Klick auf Logo/Branding in der Topbar führt abhängig vom Auth-Status: angemeldet auf `Dashboard`, unangemeldet auf `Welcome`.
|
- Klick auf Logo/Branding in der Topbar führt abhängig vom Auth-Status: angemeldet auf `Dashboard`, unangemeldet auf `Welcome`.
|
||||||
- 404-Seite verweist primär auf `Dashboard` (Button/Text), nicht mehr auf `Startseite`.
|
- 404-Seite löst den Auth-Status auf und leitet automatisch weiter: angemeldet zu `Dashboard`, unangemeldet zu `Welcome`; primärer CTA ist entsprechend dynamisch.
|
||||||
- Brand-Klick ist gegen Auth-Timing abgesichert: bei vorhandener Session führt Logo/Titel auch bei noch nicht synchronisiertem Layout-Status zuverlässig auf `Dashboard`.
|
- Brand-Klick ist gegen Auth-Timing abgesichert: bei vorhandener Session führt Logo/Titel auch bei noch nicht synchronisiertem Layout-Status zuverlässig auf `Dashboard`.
|
||||||
|
|
||||||
## Änderungen durch Codex
|
## Änderungen durch Codex
|
||||||
@@ -169,3 +169,4 @@ Ich baue alleine neben meiner Ausbildung eine einfache self-hosted Web-App für
|
|||||||
- `GUI/src/routes/404NotFound.vue` angepasst: primärer CTA zeigt jetzt `Zum Dashboard` (Route `Dashboard`), Text aktualisiert und `navigateBack()` fällt ohne History ebenfalls auf `Dashboard` zurück.
|
- `GUI/src/routes/404NotFound.vue` angepasst: primärer CTA zeigt jetzt `Zum Dashboard` (Route `Dashboard`), Text aktualisiert und `navigateBack()` fällt ohne History ebenfalls auf `Dashboard` zurück.
|
||||||
- `GUI/src/Layout.vue` Brand-Navigation nachgeschärft: `navigateToBrandTarget()` löst bei Klick zuerst die Session via `fetchCurrentUser()` auf (falls lokal noch `null`) und verhindert dadurch Fehlnavigation auf `Welcome`; Mobile-Account-Menütext auf `Zum Dashboard` vereinheitlicht.
|
- `GUI/src/Layout.vue` Brand-Navigation nachgeschärft: `navigateToBrandTarget()` löst bei Klick zuerst die Session via `fetchCurrentUser()` auf (falls lokal noch `null`) und verhindert dadurch Fehlnavigation auf `Welcome`; Mobile-Account-Menütext auf `Zum Dashboard` vereinheitlicht.
|
||||||
- Neuer globaler Skill `codexinfo-komprimieren` unter `C:/Users/famil/.codex/skills/codexinfo-komprimieren` erstellt; er liest `codexInfo.md`, verdichtet `Änderungen durch Codex` auf drei Kernzeilen und enthält ein Hilfsskript zum robusten Abschnitts-Update.
|
- Neuer globaler Skill `codexinfo-komprimieren` unter `C:/Users/famil/.codex/skills/codexinfo-komprimieren` erstellt; er liest `codexInfo.md`, verdichtet `Änderungen durch Codex` auf drei Kernzeilen und enthält ein Hilfsskript zum robusten Abschnitts-Update.
|
||||||
|
- `GUI/src/routes/404NotFound.vue` erweitert: Seite ermittelt beim Laden den Login-Status über `fetchCurrentUser()`, setzt CTA/Icon dynamisch auf `Zum Dashboard` oder `Zur Startseite` und leitet nach kurzer Verzögerung automatisch auf das passende Ziel weiter.
|
||||||
|
|||||||
Reference in New Issue
Block a user