user magic link inmplemented
This commit is contained in:
parent
06ffce8984
commit
9fc0c9bc4c
11 changed files with 1136 additions and 127 deletions
34
src/App.tsx
34
src/App.tsx
|
|
@ -6,6 +6,8 @@ import './index.css';
|
||||||
|
|
||||||
import Login from './pages/Login';
|
import Login from './pages/Login';
|
||||||
import Register from './pages/Register';
|
import Register from './pages/Register';
|
||||||
|
import PasswordResetRequest from './pages/PasswordResetRequest';
|
||||||
|
import Reset from './pages/Reset';
|
||||||
|
|
||||||
import { AuthProvider } from './providers/auth/AuthProvider';
|
import { AuthProvider } from './providers/auth/AuthProvider';
|
||||||
import { ProtectedRoute } from './providers/auth/ProtectedRoute';
|
import { ProtectedRoute } from './providers/auth/ProtectedRoute';
|
||||||
|
|
@ -39,25 +41,37 @@ function App() {
|
||||||
return (
|
return (
|
||||||
<LanguageProvider>
|
<LanguageProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<FileProvider>
|
|
||||||
<WorkflowSelectionProvider>
|
|
||||||
<Router>
|
<Router>
|
||||||
<Routes>
|
<Routes>
|
||||||
{/* Public route */}
|
{/* PUBLIC AUTH ROUTES - NO AUTHENTICATION REQUIRED */}
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/register" element={<Register />} />
|
<Route path="/register" element={<Register />} />
|
||||||
|
<Route path="/password-reset-request" element={<PasswordResetRequest />} />
|
||||||
|
<Route path="/reset" element={<Reset />} />
|
||||||
|
|
||||||
|
{/* PROTECTED ROUTE - requires authentication */}
|
||||||
<Route path="/" element={
|
<Route path="/" element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
|
<FileProvider>
|
||||||
|
<WorkflowSelectionProvider>
|
||||||
<Home />
|
<Home />
|
||||||
</ProtectedRoute>
|
|
||||||
}>
|
|
||||||
{/* All page routing is now handled by the Page Loader in Home.tsx */}
|
|
||||||
<Route path="*" element={null} />
|
|
||||||
</Route>
|
|
||||||
</Routes>
|
|
||||||
</Router>
|
|
||||||
</WorkflowSelectionProvider>
|
</WorkflowSelectionProvider>
|
||||||
</FileProvider>
|
</FileProvider>
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
|
||||||
|
{/* Catch-all redirect to home */}
|
||||||
|
<Route path="*" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<FileProvider>
|
||||||
|
<WorkflowSelectionProvider>
|
||||||
|
<Home />
|
||||||
|
</WorkflowSelectionProvider>
|
||||||
|
</FileProvider>
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
</Routes>
|
||||||
|
</Router>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</LanguageProvider>
|
</LanguageProvider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ export interface LoginResponse {
|
||||||
|
|
||||||
export interface RegisterData {
|
export interface RegisterData {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
|
||||||
email: string;
|
email: string;
|
||||||
fullName: string;
|
fullName: string;
|
||||||
language?: string;
|
language?: string;
|
||||||
|
|
@ -38,8 +37,19 @@ export interface RegisterRequest {
|
||||||
language: string;
|
language: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
privilege: string;
|
privilege: string;
|
||||||
|
authenticationAuthority: string;
|
||||||
};
|
};
|
||||||
password: string;
|
frontendUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PasswordResetRequestResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PasswordResetResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RegisterResponse {
|
export interface RegisterResponse {
|
||||||
|
|
@ -142,11 +152,13 @@ export async function fetchCurrentUserApi(authAuthority?: string): Promise<User>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a new user
|
* Register a new user (magic link based - no password required)
|
||||||
* Endpoint: POST /api/local/register
|
* Endpoint: POST /api/local/register
|
||||||
|
*
|
||||||
|
* After registration, user receives an email with a magic link to set their password.
|
||||||
*/
|
*/
|
||||||
export async function registerApi(registerData: RegisterData): Promise<RegisterResponse> {
|
export async function registerApi(registerData: RegisterData): Promise<RegisterResponse> {
|
||||||
// Prepare data to match backend expectations
|
// Prepare data to match backend expectations (no password - magic link flow)
|
||||||
const dataToSend: RegisterRequest = {
|
const dataToSend: RegisterRequest = {
|
||||||
userData: {
|
userData: {
|
||||||
username: registerData.username,
|
username: registerData.username,
|
||||||
|
|
@ -154,9 +166,10 @@ export async function registerApi(registerData: RegisterData): Promise<RegisterR
|
||||||
fullName: registerData.fullName,
|
fullName: registerData.fullName,
|
||||||
language: registerData.language || 'de',
|
language: registerData.language || 'de',
|
||||||
enabled: registerData.enabled !== undefined ? registerData.enabled : true,
|
enabled: registerData.enabled !== undefined ? registerData.enabled : true,
|
||||||
privilege: registerData.privilege || 'user'
|
privilege: registerData.privilege || 'user',
|
||||||
|
authenticationAuthority: 'local'
|
||||||
},
|
},
|
||||||
password: registerData.password
|
frontendUrl: window.location.origin
|
||||||
};
|
};
|
||||||
|
|
||||||
// Prepare headers with CSRF token if available
|
// Prepare headers with CSRF token if available
|
||||||
|
|
@ -174,19 +187,64 @@ export async function registerApi(registerData: RegisterData): Promise<RegisterR
|
||||||
const userData: any = response.data;
|
const userData: any = response.data;
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Registration successful',
|
message: 'Registration successful - check email for password setup link',
|
||||||
user: userData && typeof userData === 'object' && 'id' in userData ? {
|
user: userData && typeof userData === 'object' && 'id' in userData ? {
|
||||||
id: String(userData.id || ''),
|
id: String(userData.id || ''),
|
||||||
username: String(userData.username || ''),
|
username: String(userData.username || ''),
|
||||||
email: String(userData.email || ''),
|
email: String(userData.email || ''),
|
||||||
fullName: String(userData.fullName || ''),
|
fullName: String(userData.fullName || ''),
|
||||||
language: String(userData.language || 'en'),
|
language: String(userData.language || 'de'),
|
||||||
enabled: Boolean(userData.enabled !== false),
|
enabled: Boolean(userData.enabled !== false),
|
||||||
privilege: String(userData.privilege || 'user')
|
privilege: String(userData.privilege || 'user')
|
||||||
} : undefined
|
} : undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request password reset by username
|
||||||
|
* Endpoint: POST /api/local/password-reset-request
|
||||||
|
*
|
||||||
|
* Sends a reset email to the user's registered email address.
|
||||||
|
*/
|
||||||
|
export async function requestPasswordResetApi(username: string): Promise<PasswordResetRequestResponse> {
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
};
|
||||||
|
|
||||||
|
addCSRFTokenToHeaders(headers);
|
||||||
|
|
||||||
|
const response = await api.post<PasswordResetRequestResponse>(
|
||||||
|
'/api/local/password-reset-request',
|
||||||
|
{
|
||||||
|
username,
|
||||||
|
frontendUrl: window.location.origin
|
||||||
|
},
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset password using token from magic link
|
||||||
|
* Endpoint: POST /api/local/password-reset
|
||||||
|
*/
|
||||||
|
export async function resetPasswordApi(token: string, password: string): Promise<PasswordResetResponse> {
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
};
|
||||||
|
|
||||||
|
addCSRFTokenToHeaders(headers);
|
||||||
|
|
||||||
|
const response = await api.post<PasswordResetResponse>(
|
||||||
|
'/api/local/password-reset',
|
||||||
|
{ token, password },
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register with Microsoft account
|
* Register with Microsoft account
|
||||||
* Endpoint: POST /api/msft/register
|
* Endpoint: POST /api/msft/register
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,15 @@ import {
|
||||||
registerWithMsalApi,
|
registerWithMsalApi,
|
||||||
checkUsernameAvailabilityApi,
|
checkUsernameAvailabilityApi,
|
||||||
logoutApi,
|
logoutApi,
|
||||||
|
requestPasswordResetApi,
|
||||||
|
resetPasswordApi,
|
||||||
type LoginResponse,
|
type LoginResponse,
|
||||||
type RegisterResponse,
|
type RegisterResponse,
|
||||||
type UsernameAvailabilityResponse,
|
type UsernameAvailabilityResponse,
|
||||||
type RegisterData,
|
type RegisterData,
|
||||||
type MsalRegisterData
|
type MsalRegisterData,
|
||||||
|
type PasswordResetRequestResponse,
|
||||||
|
type PasswordResetResponse
|
||||||
} from '../api/authApi';
|
} from '../api/authApi';
|
||||||
|
|
||||||
// Regular authentication
|
// Regular authentication
|
||||||
|
|
@ -480,6 +484,84 @@ export function useUsernameAvailability() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Password reset request (by username)
|
||||||
|
export function usePasswordResetRequest() {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [success, setSuccess] = useState(false);
|
||||||
|
|
||||||
|
const requestReset = async (username: string): Promise<PasswordResetRequestResponse> => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setSuccess(false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await requestPasswordResetApi(username);
|
||||||
|
setSuccess(true);
|
||||||
|
return response;
|
||||||
|
} catch (error: any) {
|
||||||
|
// For security, we don't reveal if the username exists or not
|
||||||
|
// So we still show success even on error
|
||||||
|
setSuccess(true);
|
||||||
|
return { success: true, message: 'If a user with this username exists, a reset link has been sent to their email.' };
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
requestReset,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
success
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password reset (set new password with token)
|
||||||
|
export function usePasswordReset() {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [success, setSuccess] = useState(false);
|
||||||
|
|
||||||
|
const resetPassword = async (token: string, password: string): Promise<PasswordResetResponse> => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setSuccess(false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await resetPasswordApi(token, password);
|
||||||
|
setSuccess(true);
|
||||||
|
return response;
|
||||||
|
} catch (error: any) {
|
||||||
|
let errorMessage = 'Passwort-Zurücksetzung fehlgeschlagen';
|
||||||
|
|
||||||
|
if (error.response) {
|
||||||
|
if (error.response.data?.detail) {
|
||||||
|
if (Array.isArray(error.response.data.detail)) {
|
||||||
|
errorMessage = error.response.data.detail.map((err: any) => err.msg || err).join(', ');
|
||||||
|
} else {
|
||||||
|
errorMessage = error.response.data.detail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (error.message) {
|
||||||
|
errorMessage = error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(errorMessage);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
resetPassword,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
success
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Logout function
|
// Logout function
|
||||||
export function useLogout() {
|
export function useLogout() {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
|
||||||
|
|
@ -263,3 +263,18 @@ button:disabled {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-family: var(--font-family);
|
font-family: var(--font-family);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.passwordResetLink {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: -0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.passwordResetLink .textButton {
|
||||||
|
color: #9CA3AF;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.passwordResetLink .textButton:hover {
|
||||||
|
color: #F25843;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,15 @@ function Login() {
|
||||||
{isLoginLoading ? "wird geladen..." : "Anmelden"}
|
{isLoginLoading ? "wird geladen..." : "Anmelden"}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<div className={styles.passwordResetLink}>
|
||||||
|
<button
|
||||||
|
className={styles.textButton}
|
||||||
|
onClick={() => navigate("/password-reset-request")}
|
||||||
|
>
|
||||||
|
Passwort vergessen?
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={styles.divider}>
|
<div className={styles.divider}>
|
||||||
<span>oder</span>
|
<span>oder</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
242
src/pages/PasswordResetRequest.module.css
Normal file
242
src/pages/PasswordResetRequest.module.css
Normal file
|
|
@ -0,0 +1,242 @@
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
|
||||||
|
font-family: "DM Sans", sans-serif;
|
||||||
|
color: #181818;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainContent {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 3rem;
|
||||||
|
background-color: #181818;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginSection {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logoText {
|
||||||
|
|
||||||
|
font-size: 35px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
font-weight: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logoPower {
|
||||||
|
color: #E5E7EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logoOn {
|
||||||
|
color: #F25843;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo img {
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginBox {
|
||||||
|
|
||||||
|
|
||||||
|
background-color: #181818;
|
||||||
|
width: 25%;
|
||||||
|
height: auto;
|
||||||
|
|
||||||
|
margin-top: 5%;
|
||||||
|
padding: 2rem;
|
||||||
|
|
||||||
|
border-radius: 25px;
|
||||||
|
border: 1px solid rgba(199, 197, 178, 0.15); /* washed-out color */
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02),
|
||||||
|
0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-family: "DM Sans", sans-serif;
|
||||||
|
color: #E5E7EB;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginForm {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floatingLabelInput {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
position: absolute;
|
||||||
|
left: 16px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: #C7C5B2;
|
||||||
|
font-size: 1rem;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background-color: transparent;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
.focusedLabel {
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
top: -8px;
|
||||||
|
transform: translateY(0);
|
||||||
|
color: #F25843;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background-color: #181818;
|
||||||
|
padding: 0 4px;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 1px solid var(--color-gray-disabled);
|
||||||
|
border-radius: 25px;
|
||||||
|
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #F25843;
|
||||||
|
box-shadow: 0 0 0 2px rgba(242, 88, 67, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input::placeholder {
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix browser autocomplete styling */
|
||||||
|
.input:-webkit-autofill,
|
||||||
|
.input:-webkit-autofill:hover,
|
||||||
|
.input:-webkit-autofill:focus,
|
||||||
|
.input:-webkit-autofill:active {
|
||||||
|
-webkit-box-shadow: 0 0 0 30px #181818 inset !important;
|
||||||
|
-webkit-text-fill-color: #E5E7EB !important;
|
||||||
|
background-color: #181818 !important;
|
||||||
|
transition: background-color 5000s ease-in-out 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure label background matches when autofilled */
|
||||||
|
.input:-webkit-autofill + .label,
|
||||||
|
.input:-webkit-autofill + .focusedLabel {
|
||||||
|
background-color: #181818 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 25px;
|
||||||
|
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: none;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginButton {
|
||||||
|
background-color: #F25843;
|
||||||
|
color: #E5E7EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginButton:hover {
|
||||||
|
background-color: var(--color-secondary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.registerLink {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registerLink span {
|
||||||
|
color: #E5E7EB;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textButton {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--color-secondary);
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
.textButton:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: var(--color-secondary);
|
||||||
|
background-color: var(--color-secondary-disabled);
|
||||||
|
border: 1px solid var(--color-secondary);
|
||||||
|
border-radius: 25px;
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-align: center;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
color: #10b981;
|
||||||
|
background-color: rgba(16, 185, 129, 0.1);
|
||||||
|
border: 1px solid #10b981;
|
||||||
|
border-radius: 25px;
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-align: center;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoMessage {
|
||||||
|
background-color: rgba(59, 130, 246, 0.1);
|
||||||
|
border: 1px solid rgba(59, 130, 246, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #93c5fd;
|
||||||
|
text-align: center;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoMessage p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
129
src/pages/PasswordResetRequest.tsx
Normal file
129
src/pages/PasswordResetRequest.tsx
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import styles from './PasswordResetRequest.module.css';
|
||||||
|
import { usePasswordResetRequest } from '../hooks/useAuthentication';
|
||||||
|
import { generateAndStoreCSRFToken } from '../utils/csrfUtils';
|
||||||
|
|
||||||
|
function PasswordResetRequest() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { requestReset, isLoading, success } = usePasswordResetRequest();
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [usernameFocused, setUsernameFocused] = useState(false);
|
||||||
|
const [validationError, setValidationError] = useState<string | null>(null);
|
||||||
|
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Set page title and generate CSRF token
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = "PowerOn AI Platform - Passwort zurücksetzen";
|
||||||
|
generateAndStoreCSRFToken();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setValidationError(null);
|
||||||
|
|
||||||
|
if (!username.trim()) {
|
||||||
|
setValidationError('Bitte geben Sie Ihren Benutzernamen ein.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await requestReset(username.trim());
|
||||||
|
setSuccessMessage('Falls ein Konto mit diesem Benutzernamen existiert, wurde ein Reset-Link an die hinterlegte E-Mail-Adresse gesendet. Bitte prüfen Sie auch Ihren Spam-Ordner.');
|
||||||
|
|
||||||
|
// Redirect to login after delay
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate('/login', {
|
||||||
|
state: {
|
||||||
|
passwordResetRequested: true,
|
||||||
|
message: 'Bitte prüfen Sie Ihre E-Mail für den Passwort-Reset-Link.'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
|
} catch (err) {
|
||||||
|
// For security, still show success message even on error
|
||||||
|
setSuccessMessage('Falls ein Konto mit diesem Benutzernamen existiert, wurde ein Reset-Link an die hinterlegte E-Mail-Adresse gesendet. Bitte prüfen Sie auch Ihren Spam-Ordner.');
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate('/login');
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.mainContent}>
|
||||||
|
<div className={styles.logo}>
|
||||||
|
<div className={styles.logoText}>
|
||||||
|
<span className={styles.logoPower}>Power</span>
|
||||||
|
<span className={styles.logoOn}>On</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.loginSection}>
|
||||||
|
<div className={styles.loginBox}>
|
||||||
|
<h2 className={styles.title}>Passwort zurücksetzen</h2>
|
||||||
|
<div className={styles.loginForm}>
|
||||||
|
{validationError && (
|
||||||
|
<div className={styles.error}>{validationError}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{successMessage && (
|
||||||
|
<div className={styles.success}>{successMessage}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!successMessage && (
|
||||||
|
<>
|
||||||
|
<div className={styles.floatingLabelInput}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder=" "
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => {
|
||||||
|
setUsername(e.target.value);
|
||||||
|
setValidationError(null);
|
||||||
|
}}
|
||||||
|
onFocus={() => setUsernameFocused(true)}
|
||||||
|
onBlur={() => setUsernameFocused(false)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSubmit(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={`${styles.input} ${usernameFocused || username ? styles.focused : ''}`}
|
||||||
|
/>
|
||||||
|
<label className={usernameFocused || username ? styles.focusedLabel : styles.label}>Benutzername</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.infoMessage}>
|
||||||
|
<p>Geben Sie Ihren Benutzernamen ein. Falls ein Konto existiert, erhalten Sie einen Link zum Zurücksetzen des Passworts an Ihre hinterlegte E-Mail-Adresse.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className={`${styles.button} ${styles.loginButton}`}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? "Wird gesendet..." : "Reset-Link anfordern"}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.registerLink}>
|
||||||
|
<span>Zurück zum</span>
|
||||||
|
<button
|
||||||
|
className={styles.textButton}
|
||||||
|
onClick={() => navigate("/login")}
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PasswordResetRequest;
|
||||||
|
|
@ -227,3 +227,30 @@ button:disabled {
|
||||||
font-family: var(--font-family);
|
font-family: var(--font-family);
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
color: #10b981;
|
||||||
|
background-color: rgba(16, 185, 129, 0.1);
|
||||||
|
border: 1px solid #10b981;
|
||||||
|
border-radius: 25px;
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-align: center;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoMessage {
|
||||||
|
background-color: rgba(59, 130, 246, 0.1);
|
||||||
|
border: 1px solid rgba(59, 130, 246, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #93c5fd;
|
||||||
|
text-align: center;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoMessage p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,6 @@ import { generateAndStoreCSRFToken } from '../utils/csrfUtils';
|
||||||
|
|
||||||
interface RegisterFormData {
|
interface RegisterFormData {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
|
||||||
confirmPassword: string;
|
|
||||||
email: string;
|
email: string;
|
||||||
fullName: string;
|
fullName: string;
|
||||||
}
|
}
|
||||||
|
|
@ -20,15 +18,12 @@ function Register() {
|
||||||
const { checkAvailability, isChecking, error: availabilityError } = useUsernameAvailability();
|
const { checkAvailability, isChecking, error: availabilityError } = useUsernameAvailability();
|
||||||
const [formData, setFormData] = useState<RegisterFormData>({
|
const [formData, setFormData] = useState<RegisterFormData>({
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
|
||||||
confirmPassword: '',
|
|
||||||
email: '',
|
email: '',
|
||||||
fullName: ''
|
fullName: ''
|
||||||
});
|
});
|
||||||
const [validationError, setValidationError] = useState<string | null>(null);
|
const [validationError, setValidationError] = useState<string | null>(null);
|
||||||
|
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||||
const [usernameFocused, setUsernameFocused] = useState(false);
|
const [usernameFocused, setUsernameFocused] = useState(false);
|
||||||
const [passwordFocused, setPasswordFocused] = useState(false);
|
|
||||||
const [confirmPasswordFocused, setConfirmPasswordFocused] = useState(false);
|
|
||||||
const [emailFocused, setEmailFocused] = useState(false);
|
const [emailFocused, setEmailFocused] = useState(false);
|
||||||
const [fullNameFocused, setFullNameFocused] = useState(false);
|
const [fullNameFocused, setFullNameFocused] = useState(false);
|
||||||
const [usernameHighlight, setUsernameHighlight] = useState(false);
|
const [usernameHighlight, setUsernameHighlight] = useState(false);
|
||||||
|
|
@ -55,16 +50,11 @@ function Register() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateForm = (): boolean => {
|
const validateForm = (): boolean => {
|
||||||
if (!formData.username || !formData.password || !formData.confirmPassword || !formData.email || !formData.fullName) {
|
if (!formData.username || !formData.email || !formData.fullName) {
|
||||||
setValidationError('Bitte füllen Sie alle Pflichtfelder aus.');
|
setValidationError('Bitte füllen Sie alle Pflichtfelder aus.');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (formData.password !== formData.confirmPassword) {
|
|
||||||
setValidationError('Die Passwörter stimmen nicht überein.');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.email.includes('@')) {
|
if (!formData.email.includes('@')) {
|
||||||
setValidationError('Bitte geben Sie eine gültige E-Mail-Adresse ein.');
|
setValidationError('Bitte geben Sie eine gültige E-Mail-Adresse ein.');
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -96,15 +86,21 @@ function Register() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Username is available, proceed with registration
|
// Username is available, proceed with registration (no password - magic link flow)
|
||||||
const { confirmPassword, ...registrationData } = formData;
|
await register(formData);
|
||||||
await register(registrationData);
|
|
||||||
|
// Show success message instead of immediate redirect
|
||||||
|
setSuccessMessage('Registrierung erfolgreich! Bitte prüfen Sie Ihre E-Mail (auch den Spam-Ordner) für den Link zum Setzen Ihres Passworts.');
|
||||||
|
|
||||||
|
// Redirect to login page after delay
|
||||||
|
setTimeout(() => {
|
||||||
navigate('/login', {
|
navigate('/login', {
|
||||||
state: {
|
state: {
|
||||||
registered: true,
|
registered: true,
|
||||||
message: 'Registration erfolgreich. Bitte melden Sie sich an.'
|
message: 'Registrierung erfolgreich. Bitte prüfen Sie Ihre E-Mail für den Passwort-Link.'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}, 5000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Registration failed:', err);
|
console.error('Registration failed:', err);
|
||||||
}
|
}
|
||||||
|
|
@ -135,6 +131,12 @@ function Register() {
|
||||||
<div className={styles.error}>{getErrorMessage()}</div>
|
<div className={styles.error}>{getErrorMessage()}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{successMessage && (
|
||||||
|
<div className={styles.success}>{successMessage}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!successMessage && (
|
||||||
|
<>
|
||||||
<div className={styles.floatingLabelInput}>
|
<div className={styles.floatingLabelInput}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -177,32 +179,8 @@ function Register() {
|
||||||
<label className={fullNameFocused || formData.fullName ? styles.focusedLabel : styles.label}>Vollständiger Name</label>
|
<label className={fullNameFocused || formData.fullName ? styles.focusedLabel : styles.label}>Vollständiger Name</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.floatingLabelInput}>
|
<div className={styles.infoMessage}>
|
||||||
<input
|
<p>Nach der Registrierung erhalten Sie eine E-Mail mit einem Link zum Setzen Ihres Passworts.</p>
|
||||||
type="password"
|
|
||||||
name="password"
|
|
||||||
placeholder=" "
|
|
||||||
value={formData.password}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
onFocus={() => setPasswordFocused(true)}
|
|
||||||
onBlur={() => setPasswordFocused(false)}
|
|
||||||
className={`${styles.input} ${passwordFocused || formData.password ? styles.focused : ''}`}
|
|
||||||
/>
|
|
||||||
<label className={passwordFocused || formData.password ? styles.focusedLabel : styles.label}>Passwort</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.floatingLabelInput}>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
name="confirmPassword"
|
|
||||||
placeholder=" "
|
|
||||||
value={formData.confirmPassword}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
onFocus={() => setConfirmPasswordFocused(true)}
|
|
||||||
onBlur={() => setConfirmPasswordFocused(false)}
|
|
||||||
className={`${styles.input} ${confirmPasswordFocused || formData.confirmPassword ? styles.focused : ''}`}
|
|
||||||
/>
|
|
||||||
<label className={confirmPasswordFocused || formData.confirmPassword ? styles.focusedLabel : styles.label}>Passwort bestätigen</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.disclaimer}>
|
<div className={styles.disclaimer}>
|
||||||
|
|
@ -218,6 +196,8 @@ function Register() {
|
||||||
>
|
>
|
||||||
{isLoading ? "Registrierung läuft..." : isChecking ? "Benutzername wird geprüft..." : "Registrieren"}
|
{isLoading ? "Registrierung läuft..." : isChecking ? "Benutzername wird geprüft..." : "Registrieren"}
|
||||||
</button>
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={styles.registerLink}>
|
<div className={styles.registerLink}>
|
||||||
<span>Bereits registriert?</span>
|
<span>Bereits registriert?</span>
|
||||||
|
|
|
||||||
234
src/pages/Reset.module.css
Normal file
234
src/pages/Reset.module.css
Normal file
|
|
@ -0,0 +1,234 @@
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
|
||||||
|
font-family: "DM Sans", sans-serif;
|
||||||
|
color: #181818;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainContent {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 3rem;
|
||||||
|
background-color: #181818;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginSection {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logoText {
|
||||||
|
|
||||||
|
font-size: 35px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
font-weight: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logoPower {
|
||||||
|
color: #E5E7EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logoOn {
|
||||||
|
color: #F25843;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo img {
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginBox {
|
||||||
|
|
||||||
|
|
||||||
|
background-color: #181818;
|
||||||
|
width: 25%;
|
||||||
|
height: auto;
|
||||||
|
|
||||||
|
margin-top: 5%;
|
||||||
|
padding: 2rem;
|
||||||
|
|
||||||
|
border-radius: 25px;
|
||||||
|
border: 1px solid rgba(199, 197, 178, 0.15); /* washed-out color */
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02),
|
||||||
|
0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-family: "DM Sans", sans-serif;
|
||||||
|
color: #E5E7EB;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginForm {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floatingLabelInput {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
position: absolute;
|
||||||
|
left: 16px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: #C7C5B2;
|
||||||
|
font-size: 1rem;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background-color: transparent;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
.focusedLabel {
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
top: -8px;
|
||||||
|
transform: translateY(0);
|
||||||
|
color: #F25843;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background-color: #181818;
|
||||||
|
padding: 0 4px;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 1px solid var(--color-gray-disabled);
|
||||||
|
border-radius: 25px;
|
||||||
|
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #F25843;
|
||||||
|
box-shadow: 0 0 0 2px rgba(242, 88, 67, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input::placeholder {
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix browser autocomplete styling */
|
||||||
|
.input:-webkit-autofill,
|
||||||
|
.input:-webkit-autofill:hover,
|
||||||
|
.input:-webkit-autofill:focus,
|
||||||
|
.input:-webkit-autofill:active {
|
||||||
|
-webkit-box-shadow: 0 0 0 30px #181818 inset !important;
|
||||||
|
-webkit-text-fill-color: #E5E7EB !important;
|
||||||
|
background-color: #181818 !important;
|
||||||
|
transition: background-color 5000s ease-in-out 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure label background matches when autofilled */
|
||||||
|
.input:-webkit-autofill + .label,
|
||||||
|
.input:-webkit-autofill + .focusedLabel {
|
||||||
|
background-color: #181818 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.passwordHint {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #9CA3AF;
|
||||||
|
margin-top: -0.5rem;
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 25px;
|
||||||
|
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: none;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginButton {
|
||||||
|
background-color: #F25843;
|
||||||
|
color: #E5E7EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginButton:hover {
|
||||||
|
background-color: var(--color-secondary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.registerLink {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registerLink span {
|
||||||
|
color: #E5E7EB;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textButton {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--color-secondary);
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
.textButton:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: var(--color-secondary);
|
||||||
|
background-color: var(--color-secondary-disabled);
|
||||||
|
border: 1px solid var(--color-secondary);
|
||||||
|
border-radius: 25px;
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-align: center;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
color: #10b981;
|
||||||
|
background-color: rgba(16, 185, 129, 0.1);
|
||||||
|
border: 1px solid #10b981;
|
||||||
|
border-radius: 25px;
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-align: center;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
219
src/pages/Reset.tsx
Normal file
219
src/pages/Reset.tsx
Normal file
|
|
@ -0,0 +1,219 @@
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import styles from './Reset.module.css';
|
||||||
|
import { usePasswordReset } from '../hooks/useAuthentication';
|
||||||
|
import { generateAndStoreCSRFToken } from '../utils/csrfUtils';
|
||||||
|
|
||||||
|
function Reset() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const { resetPassword, isLoading, error } = usePasswordReset();
|
||||||
|
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
|
const [passwordFocused, setPasswordFocused] = useState(false);
|
||||||
|
const [confirmPasswordFocused, setConfirmPasswordFocused] = useState(false);
|
||||||
|
const [validationError, setValidationError] = useState<string | null>(null);
|
||||||
|
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||||
|
const [tokenError, setTokenError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Get token from URL
|
||||||
|
const token = searchParams.get('token');
|
||||||
|
|
||||||
|
// Set page title and generate CSRF token
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = "PowerOn AI Platform - Neues Passwort setzen";
|
||||||
|
generateAndStoreCSRFToken();
|
||||||
|
|
||||||
|
// Validate token exists and format
|
||||||
|
if (!token) {
|
||||||
|
setTokenError('Ungültiger Reset-Link. Bitte fordern Sie einen neuen Link an.');
|
||||||
|
} else if (!_isValidUUID(token)) {
|
||||||
|
setTokenError('Ungültiger Reset-Link. Bitte fordern Sie einen neuen Link an.');
|
||||||
|
}
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
const _isValidUUID = (str: string): boolean => {
|
||||||
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||||
|
return uuidRegex.test(str);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateForm = (): boolean => {
|
||||||
|
if (!password || password.length < 8) {
|
||||||
|
setValidationError('Passwort muss mindestens 8 Zeichen lang sein.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
setValidationError('Die Passwörter stimmen nicht überein.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setValidationError(null);
|
||||||
|
|
||||||
|
if (!validateForm()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
setValidationError('Token fehlt. Bitte fordern Sie einen neuen Reset-Link an.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await resetPassword(token, password);
|
||||||
|
setSuccessMessage('Passwort erfolgreich gesetzt! Sie werden zum Login weitergeleitet...');
|
||||||
|
|
||||||
|
// Redirect to login after delay
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate('/login', {
|
||||||
|
state: {
|
||||||
|
passwordReset: true,
|
||||||
|
message: 'Passwort erfolgreich geändert. Bitte melden Sie sich an.'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 3000);
|
||||||
|
} catch (err: any) {
|
||||||
|
// Error is already set by the hook
|
||||||
|
const errorMessage = err?.response?.data?.detail || err?.message || 'Passwort-Zurücksetzung fehlgeschlagen.';
|
||||||
|
if (errorMessage.includes('abgelaufen') || errorMessage.includes('expired') || errorMessage.includes('Ungültig') || errorMessage.includes('invalid')) {
|
||||||
|
setValidationError('Der Reset-Link ist ungültig oder abgelaufen. Bitte fordern Sie einen neuen Link an.');
|
||||||
|
} else {
|
||||||
|
setValidationError(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show token error if invalid
|
||||||
|
if (tokenError) {
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.mainContent}>
|
||||||
|
<div className={styles.logo}>
|
||||||
|
<div className={styles.logoText}>
|
||||||
|
<span className={styles.logoPower}>Power</span>
|
||||||
|
<span className={styles.logoOn}>On</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.loginSection}>
|
||||||
|
<div className={styles.loginBox}>
|
||||||
|
<h2 className={styles.title}>Neues Passwort setzen</h2>
|
||||||
|
<div className={styles.loginForm}>
|
||||||
|
<div className={styles.error}>{tokenError}</div>
|
||||||
|
<div className={styles.registerLink}>
|
||||||
|
<button
|
||||||
|
className={styles.textButton}
|
||||||
|
onClick={() => navigate("/password-reset-request")}
|
||||||
|
>
|
||||||
|
Neuen Reset-Link anfordern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className={styles.registerLink}>
|
||||||
|
<span>oder zurück zum</span>
|
||||||
|
<button
|
||||||
|
className={styles.textButton}
|
||||||
|
onClick={() => navigate("/login")}
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.mainContent}>
|
||||||
|
<div className={styles.logo}>
|
||||||
|
<div className={styles.logoText}>
|
||||||
|
<span className={styles.logoPower}>Power</span>
|
||||||
|
<span className={styles.logoOn}>On</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.loginSection}>
|
||||||
|
<div className={styles.loginBox}>
|
||||||
|
<h2 className={styles.title}>Neues Passwort setzen</h2>
|
||||||
|
<div className={styles.loginForm}>
|
||||||
|
{(validationError || error) && (
|
||||||
|
<div className={styles.error}>{validationError || error}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{successMessage && (
|
||||||
|
<div className={styles.success}>{successMessage}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!successMessage && (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className={styles.floatingLabelInput}>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder=" "
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => {
|
||||||
|
setPassword(e.target.value);
|
||||||
|
setValidationError(null);
|
||||||
|
}}
|
||||||
|
onFocus={() => setPasswordFocused(true)}
|
||||||
|
onBlur={() => setPasswordFocused(false)}
|
||||||
|
className={`${styles.input} ${passwordFocused || password ? styles.focused : ''}`}
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
<label className={passwordFocused || password ? styles.focusedLabel : styles.label}>Neues Passwort</label>
|
||||||
|
</div>
|
||||||
|
<div className={styles.passwordHint}>Mindestens 8 Zeichen</div>
|
||||||
|
|
||||||
|
<div className={styles.floatingLabelInput}>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder=" "
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => {
|
||||||
|
setConfirmPassword(e.target.value);
|
||||||
|
setValidationError(null);
|
||||||
|
}}
|
||||||
|
onFocus={() => setConfirmPasswordFocused(true)}
|
||||||
|
onBlur={() => setConfirmPasswordFocused(false)}
|
||||||
|
className={`${styles.input} ${confirmPasswordFocused || confirmPassword ? styles.focused : ''}`}
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
<label className={confirmPasswordFocused || confirmPassword ? styles.focusedLabel : styles.label}>Passwort bestätigen</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={`${styles.button} ${styles.loginButton}`}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? "Wird gespeichert..." : "Passwort setzen"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.registerLink}>
|
||||||
|
<span>Zurück zum</span>
|
||||||
|
<button
|
||||||
|
className={styles.textButton}
|
||||||
|
onClick={() => navigate("/login")}
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Reset;
|
||||||
Loading…
Reference in a new issue