From fdfca7c75765f2d0059868bf9306c01dd4cc137e Mon Sep 17 00:00:00 2001 From: Jonathan Bourdon Date: Mon, 12 May 2025 15:45:12 -0400 Subject: [PATCH] feat(auth): adds local account authentication --- .../Features/Users/Handlers/ForgotPassword.cs | 66 +++++ .../src/Web/Features/Users/Handlers/Login.cs | 78 ++++++ .../Users/Handlers/LoginWithFacebook.cs | 1 - .../Users/Handlers/LoginWithGoogle.cs | 1 - .../Web/Features/Users/Handlers/Register.cs | 97 +++++++ .../Features/Users/Handlers/ResetPassword.cs | 55 ++++ .../Features/Users/Handlers/SetPassword.cs | 50 ++++ .../Features/Users/Services/IEmailSender.cs | 29 ++ .../src/Web/Features/Users/WebsiteOptions.cs | 10 + backend/src/Web/Program.cs | 4 + backend/src/Web/appsettings.Development.json | 9 + backend/src/Web/appsettings.Production.json | 3 + frontend/src/router/router.js | 24 +- frontend/src/stores/authStore.js | 26 +- frontend/src/views/ForgotPasswordView.vue | 201 ++++++++++++++ frontend/src/views/LoginView.vue | 179 +++++++++--- frontend/src/views/RegisterView.vue | 195 ++++++++++++++ frontend/src/views/ResetPasswordView.vue | 254 ++++++++++++++++++ frontend/src/views/profile/ProfilePage.vue | 15 ++ .../views/profile/account/AddressDialog.vue | 55 ---- .../views/profile/account/BirthdayDialog.vue | 55 ---- .../profile/account/ChangePasswordDialog.vue | 183 +++++++++++++ .../src/views/profile/account/PhoneDialog.vue | 55 ---- .../views/profile/account/PortraitDialog.vue | 80 ------ 24 files changed, 1446 insertions(+), 279 deletions(-) create mode 100644 backend/src/Web/Features/Users/Handlers/ForgotPassword.cs create mode 100644 backend/src/Web/Features/Users/Handlers/Login.cs create mode 100644 backend/src/Web/Features/Users/Handlers/Register.cs create mode 100644 backend/src/Web/Features/Users/Handlers/ResetPassword.cs create mode 100644 backend/src/Web/Features/Users/Handlers/SetPassword.cs create mode 100644 backend/src/Web/Features/Users/Services/IEmailSender.cs create mode 100644 backend/src/Web/Features/Users/WebsiteOptions.cs create mode 100644 frontend/src/views/ForgotPasswordView.vue create mode 100644 frontend/src/views/RegisterView.vue create mode 100644 frontend/src/views/ResetPasswordView.vue delete mode 100644 frontend/src/views/profile/account/AddressDialog.vue delete mode 100644 frontend/src/views/profile/account/BirthdayDialog.vue create mode 100644 frontend/src/views/profile/account/ChangePasswordDialog.vue delete mode 100644 frontend/src/views/profile/account/PhoneDialog.vue delete mode 100644 frontend/src/views/profile/account/PortraitDialog.vue diff --git a/backend/src/Web/Features/Users/Handlers/ForgotPassword.cs b/backend/src/Web/Features/Users/Handlers/ForgotPassword.cs new file mode 100644 index 0000000..d2dbcd4 --- /dev/null +++ b/backend/src/Web/Features/Users/Handlers/ForgotPassword.cs @@ -0,0 +1,66 @@ +using Hutopy.Web.Features.Users; +using Hutopy.Web.Features.Users.Data; +using Hutopy.Web.Features.Users.Services; +using Microsoft.Extensions.Options; +using System.Text; +using System.Web; + +namespace Hutopy.Web.Features.Users.Handlers; + +[PublicAPI] +public record ForgotPasswordRequest( + string Email); + +[PublicAPI] +public class ForgotPasswordHandler( + IdentityUserManager userManager, + IEmailSender emailSender, + ILogger logger, + IOptionsSnapshot options) + : Endpoint +{ + public override void Configure() + { + AllowAnonymous(); + Post("/api/users/forgot-password"); + Options(o => o.WithTags("Users")); + } + + public override async Task HandleAsync( + ForgotPasswordRequest request, + CancellationToken ct) + { + // Find user by email + var user = await userManager.FindByEmailAsync(request.Email); + + // Always return OK even if user not found to prevent email enumeration + if (user is null) + { + await SendOkAsync(ct); + return; + } + + // Generate password reset token + var token = await userManager.GeneratePasswordResetTokenAsync(user); + + // URL encode the token as it may contain characters that are not URL safe + var encodedToken = HttpUtility.UrlEncode(token); + + // Build reset link + var resetLink = $"{options.FrontendBaseUrl;}/reset-password?email={HttpUtility.UrlEncode(request.Email)}&token={encodedToken}"; + + // TODO: Write a better email template + var subject = "Reset Your Password"; + var message = new StringBuilder() + .AppendLine("

Reset Your Password

") + .AppendLine("

Please click the link below to reset your password:

") + .AppendLine($"

Reset Password

") + .AppendLine("

If you did not request a password reset, please ignore this email.

") + .ToString(); + + // Send email + await emailSender.SendEmailAsync(request.Email, subject, message); + + await SendOkAsync(ct); + } +} diff --git a/backend/src/Web/Features/Users/Handlers/Login.cs b/backend/src/Web/Features/Users/Handlers/Login.cs new file mode 100644 index 0000000..633ff6b --- /dev/null +++ b/backend/src/Web/Features/Users/Handlers/Login.cs @@ -0,0 +1,78 @@ +using Hutopy.Web.Common.Security; +using Hutopy.Web.Features.Users.Data; +using Microsoft.Extensions.Options; + +namespace Hutopy.Web.Features.Users.Handlers; + +[PublicAPI] +public record LoginRequest( + string Email, + string Password); + +[PublicAPI] +public record LoginResponse( + string AccessToken, + string RefreshToken); + +[PublicAPI] +public class LoginHandler( + IdentityUserManager userManager, + IOptionsSnapshot jwtOptions) + : Endpoint +{ + public override void Configure() + { + AllowAnonymous(); + Post("/api/users/login"); + Options(o => o.WithTags("Users")); + } + + public override async Task HandleAsync( + LoginRequest request, + CancellationToken ct) + { + // Find user by email + var user = await userManager.FindByEmailAsync(request.Email); + if (user is null) + { + await SendStringAsync( + "Invalid email or password", + 401, + cancellation: ct); + return; + } + + // Verify password + var isPasswordValid = await userManager.CheckPasswordAsync(user, request.Password); + if (!isPasswordValid) + { + await SendStringAsync( + "Invalid email or password", + 401, + cancellation: ct); + return; + } + + // Generate new refresh token + user.RefreshToken = RefreshTokenGenerator.Next(); + user.RefreshTokenExpiryTime = DateTime.UtcNow.Add(jwtOptions.Value.RefreshTokenLifetime); + await userManager.UpdateAsync(user); + + // Generate JWT token + var accessToken = JwtTokenHelper.GenerateJwtToken( + expiresIn: jwtOptions.Value.Lifetime, + issuer: jwtOptions.Value.Issuer, + audience: jwtOptions.Value.Audience, + key: jwtOptions.Value.Key, + userId: user.Id.ToString(), + email: user.Email ?? string.Empty, + alias: user.Alias, + firstname: user.Firstname ?? string.Empty, + lastname: user.Lastname ?? string.Empty, + portraitUrl: user.PortraitUrl); + + await SendOkAsync( + new LoginResponse(accessToken, user.RefreshToken), + cancellation: ct); + } +} diff --git a/backend/src/Web/Features/Users/Handlers/LoginWithFacebook.cs b/backend/src/Web/Features/Users/Handlers/LoginWithFacebook.cs index f518f7c..f0ebcd0 100644 --- a/backend/src/Web/Features/Users/Handlers/LoginWithFacebook.cs +++ b/backend/src/Web/Features/Users/Handlers/LoginWithFacebook.cs @@ -2,7 +2,6 @@ using System.Text.Json.Serialization; using Hutopy.Web.Common.Security; using Hutopy.Web.Features.Users.Data; -using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; using IdentityUser = Hutopy.Web.Features.Users.Data.IdentityUser; diff --git a/backend/src/Web/Features/Users/Handlers/LoginWithGoogle.cs b/backend/src/Web/Features/Users/Handlers/LoginWithGoogle.cs index 8680b57..a0c37e2 100644 --- a/backend/src/Web/Features/Users/Handlers/LoginWithGoogle.cs +++ b/backend/src/Web/Features/Users/Handlers/LoginWithGoogle.cs @@ -2,7 +2,6 @@ using System.Text.Json.Serialization; using Hutopy.Web.Common.Security; using Hutopy.Web.Features.Users.Data; -using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; using IdentityUser = Hutopy.Web.Features.Users.Data.IdentityUser; diff --git a/backend/src/Web/Features/Users/Handlers/Register.cs b/backend/src/Web/Features/Users/Handlers/Register.cs new file mode 100644 index 0000000..3039a73 --- /dev/null +++ b/backend/src/Web/Features/Users/Handlers/Register.cs @@ -0,0 +1,97 @@ +using Hutopy.Web.Common.Security; +using Hutopy.Web.Features.Users.Data; +using Microsoft.Extensions.Options; +using IdentityUser = Hutopy.Web.Features.Users.Data.IdentityUser; + +namespace Hutopy.Web.Features.Users.Handlers; + +[PublicAPI] +public record RegisterRequest( + string Email, + string Password, + string Name); + +[PublicAPI] +public record RegisterResponse( + string AccessToken, + string RefreshToken); + +[PublicAPI] +public class RegisterHandler( + IdentityUserManager userManager, + IOptionsSnapshot jwtOptions) + : Endpoint +{ + public override void Configure() + { + AllowAnonymous(); + Post("/api/users/register"); + Options(o => o.WithTags("Users")); + } + + public override async Task HandleAsync( + RegisterRequest request, + CancellationToken ct) + { + // Check if user already exists + var existingUser = await userManager.FindByEmailAsync(request.Email); + if (existingUser is not null) + { + await SendStringAsync( + "A user with this email already exists", + 400, + cancellation: ct); + return; + } + + // Create refresh token + var refreshToken = RefreshTokenGenerator.Next(); + + // Split name into firstname and lastname (if provided) + var nameParts = request.Name.Split(' ', 2); + var firstname = nameParts[0]; + var lastname = nameParts.Length > 1 ? nameParts[1] : string.Empty; + + // Create new user + var user = new IdentityUser + { + UserName = request.Email, + Email = request.Email, + Firstname = firstname, + Lastname = lastname, + Alias = request.Name, + RefreshToken = refreshToken, + RefreshTokenExpiryTime = DateTime.UtcNow.Add(jwtOptions.Value.RefreshTokenLifetime) + }; + + var result = await userManager.CreateAsync( + user, + request.Password); + + if (!result.Succeeded) + { + await SendStringAsync( + result.Errors.First().Description, + 400, + cancellation: ct); + return; + } + + // Generate JWT token + var accessToken = JwtTokenHelper.GenerateJwtToken( + expiresIn: jwtOptions.Value.Lifetime, + issuer: jwtOptions.Value.Issuer, + audience: jwtOptions.Value.Audience, + key: jwtOptions.Value.Key, + userId: user.Id.ToString(), + email: user.Email ?? string.Empty, + alias: user.Alias, + firstname: user.Firstname ?? string.Empty, + lastname: user.Lastname ?? string.Empty, + portraitUrl: user.PortraitUrl); + + await SendOkAsync( + new RegisterResponse(accessToken, user.RefreshToken), + cancellation: ct); + } +} diff --git a/backend/src/Web/Features/Users/Handlers/ResetPassword.cs b/backend/src/Web/Features/Users/Handlers/ResetPassword.cs new file mode 100644 index 0000000..b005eae --- /dev/null +++ b/backend/src/Web/Features/Users/Handlers/ResetPassword.cs @@ -0,0 +1,55 @@ +using Hutopy.Web.Features.Users.Data; + +namespace Hutopy.Web.Features.Users.Handlers; + +[PublicAPI] +public record ResetPasswordRequest( + string Email, + string Token, + string NewPassword); + +[PublicAPI] +public class ResetPasswordHandler( + IdentityUserManager userManager) + : Endpoint +{ + public override void Configure() + { + AllowAnonymous(); + Post("/api/users/reset-password"); + Options(o => o.WithTags("Users")); + } + + public override async Task HandleAsync( + ResetPasswordRequest request, + CancellationToken ct) + { + // Find user by email + var user = await userManager.FindByEmailAsync(request.Email); + if (user is null) + { + await SendStringAsync( + "Invalid request", + 400, + cancellation: ct); + return; + } + + // Reset password with token + var result = await userManager.ResetPasswordAsync( + user, + request.Token, + request.NewPassword); + + if (!result.Succeeded) + { + await SendStringAsync( + "Invalid or expired token", + 400, + cancellation: ct); + return; + } + + await SendOkAsync(ct); + } +} diff --git a/backend/src/Web/Features/Users/Handlers/SetPassword.cs b/backend/src/Web/Features/Users/Handlers/SetPassword.cs new file mode 100644 index 0000000..9b1f60e --- /dev/null +++ b/backend/src/Web/Features/Users/Handlers/SetPassword.cs @@ -0,0 +1,50 @@ +using Hutopy.Web.Common.Security; +using Hutopy.Web.Features.Users.Data; + +namespace Hutopy.Web.Features.Users.Handlers; + +[PublicAPI] +public record SetPasswordRequest( + string NewPassword); + +[PublicAPI] +public class SetPasswordHandler( + IdentityUserManager userManager) + : Endpoint +{ + public override void Configure() + { + Post("/api/users/set-password"); + Options(o => o.WithTags("Users")); + } + + public override async Task HandleAsync( + SetPasswordRequest request, + CancellationToken ct) + { + // Get current user id from claims + var userId = User.GetUserId().ToString(); + + // Get user from database + var user = await userManager.FindByIdAsync(userId); + if (user is null) + { + await SendForbiddenAsync(ct); + return; + } + + var resetToken = await userManager.GeneratePasswordResetTokenAsync(user); + var result = await userManager.ResetPasswordAsync(user, resetToken, request.NewPassword); + + if (!result.Succeeded) + { + await SendStringAsync( + result.Errors.First().Description, + 400, + cancellation: ct); + return; + } + + await SendOkAsync(ct); + } +} diff --git a/backend/src/Web/Features/Users/Services/IEmailSender.cs b/backend/src/Web/Features/Users/Services/IEmailSender.cs new file mode 100644 index 0000000..56151b8 --- /dev/null +++ b/backend/src/Web/Features/Users/Services/IEmailSender.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; + +namespace Hutopy.Web.Features.Users.Services; + +public interface IEmailSender +{ + Task SendEmailAsync(string email, string subject, string message); +} + +public class EmailSender(ILogger logger) + : IEmailSender +{ + + public async Task SendEmailAsync(string email, string subject, string message) + { + try + { + logger.LogInformation("Sending email to {Email} with subject: {Subject}", email, subject); + // TODO: Implement actual email sending logic + await Task.Delay(1000); + logger.LogInformation("Email sent successfully to {Email}", email); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to send email to {Email}", email); + throw; + } + } +} diff --git a/backend/src/Web/Features/Users/WebsiteOptions.cs b/backend/src/Web/Features/Users/WebsiteOptions.cs new file mode 100644 index 0000000..5d491e8 --- /dev/null +++ b/backend/src/Web/Features/Users/WebsiteOptions.cs @@ -0,0 +1,10 @@ +using System; + +namespace Hutopy.Web.Features.Users; + +public class WebsiteOptions +{ + public const string SectionName = "Website"; + + public string FrontendBaseUrl { get; set; } = "https://localhost:5173"; +} diff --git a/backend/src/Web/Program.cs b/backend/src/Web/Program.cs index 563f11d..91d42bb 100644 --- a/backend/src/Web/Program.cs +++ b/backend/src/Web/Program.cs @@ -8,6 +8,7 @@ using Hutopy.Web.Features.Messages; using Hutopy.Web.Features.Messages.Data; using Hutopy.Web.Features.Users; using Hutopy.Web.Features.Users.Data; +using Hutopy.Web.Features.Users.Services; using Microsoft.AspNetCore.HttpOverrides; using NSwag; using NSwag.Generation.AspNetCore.Processors; @@ -36,6 +37,8 @@ builder.Services.AddCors(options => }); }); +builder.Services.AddTransient(); + // Add services to the container. builder.Services.AddWebServices(); builder.Services.AddAuthorizationAndAuthentication(builder.Configuration); @@ -84,6 +87,7 @@ builder.AddMembershipModule( o => o.MigrationsHistoryTable("__EFMigrationsHistory", MembershipDbContext.SchemaName))); builder.Services.Configure(builder.Configuration.GetRequiredSection(JwtOptions.SectionName)); +builder.Services.Configure(builder.Configuration.GetRequiredSection(WebsiteOptions.SectionName)); var app = builder.Build(); diff --git a/backend/src/Web/appsettings.Development.json b/backend/src/Web/appsettings.Development.json index fee981d..33a0982 100644 --- a/backend/src/Web/appsettings.Development.json +++ b/backend/src/Web/appsettings.Development.json @@ -6,5 +6,14 @@ "SecretKey": "sk_test_51OoveVDrRyqXtNdBaOs1DFFja0XhrQtJoAo83uSySMuqw4Wyt9NsuugrIHRqet9a50cr5GvolpTP8EZuTSttcgYx00gOUPNDoI", "WebhookSecret": "whsec_cee07ef14cf784850cab63567048b5326fec7fd29c03f4659476524f8299aff1", "HutopyRate": 0.05 + }, + "Website": { + "FrontendBaseUrl": "https://localhost:5173" + }, + "Authentication": { + "Jwt": { + "Lifetime": "00:05:00", + "RefreshTokenLifetime": "0.00:30:00" + } } } diff --git a/backend/src/Web/appsettings.Production.json b/backend/src/Web/appsettings.Production.json index 92f848f..1495d88 100644 --- a/backend/src/Web/appsettings.Production.json +++ b/backend/src/Web/appsettings.Production.json @@ -4,5 +4,8 @@ }, "Stripe": { "HutopyRate": 0.05 + }, + "Website": { + "FrontendBaseUrl": "https://hutopy.ca" } } \ No newline at end of file diff --git a/frontend/src/router/router.js b/frontend/src/router/router.js index 8b242fe..cf73b4d 100644 --- a/frontend/src/router/router.js +++ b/frontend/src/router/router.js @@ -16,6 +16,9 @@ import LoginView from '../views/LoginView.vue'; import PaymentCompleted from '../views/PaymentCompleted.vue'; import Landing from '../views/main/Landing.vue'; import CreateCreator from "@/views/creators/CreateCreator.vue"; +import RegisterView from "@/views/RegisterView.vue"; +import ForgotPasswordView from "@/views/ForgotPasswordView.vue"; +import ResetPasswordView from "@/views/ResetPasswordView.vue"; const routes = [ { @@ -108,6 +111,25 @@ const routes = [ component: CreateCreator, meta: { requiresAuth: true }, }, + { + path: '/register', + name: 'register', + component: RegisterView, + meta: { requiresAuth: false } + }, + { + path: '/forgot-password', + name: 'forgot-password', + component: ForgotPasswordView, + meta: { notAuthenticated: true } + }, + { + path: '/reset-password', + name: 'reset-password', + component: ResetPasswordView, + meta: { notAuthenticated: true }, + props: (route) => ({ email: route.query.email, token: route.query.token }) + } ]; const router = createRouter({ @@ -136,4 +158,4 @@ router.beforeEach((to, from, next) => { } }); -export default router; +export default router; \ No newline at end of file diff --git a/frontend/src/stores/authStore.js b/frontend/src/stores/authStore.js index 6204e0c..3841156 100644 --- a/frontend/src/stores/authStore.js +++ b/frontend/src/stores/authStore.js @@ -244,6 +244,29 @@ export const useAuthStore = defineStore('auth', () => { return isExpiring; } + async function changePassword(newPassword) { + console.log('changePassword called'); + if (!isAuthenticated.value) { + throw new Error('User must be authenticated to change password'); + } + + if (!newPassword) { + throw new Error('New password is required'); + } + + try { + const response = await clientApi.post('api/users/set-password', { + newPassword + }); + + console.log('Password changed successfully'); + return true; + } catch (error) { + console.error('Password change failed:', error); + throw error; + } + } + return { accessToken, refreshToken, @@ -255,6 +278,7 @@ export const useAuthStore = defineStore('auth', () => { loginWithFacebook, logout, refresh, - isTokenExpiringSoon + isTokenExpiringSoon, + changePassword }; }); \ No newline at end of file diff --git a/frontend/src/views/ForgotPasswordView.vue b/frontend/src/views/ForgotPasswordView.vue new file mode 100644 index 0000000..755de99 --- /dev/null +++ b/frontend/src/views/ForgotPasswordView.vue @@ -0,0 +1,201 @@ + + + + + + + +{ + "en": { + "title": "Forgot Password", + "description": "Enter your email address and we'll send you a link to reset your password.", + "email": "Email", + "resetPassword": "Reset Password", + "backToLogin": "Back to Login", + "resetEmailSent": "Password reset email sent. Please check your inbox.", + "resetRequestFailed": "Failed to request password reset. Please try again.", + "emailRequired": "Email is required." + }, + "fr": { + "title": "Mot de passe oublié", + "description": "Entrez votre adresse e-mail et nous vous enverrons un lien pour réinitialiser votre mot de passe.", + "email": "Email", + "resetPassword": "Réinitialiser le mot de passe", + "backToLogin": "Retour à la connexion", + "resetEmailSent": "Email de réinitialisation du mot de passe envoyé. Veuillez vérifier votre boîte de réception.", + "resetRequestFailed": "Échec de la demande de réinitialisation du mot de passe. Veuillez réessayer.", + "emailRequired": "L'email est requis." + }, + "es": { + "title": "Olvidé mi contraseña", + "description": "Ingrese su dirección de correo electrónico y le enviaremos un enlace para restablecer su contraseña.", + "email": "Correo electrónico", + "resetPassword": "Restablecer contraseña", + "backToLogin": "Volver al inicio de sesión", + "resetEmailSent": "Correo electrónico de restablecimiento de contraseña enviado. Por favor revise su bandeja de entrada.", + "resetRequestFailed": "No se pudo solicitar el restablecimiento de contraseña. Por favor, inténtelo de nuevo.", + "emailRequired": "El correo electrónico es obligatorio." + } +} + diff --git a/frontend/src/views/LoginView.vue b/frontend/src/views/LoginView.vue index 491ec54..6439600 100644 --- a/frontend/src/views/LoginView.vue +++ b/frontend/src/views/LoginView.vue @@ -1,3 +1,83 @@ + + - - { "en": { - "title": "Sign in to Hutopy", - "alt": "Hutopy Login" + "title": "Sign in", + "alt": "Login", + "email": "Email", + "password": "Password", + "signIn": "Connect", + "forgotPassword": "Forgot password?", + "orContinueWith": "Or", + "noAccount": "Don't have an account?", + "register": "Register", + "loginFailed": "Login failed. Please check your credentials.", + "continueWithGoogle": "Continue with Google" }, "fr": { - "title": "Connectez-vous à Hutopy", - "alt": "Connexion Hutopy" + "title": "Se connecter", + "alt": "Connexion", + "email": "Email", + "password": "Mot de passe", + "signIn": "Connexion", + "forgotPassword": "Mot de passe oublié?", + "orContinueWith": "Ou", + "noAccount": "Vous n'avez pas de compte?", + "register": "S'inscrire", + "loginFailed": "Échec de la connexion. Veuillez vérifier vos identifiants.", + "continueWithGoogle": "Continuer avec Google" }, "es": { - "title": "Iniciar sesión en Hutopy", - "alt": "Inicio de sesión Hutopy" + "title": "Iniciar sesión", + "alt": "Inicio de sesión", + "email": "Correo electrónico", + "password": "Contraseña", + "signIn": "Conéctate", + "forgotPassword": "¿Olvidó su contraseña?", + "orContinueWith": "o", + "noAccount": "¿No tiene una cuenta?", + "register": "Registrarse", + "loginFailed": "Error de inicio de sesión. Por favor, compruebe sus credenciales.", + "continueWithGoogle": "Continuar con Google" } } - + \ No newline at end of file diff --git a/frontend/src/views/RegisterView.vue b/frontend/src/views/RegisterView.vue new file mode 100644 index 0000000..608b74f --- /dev/null +++ b/frontend/src/views/RegisterView.vue @@ -0,0 +1,195 @@ + + + + + + + +{ + "en": { + "title": "Create your account", + "alt": "Hutopy Registration", + "name": "Full Name", + "email": "Email", + "password": "Password", + "confirmPassword": "Confirm Password", + "passwordRequirements": "Password must be at least 8 characters", + "register": "Register", + "alreadyHaveAccount": "Already have an account?", + "signIn": "Sign in", + "passwordsDoNotMatch": "Passwords do not match", + "registrationFailed": "Registration failed. Please try again." + }, + "fr": { + "title": "Créer votre compte", + "alt": "Inscription Hutopy", + "name": "Nom complet", + "email": "Email", + "password": "Mot de passe", + "confirmPassword": "Confirmer le mot de passe", + "passwordRequirements": "Le mot de passe doit comporter au moins 8 caractères", + "register": "S'inscrire", + "alreadyHaveAccount": "Vous avez déjà un compte?", + "signIn": "Se connecter", + "passwordsDoNotMatch": "Les mots de passe ne correspondent pas", + "registrationFailed": "L'inscription a échoué. Veuillez réessayer." + }, + "es": { + "title": "Crea tu cuenta", + "alt": "Registro de Hutopy", + "name": "Nombre completo", + "email": "Correo electrónico", + "password": "Contraseña", + "confirmPassword": "Confirmar contraseña", + "passwordRequirements": "La contraseña debe tener al menos 8 caracteres", + "register": "Registrarse", + "alreadyHaveAccount": "¿Ya tienes una cuenta?", + "signIn": "Iniciar sesión", + "passwordsDoNotMatch": "Las contraseñas no coinciden", + "registrationFailed": "El registro falló. Por favor, inténtelo de nuevo." + } +} + \ No newline at end of file diff --git a/frontend/src/views/ResetPasswordView.vue b/frontend/src/views/ResetPasswordView.vue new file mode 100644 index 0000000..1f8b371 --- /dev/null +++ b/frontend/src/views/ResetPasswordView.vue @@ -0,0 +1,254 @@ + + + + + + + +{ + "en": { + "title": "Reset Your Password", + "newPassword": "New Password", + "confirmPassword": "Confirm Password", + "passwordRequirements": "Password must be at least 8 characters", + "resetPassword": "Reset Password", + "passwordResetSuccess": "Your password has been reset successfully!", + "proceedToLogin": "Proceed to Login", + "passwordsDoNotMatch": "Passwords do not match", + "passwordTooShort": "Password must be at least 8 characters long", + "resetFailed": "Password reset failed. Please try again or request a new reset link.", + "invalidResetLink": "Invalid or expired reset link. Please request a new password reset." + }, + "fr": { + "title": "Réinitialiser Votre Mot de Passe", + "newPassword": "Nouveau Mot de Passe", + "confirmPassword": "Confirmer le Mot de Passe", + "passwordRequirements": "Le mot de passe doit comporter au moins 8 caractères", + "resetPassword": "Réinitialiser le Mot de Passe", + "passwordResetSuccess": "Votre mot de passe a été réinitialisé avec succès!", + "proceedToLogin": "Procéder à la Connexion", + "passwordsDoNotMatch": "Les mots de passe ne correspondent pas", + "passwordTooShort": "Le mot de passe doit comporter au moins 8 caractères", + "resetFailed": "Échec de la réinitialisation du mot de passe. Veuillez réessayer ou demander un nouveau lien de réinitialisation.", + "invalidResetLink": "Lien de réinitialisation invalide ou expiré. Veuillez demander une nouvelle réinitialisation de mot de passe." + }, + "es": { + "title": "Restablecer su Contraseña", + "newPassword": "Nueva Contraseña", + "confirmPassword": "Confirmar Contraseña", + "passwordRequirements": "La contraseña debe tener al menos 8 caracteres", + "resetPassword": "Restablecer Contraseña", + "passwordResetSuccess": "¡Su contraseña ha sido restablecida con éxito!", + "proceedToLogin": "Proceder al Inicio de Sesión", + "passwordsDoNotMatch": "Las contraseñas no coinciden", + "passwordTooShort": "La contraseña debe tener al menos 8 caracteres", + "resetFailed": "Error al restablecer la contraseña. Inténtelo de nuevo o solicite un nuevo enlace de restablecimiento.", + "invalidResetLink": "Enlace de restablecimiento inválido o caducado. Solicite un nuevo restablecimiento de contraseña." + } +} + diff --git a/frontend/src/views/profile/ProfilePage.vue b/frontend/src/views/profile/ProfilePage.vue index 473a023..ed31c3d 100644 --- a/frontend/src/views/profile/ProfilePage.vue +++ b/frontend/src/views/profile/ProfilePage.vue @@ -3,10 +3,12 @@ import {ref, markRaw} from 'vue'; import {useCreatorProfileStore} from '@/stores/creatorProfileStore.js'; import {useUserProfileStore} from "@/stores/userProfileStore.js"; import {useClient} from '@/plugins/api.js'; +import {useRouter} from 'vue-router'; import SocialsDialog from './creators/SocialsDialog.vue'; import AliasDialog from "@/views/profile/account/AliasDialog.vue"; import FullnameDialog from "@/views/profile/account/FullnameDialog.vue"; import EmailDialog from "@/views/profile/account/EmailDialog.vue"; +import ChangePasswordDialog from "@/views/profile/account/ChangePasswordDialog.vue"; import ChangeStripeIdDialog from '@/views/profile/creators/ChangeStripeIdDialog.vue'; import ChangeNameDialog from '@/views/profile/creators/ChangeNameDialog.vue'; import ChangeSlugDialog from '@/views/profile/creators/ChangeSlugDialog.vue'; @@ -25,6 +27,7 @@ import {useI18n} from 'vue-i18n'; import QRCodeVue from 'qrcode.vue'; const {t} = useI18n(); +const router = useRouter(); const userProfileStore = useUserProfileStore() const creatorProfileStore = useCreatorProfileStore(); const baseURL = window.location.origin; @@ -102,6 +105,7 @@ const deleteDialogShown = ref(false); const componentsMap = { EmailDialog: markRaw(EmailDialog), + ChangePasswordDialog: markRaw(ChangePasswordDialog), SocialsDialog: markRaw(SocialsDialog), ChangeSlugDialog: markRaw(ChangeSlugDialog), ChangeNameDialog: markRaw(ChangeNameDialog), @@ -251,6 +255,14 @@ async function deconfigureStripe() { +
+ +
+