diff --git a/api/src/Feature.Auth/ForgotPassword/Endpoint.cs b/api/src/Feature.Auth/ForgotPassword/Endpoint.cs index b0a41ff92..b4310642f 100644 --- a/api/src/Feature.Auth/ForgotPassword/Endpoint.cs +++ b/api/src/Feature.Auth/ForgotPassword/Endpoint.cs @@ -27,13 +27,24 @@ public override void Configure() public override async Task> ExecuteAsync(Request request, CancellationToken ct) { var user = await userManager.FindByEmailAsync(request.Email.Trim()); - if (user is null || !await userManager.IsEmailConfirmedAsync(user)) + if (user is null) { logger.LogWarning("Possible user enumeration. Unknown email received {email}", request.Email); // Don't reveal that the user does not exist or is not confirmed return TypedResults.Ok(); } + var mail = await userManager.IsEmailConfirmedAsync(user) + ? await GetResetPasswordEmail(user) + : GetAcceptInviteEmail(user); + + jobService.EnqueueSendEmail(request.Email, mail.Subject, mail.Body); + + return TypedResults.Ok(); + } + + private async Task GetResetPasswordEmail(ApplicationUser user) + { // For more information on how to enable account confirmation and password reset please // visit https://go.microsoft.com/fwlink/?LinkID=532713 var code = await userManager.GeneratePasswordResetTokenAsync(user); @@ -45,8 +56,21 @@ public override async Task> ExecuteAsync(Request var emailProps = new ResetPasswordEmailProps(FullName: user.DisplayName, CdnUrl: _apiConfig.WebAppUrl, ResetPasswordUrl: passwordResetUrl); var mail = emailFactory.GenerateResetPasswordEmail(emailProps); - jobService.EnqueueSendEmail(request.Email, mail.Subject, mail.Body); + return mail; + } - return TypedResults.Ok(); + private EmailModel GetAcceptInviteEmail(ApplicationUser user) + { + var endpointUri = new Uri(Path.Combine($"{_apiConfig.WebAppUrl}", "accept-invite")); + string acceptInviteUrl = + QueryHelpers.AddQueryString(endpointUri.ToString(), "invitationToken", user.InvitationToken); + + var confirmEmailProps = new ConfirmEmailProps( + CdnUrl: _apiConfig.WebAppUrl, + FullName: user.DisplayName, + ConfirmUrl: acceptInviteUrl); + + var mail = emailFactory.GenerateConfirmAccountEmail(confirmEmailProps); + return mail; } } diff --git a/web/src/components/ui/password-input.tsx b/web/src/components/ui/password-input.tsx index 3b0ee3f91..d03952187 100644 --- a/web/src/components/ui/password-input.tsx +++ b/web/src/components/ui/password-input.tsx @@ -1,12 +1,13 @@ +import { EyeIcon, EyeOffIcon } from 'lucide-react'; +import * as React from 'react'; + import { Button } from '@/components/ui/button'; -import { Input, InputProps } from '@/components/ui/input'; +import { Input, type InputProps } from '@/components/ui/input'; import { cn } from '@/lib/utils'; -import { EyeIcon, EyeOffIcon } from 'lucide-react'; -import { forwardRef, useState } from 'react'; -const PasswordInput = forwardRef(({ className, ...props }, ref) => { - const [showPassword, setShowPassword] = useState(false); - const disabled = props['value'] === '' || props['value'] === undefined || props['disabled']; +const PasswordInput = React.forwardRef(({ className, ...props }, ref) => { + const [showPassword, setShowPassword] = React.useState(false); + const disabled = props.value === '' || props.value === undefined || props.disabled; return (
diff --git a/web/src/features/auth/AcceptInvite.tsx b/web/src/features/auth/AcceptInvite.tsx index ae69956a0..6e00b587f 100644 --- a/web/src/features/auth/AcceptInvite.tsx +++ b/web/src/features/auth/AcceptInvite.tsx @@ -12,6 +12,7 @@ import { Route as AcceptInviteRoute } from '@/routes/accept-invite/index'; import { useMutation } from '@tanstack/react-query'; import { noAuthApi } from '@/common/no-auth-api'; import { toast } from '@/components/ui/use-toast'; +import { PasswordInput } from '@/components/ui/password-input'; const formSchema = z .object({ @@ -39,7 +40,7 @@ function AcceptInvite() { const form = useForm>({ resolver: zodResolver(formSchema), - mode: 'all', + mode: 'onChange', defaultValues: { password: '', confirmPassword: '', @@ -99,7 +100,13 @@ function AcceptInvite() { Password - + @@ -113,7 +120,13 @@ function AcceptInvite() { Confirm password - + diff --git a/web/src/features/auth/ForgotPassword.tsx b/web/src/features/auth/ForgotPassword.tsx index ca723d84f..ecbe43031 100644 --- a/web/src/features/auth/ForgotPassword.tsx +++ b/web/src/features/auth/ForgotPassword.tsx @@ -27,7 +27,7 @@ interface ForgotPasswordRequest { function ForgotPassword() { const form = useForm>({ resolver: zodResolver(formSchema), - mode: 'all', + mode: 'onChange', defaultValues: { email: '', }, @@ -72,7 +72,7 @@ function ForgotPassword() { name='email' render={({ field }) => ( - Password + Email @@ -83,7 +83,7 @@ function ForgotPassword() { diff --git a/web/src/features/auth/Login.tsx b/web/src/features/auth/Login.tsx index b0e79b9ed..7998a21de 100644 --- a/web/src/features/auth/Login.tsx +++ b/web/src/features/auth/Login.tsx @@ -11,6 +11,7 @@ import { LoginDTO } from '@/common/auth-api'; import { useNavigate } from '@tanstack/react-router'; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; import Logo from '@/components/layout/Header/Logo'; +import { PasswordInput } from '@/components/ui/password-input'; const formSchema = z.object({ email: z @@ -27,7 +28,7 @@ function Login() { const navigate = useNavigate(); const form = useForm>({ resolver: zodResolver(formSchema), - mode: 'all', + mode: 'onChange', defaultValues: { email: '', password: '', @@ -77,7 +78,13 @@ function Login() { Password - + diff --git a/web/src/features/auth/ResetPassword.tsx b/web/src/features/auth/ResetPassword.tsx index b580115b3..7ff130a61 100644 --- a/web/src/features/auth/ResetPassword.tsx +++ b/web/src/features/auth/ResetPassword.tsx @@ -14,6 +14,7 @@ import { noAuthApi } from '@/common/no-auth-api'; import { toast } from '@/components/ui/use-toast'; import { useNavigate } from '@tanstack/react-router'; import type { FunctionComponent } from '@/common/types'; +import { PasswordInput } from '@/components/ui/password-input'; interface ResetPasswordRequest { password: string; @@ -48,7 +49,7 @@ function ResetPassword(): FunctionComponent { const form = useForm>({ resolver: zodResolver(formSchema), - mode: 'all', + mode: 'onChange', defaultValues: { email: '', password: '', @@ -113,7 +114,13 @@ function ResetPassword(): FunctionComponent { Password - + @@ -127,7 +134,13 @@ function ResetPassword(): FunctionComponent { Confirm your password - +