transactions

This commit is contained in:
2025-08-24 14:18:20 +02:00
parent 1b2d95344a
commit 5777e351bf
107 changed files with 4940 additions and 1266 deletions

View File

@@ -22,11 +22,7 @@ export class CryptoService {
private async fetchPublicKey(): Promise<CryptoKey> {
try {
const response = await axios.get(`${this.cryptoUrl}`, {
headers: {
'X-Thumbprint': this.thumbprintService.getThumbprint(),
},
});
const response = await axios.get(`${this.cryptoUrl}`);
if (response.status !== 200) {
throw new Error('Failed to fetch public key');
@@ -39,7 +35,7 @@ export class CryptoService {
binaryDer,
{
name: 'RSA-OAEP',
hash: { name: 'SHA-256' },
hash: 'SHA-256',
},
false,
['encrypt']

View File

@@ -22,6 +22,28 @@ export class userService {
return this.userSubject.asObservable();
}
async systemLogin(
email: string | null | undefined,
password: string | null | undefined,
systemKey: string | null | undefined
) {
if (email == null || password == null || systemKey == null) {
return;
}
const encryptedPassword = this.cryptoService.encryptData(password);
const encryptedSystemKey = this.cryptoService.encryptData(systemKey);
const response = await axios.post('/users/login', {
email,
password: await encryptedPassword,
systemKey: await encryptedSystemKey,
});
const { jwt, refresh, usermodel } = response.data;
localStorage.setItem('jwt', jwt);
localStorage.setItem('refresh', refresh);
this.setUser(usermodel);
return usermodel;
}
async login(
email: string | null | undefined,
password: string | null | undefined
@@ -40,4 +62,25 @@ export class userService {
this.setUser(usermodel);
return usermodel;
}
async register(
name: string | null | undefined,
email: string | null | undefined,
password: string | null | undefined
): Promise<userModel> {
if (email == null || password == null) {
throw new Error('Email and password must not be null');
}
const encrypted = this.cryptoService.encryptData(password);
const response = await axios.post('/users/register', {
name,
email,
password: await encrypted,
});
const { jwt, refresh, usermodel } = response.data;
localStorage.setItem('jwt', jwt);
localStorage.setItem('refresh', refresh);
this.setUser(usermodel);
return usermodel;
}
}

View File

@@ -1 +1,29 @@
<p>forgot-password-view works!</p>
<div class="forgot-password-form-container">
<h2>Recuperar contraseña</h2>
@if(showingForm()) {
<form class="forgot-password-form" [formGroup]="form" (ngSubmit)="onSubmit()">
<div class="text-input effect-20" [class.has-content]="hasEmailContent()">
<input id="email" type="email" formControlName="email" />
<label for="email">Email</label>
<span class="focus-border">
<i></i>
</span>
</div>
<div class="form-buttons">
<svg-button
label="send-reset-link"
type="submit"
text="Enviar enlace de restablecimiento"
[disabled]="!form.valid"
></svg-button>
</div>
</form>
} @else{
<div class="success-message">
<p>
Si existe un usuario con ese correo electrónico, se enviará un enlace para
iniciar sesión y restablecer la contraseña.
</p>
</div>
}
</div>

View File

@@ -0,0 +1,18 @@
.forgot-password-form-container {
max-width: 400px;
margin: 0 auto;
padding: 20px;
border: 1px solid #ccc;
border-radius: 5px;
background-color: #fff;
}
.forgot-password-form {
display: flex;
flex-direction: column;
}
.form-buttons {
display: flex;
justify-content: flex-end;
}

View File

@@ -1,11 +1,37 @@
import { Component } from '@angular/core';
import { Component, inject, signal } from '@angular/core';
import { SvgButton } from '../../../utils/svg-button/svg-button';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { emailValidator } from '../../../utils/validators/emailValidator';
import axios from 'axios';
@Component({
selector: 'forgot-password-view',
imports: [],
imports: [SvgButton, ReactiveFormsModule],
templateUrl: './forgot-password-view.html',
styleUrl: './forgot-password-view.scss'
styleUrl: './forgot-password-view.scss',
})
export class ForgotPasswordView {
private formBuilder = inject(FormBuilder);
form = this.formBuilder.group({
email: ['', [Validators.required, emailValidator]],
});
showingForm = signal(true);
get email() {
return this.form.get('email');
}
hasEmailContent(): boolean {
const emailValue = this.email?.value;
return emailValue ? emailValue.trim().length > 0 : false;
}
async onSubmit() {
if (this.form.valid) {
this.showingForm.set(false);
await axios.post('/users/forgot-password', {
email: this.email?.value,
});
}
}
}

View File

@@ -6,7 +6,11 @@
<h2>Acceder</h2>
@if(submitError()) {
<div class="error-message">{{ submitError() }}</div>
}
@if(showRegisterLink()) {
<div class="register-link">
<a href="/register">Pincha aquí para registrarte</a>
</div>
} }
<form class="login-form" [formGroup]="loginForm" (ngSubmit)="onSubmit()">
<div
id="login-view-email-input"
@@ -53,22 +57,36 @@
</span>
} }
</div>
} @if(isLoginSystem()) {
<div
id="login-view-system-key-input"
class="text-input effect-20"
[class.has-content]="hasSystemKeyContent()"
>
<input id="system-key" type="password" formControlName="systemKey" />
<label for="system-key">System Key</label>
<span class="focus-border">
<i></i>
</span>
</div>
}
<svg-button
label="login"
type="submit"
text="Entrar"
ngClass="login-button"
[disabled]="!loginForm.valid || disableLogin()"
></svg-button>
<svg-button
label="forgot-password"
type="button"
text="¿Contraseña olvidada?"
ngClass="forgot-password-button"
[disabled]="!loginForm.valid"
(click)="onForgotPassword()"
></svg-button>
<div class="login-buttons">
<svg-button
label="login"
type="submit"
text="Entrar"
ngClass="login-button"
[disabled]="!loginForm.valid || disableLogin()"
></svg-button>
<svg-button
label="forgot-password"
type="button"
text="¿Contraseña olvidada?"
ngClass="forgot-password-button"
[disabled]="!loginForm.valid"
(click)="onForgotPassword()"
></svg-button>
</div>
</form>
<div class="provider-separator">
<span class="line"></span>
@@ -108,6 +126,7 @@
text="Más opciones de inicio de sesión"
(click)="onMoreLoginOptions()"
></svg-button>
@if(!showRegisterLink()) {
<svg-button
label="register-link"
type="button"
@@ -115,6 +134,7 @@
ngClass="register-link-button"
(click)="onRegister()"
></svg-button>
}
</div>
}
</div>

View File

@@ -7,14 +7,14 @@
align-items: center;
justify-content: center;
width: 20%;
padding-top: 5rem;
padding-top: 2rem;
margin: 0 auto;
gap: 1.5rem;
.login-form {
display: flex;
flex-direction: column;
gap: 1rem;
gap: 1.5rem;
width: 100%;
}
@@ -46,6 +46,13 @@
}
}
.login-buttons {
display: flex;
flex-direction: column;
gap: 1rem;
width: 100%;
}
.provider-buttons {
display: grid;
grid-template-columns: 1fr 1fr;

View File

@@ -6,7 +6,7 @@ import { Router } from '@angular/router';
import { emailValidator } from '../../../utils/validators/emailValidator';
import { PasswordValidator } from '../../../utils/validators/passwordValidator';
import { emailPasswordDistinctValidator } from '../../../utils/validators/distinctEmailPasswordValidator';
import { from, single } from 'rxjs';
import { from } from 'rxjs';
@Component({
selector: 'login-view',
@@ -19,18 +19,20 @@ export class LoginView {
private formBuilder = inject(FormBuilder);
private router = inject(Router);
private timer: any;
private cuentaAtras: number = 30;
private disableLogin = signal(false);
private entrando = signal(false);
private submitError = signal<string | null>(null);
cuentaAtras: number = 30;
disableLogin = signal(false);
entrando = signal(false);
submitError = signal<string | null>(null);
showRegisterLink = signal(false);
private loginForm = this.formBuilder.group(
loginForm = this.formBuilder.group(
{
email: ['', [Validators.required, emailValidator]],
password: [
'',
[Validators.required, Validators.minLength(8), PasswordValidator],
],
systemKey: [''],
},
{ validators: emailPasswordDistinctValidator }
);
@@ -43,6 +45,10 @@ export class LoginView {
return this.loginForm.get('password');
}
get systemKey() {
return this.loginForm.get('systemKey');
}
// Métodos para verificar si los campos tienen contenido
hasEmailContent(): boolean {
const emailValue = this.email?.value;
@@ -72,6 +78,19 @@ export class LoginView {
: false;
}
isLoginSystem(): boolean {
const emailValue = this.email?.value;
if (emailValue && emailValue == '@system') {
return true;
}
return false;
}
hasSystemKeyContent(): boolean {
const systemKeyValue = this.systemKey?.value;
return systemKeyValue ? systemKeyValue.trim().length > 0 : false;
}
ngOnDestroy() {
this.submitError.set(null);
this.limpiarTimer();
@@ -101,10 +120,20 @@ export class LoginView {
}
onSubmit() {
if (this.loginForm.valid) {
const email = this.loginForm.value.email;
const password = this.loginForm.value.password;
if (this.isLoginSystem()) {
const systemKey = this.systemKey?.value;
from(this.userService.systemLogin(email, password, systemKey)).subscribe({
next: (user) => {
this.router.navigate(['/']);
},
error: (error) => {
this.router.navigate(['/']);
},
});
} else if (this.loginForm.valid) {
this.entrando.set(true);
const email = this.loginForm.value.email;
const password = this.loginForm.value.password;
from(this.userService.login(email, password)).subscribe({
next: (user) => {
this.router.navigate(['/']);
@@ -131,9 +160,8 @@ export class LoginView {
'No se ha podido conectar con el servidor. Vuelva a intentarlo más tarde. Reconectando...'
);
} else if (error.status === 404) {
this.submitError.set(
'El usuario nunca ha existido. ¿Quieres registrarte?'
);
this.submitError.set('El usuario nunca ha existido.');
this.showRegisterLink.set(true);
}
},
});

View File

@@ -1 +1,83 @@
<p>register-view works!</p>
<div class="register-view">
<form (ngSubmit)="onSubmit()" [formGroup]="form">
<div
id="login-view-email-input"
class="text-input effect-20"
[class.has-content]="hasNameContent()"
>
<input id="name" type="text" formControlName="name" />
<label for="name">Name</label>
<span class="focus-border">
<i></i>
</span>
</div>
<div
id="login-view-email-input"
class="text-input effect-20"
[class.has-content]="hasEmailContent()"
>
<input id="email" type="email" formControlName="email" />
<label for="email">Email</label>
<span class="focus-border">
<i></i>
</span>
</div>
@if(emailHasErrors()) {
<div>
<span>* El email no es válido. </span>
@if(this.email?.hasError('email')) {
<span>El email no tiene un formato válido.</span>
}
</div>
}
<div
id="login-view-password-input"
class="text-input effect-20"
[class.has-content]="hasPasswordContent()"
>
<input id="password" type="password" formControlName="password" />
<label for="password">Password</label>
<span class="focus-border">
<i></i>
</span>
</div>
@if(passwordHasErrors()) {
<div>
<span>* La contraseña no es válida. <br /></span>
@if(this.password?.hasError('minlength')) {
<span> Debe tener al menos 8 caracteres. </span>
} @else { @if(this.password?.hasError('passwordContainsEmailOrLeet')) {
<span> No puede contener trazas del email. </span>
} @if(this.password?.hasError('invalidPassword')) {
<span>
No tiene un formato válido. <br />
Tiene que contener al menos una letra mayúscula, una letra minúscula, un
número y un carácter especial.
</span>
} }
</div>
}
<div
id="login-view-password-input"
class="text-input effect-20"
[class.has-content]="hasConfirmPasswordContent()"
>
<input
id="confirm-password"
type="password"
formControlName="confirmPassword"
/>
<label for="confirm-password">Confirm Password</label>
<span class="focus-border">
<i></i>
</span>
</div>
@if(confirmPasswordHasErrors()) {
<div>
<span>* Las dos contraseñas tienen que coincidir.</span>
</div>
}
<button type="submit">Submit</button>
</form>
</div>

View File

@@ -0,0 +1,67 @@
.register-view {
form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.text-input {
position: relative;
input {
width: 100%;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
&:focus {
border-color: #007bff;
outline: none;
}
}
label {
position: absolute;
top: 0.5rem;
left: 0.5rem;
transition: 0.2s;
}
&.has-content {
label {
top: -1rem;
left: 0.5rem;
font-size: 0.8rem;
color: #007bff;
}
}
.focus-border {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background: #007bff;
transform: scaleX(0);
transition: transform 0.2s;
}
&:focus-within .focus-border {
transform: scaleX(1);
}
}
button[type="submit"] {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
background: #007bff;
color: white;
cursor: pointer;
&:hover {
background: darken(#007bff, 10%);
}
}
}

View File

@@ -1,11 +1,139 @@
import { Component } from '@angular/core';
import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { emailValidator } from '../../../utils/validators/emailValidator';
import { SigningMethods, userModel } from '../../../models/userModel';
import { emailPasswordDistinctValidator } from '../../../utils/validators/distinctEmailPasswordValidator';
import { from } from 'rxjs';
import { Router } from '@angular/router';
import { userService } from '../../services/userService/userService';
@Component({
selector: 'register-view',
imports: [],
imports: [ReactiveFormsModule],
templateUrl: './register-view.html',
styleUrl: './register-view.scss'
styleUrl: './register-view.scss',
})
export class RegisterView {
private userService = inject(userService);
private formBuilder = inject(FormBuilder);
private router = inject(Router);
form = this.formBuilder.group(
{
name: ['', [Validators.required]],
email: ['', [Validators.required, emailValidator]],
password: ['', [Validators.required, Validators.minLength(6)]],
confirmPassword: [''],
// preferredSigninMethod: [SigningMethods.Password],
// profilePicture: [null],
// bio: ['', [Validators.maxLength(500)]],
// socialMedia: this.formBuilder.group({
// facebook: [''],
// twitter: [''],
// instagram: [''],
// }),
// termsAccepted: [false, [Validators.requiredTrue]],
},
{ validators: emailPasswordDistinctValidator }
);
get name() {
return this.form.get('name');
}
get email() {
return this.form.get('email');
}
get password() {
return this.form.get('password');
}
get confirmPassword() {
return this.form.get('confirmPassword');
}
get bio() {
return this.form.get('bio');
}
get socialMedia() {
return this.form.get('socialMedia');
}
get profilePicture() {
return this.form.get('profilePicture');
}
get preferredSigninMethod() {
return this.form.get('preferredSigninMethod');
}
// getTermsAccepted() {
// return this.form.value.termsAccepted;
// }
onSubmit() {
if (this.form.valid) {
const email = this.form.value.email;
const password = this.form.value.password;
const name = this.form.value.name;
from(this.userService.register(name, email, password)).subscribe({
next: (user) => {
this.router.navigate(['/']);
},
error: (error) => {
console.error('Register error:', error);
},
});
}
}
hasNameContent(): boolean {
const nameValue = this.name?.value;
return nameValue ? nameValue.trim().length > 0 : false;
}
hasEmailContent(): boolean {
const emailValue = this.email?.value;
return emailValue ? emailValue.trim().length > 0 : false;
}
hasPasswordContent(): boolean {
const passwordValue = this.password?.value;
return passwordValue ? passwordValue.trim().length > 0 : false;
}
hasConfirmPasswordContent(): boolean {
const confirmPasswordValue = this.confirmPassword?.value;
return confirmPasswordValue
? confirmPasswordValue.trim().length > 0
: false;
}
emailHasErrors(): boolean {
const emailControl = this.email;
return emailControl
? this.hasEmailContent() &&
emailControl.invalid &&
(emailControl.dirty || emailControl.touched)
: false;
}
passwordHasErrors(): boolean {
const passwordControl = this.password;
return passwordControl
? this.hasPasswordContent() &&
passwordControl.invalid &&
(passwordControl.dirty || passwordControl.touched)
: false;
}
confirmPasswordHasErrors(): boolean {
const confirmPasswordControl = this.confirmPassword;
return confirmPasswordControl
? this.hasConfirmPasswordContent() &&
confirmPasswordControl.invalid &&
(confirmPasswordControl.dirty || confirmPasswordControl.touched)
: false;
}
}

View File

@@ -17,6 +17,7 @@ type personModelType = {
profilePicture?: string | null;
avatar?: string | null;
socialMedia?: socialMediaType | null;
bio?: string | null;
};
export class personModel {
@@ -26,18 +27,21 @@ export class personModel {
profilePicture,
avatar,
socialMedia,
bio,
}: personModelType) {
this.id = id;
this.name = name;
this.profilePicture = profilePicture || null;
this.avatar = avatar || null;
this.socialMedia = socialMedia || null;
this.bio = bio || null;
}
public id: string;
public name: string;
public profilePicture: string | null = null;
public avatar: string | null = null;
public bio: string | null = null;
public socialMedia: {
facebook: string | null;
instagram: string | null;

View File

@@ -1,7 +1,7 @@
import { personModel } from './personModel';
import { roleModel } from './roleModel';
export enum SigningMethods {
enum SigningMethods {
Password = 'password',
MagicLink = 'magic-link',
Passkeys = 'passkeys',
@@ -11,7 +11,7 @@ export enum SigningMethods {
Microsoft = 'microsoft',
}
export class userModel extends personModel {
class userModel extends personModel {
constructor(
public override id: string,
public email: string,
@@ -66,3 +66,5 @@ export class userModel extends personModel {
true
);
}
export { SigningMethods, userModel };

View File

@@ -18,10 +18,6 @@
color: $primary-white !important;
background-color: $disabled-color !important;
border-color: $disabled-color !important;
&:hover {
transform: scale(1);
box-shadow: none;
}
}
}
@@ -37,13 +33,13 @@
}
.register-link-button {
color: #1565c0;
background-color: rgba(#1565c0, 0.08);
border-color: #1565c0;
color: #607d8b;
background-color: rgba(#607d8b, 0.08);
border-color: #607d8b;
&:hover {
color: $primary-white;
background-color: #1565c0;
border-color: #1565c0;
background-color: #607d8b;
border-color: #607d8b;
}
}

View File

@@ -6,6 +6,7 @@ $no-validated: #0603a1; // Example color for effect 20 border
.text-input {
width: 100%;
font-size: larger;
}
// Contenedor para el efecto
@@ -19,7 +20,7 @@ $no-validated: #0603a1; // Example color for effect 20 border
background: transparent;
color: $text-dark;
width: 100%;
height: 2.5rem;
height: 2.8rem;
padding-left: 1rem;
box-sizing: border-box;
font-size: 1rem;

View File

View File

View File

@@ -2,6 +2,7 @@
@use "../../styles/variables" as *;
button {
font-size: larger;
background: $primary-white;
border: 2px solid $standard-button-border;
color: $standard-button-icon;
@@ -20,6 +21,19 @@ button {
align-items: center;
align-content: center;
justify-content: space-evenly;
&:disabled {
$disabled-color: rgba(
$color: #999999,
$alpha: 0.45,
);
color: $primary-white !important;
background-color: $disabled-color !important;
border-color: $disabled-color !important;
&:hover {
transform: scale(1);
box-shadow: none;
}
}
label {
text-align: center;