Compare commits
2 Commits
feature/ev
...
auth/googl
| Author | SHA1 | Date | |
|---|---|---|---|
| 20c8b04638 | |||
| c662c27fc7 |
@@ -6,6 +6,7 @@ import { HttpTestingController, provideHttpClientTesting } from '@angular/common
|
|||||||
import { environment } from '../../../../environments/environment';
|
import { environment } from '../../../../environments/environment';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { CookieService } from '../cookie/cookie.service';
|
import { CookieService } from '../cookie/cookie.service';
|
||||||
|
import { TenantService } from '../tenant.service';
|
||||||
|
|
||||||
describe('AuthService', () => {
|
describe('AuthService', () => {
|
||||||
let cookieStore: Record<string, string> = {};
|
let cookieStore: Record<string, string> = {};
|
||||||
@@ -95,7 +96,7 @@ describe('AuthService', () => {
|
|||||||
httpController.verify();
|
httpController.verify();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('propagates a 401 from /me without clearing the session', async () => {
|
it('clears an expired session when /me returns 401', async () => {
|
||||||
cookieStore['shopit.auth.token'] = 'expired-token';
|
cookieStore['shopit.auth.token'] = 'expired-token';
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@@ -116,11 +117,11 @@ describe('AuthService', () => {
|
|||||||
|
|
||||||
request.flush({ message: 'Unauthorized' }, { status: 401, statusText: 'Unauthorized' });
|
request.flush({ message: 'Unauthorized' }, { status: 401, statusText: 'Unauthorized' });
|
||||||
|
|
||||||
await expect(bootstrapPromise).rejects.toMatchObject({ status: 401 });
|
await expect(bootstrapPromise).resolves.toBeUndefined();
|
||||||
expect(service.user()).toBeNull();
|
expect(service.user()).toBeNull();
|
||||||
expect(service.token()).toBe('expired-token');
|
expect(service.token()).toBeNull();
|
||||||
expect(service.isAuthenticated()).toBe(true);
|
expect(service.isAuthenticated()).toBe(false);
|
||||||
expect(cookieStore['shopit.auth.token']).toBe('expired-token');
|
expect(cookieStore['shopit.auth.token']).toBeUndefined();
|
||||||
|
|
||||||
httpController.verify();
|
httpController.verify();
|
||||||
});
|
});
|
||||||
@@ -132,6 +133,10 @@ describe('AuthService', () => {
|
|||||||
provideHttpClientTesting(),
|
provideHttpClientTesting(),
|
||||||
AuthService,
|
AuthService,
|
||||||
{ provide: CookieService, useValue: createCookieServiceStub() },
|
{ provide: CookieService, useValue: createCookieServiceStub() },
|
||||||
|
{
|
||||||
|
provide: TenantService,
|
||||||
|
useValue: { getTenant: () => ({ codigo: 'tenant-test' }) }
|
||||||
|
},
|
||||||
TransferState
|
TransferState
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
@@ -152,6 +157,7 @@ describe('AuthService', () => {
|
|||||||
|
|
||||||
const request = httpController.expectOne(`${environment.url}register`);
|
const request = httpController.expectOne(`${environment.url}register`);
|
||||||
expect(request.request.method).toBe('POST');
|
expect(request.request.method).toBe('POST');
|
||||||
|
expect(request.request.body.tenant_codigo).toBe('tenant-test');
|
||||||
request.flush({
|
request.flush({
|
||||||
message: 'Usuario registrado correctamente.',
|
message: 'Usuario registrado correctamente.',
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
import { DOCUMENT } from '@angular/common';
|
||||||
|
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
||||||
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
|
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
|
||||||
import { computed, inject, Injectable, PLATFORM_ID, signal, TransferState, makeStateKey } from '@angular/core';
|
import { computed, inject, Injectable, PLATFORM_ID, signal, TransferState, makeStateKey } from '@angular/core';
|
||||||
import { firstValueFrom, map, Observable, of, tap } from 'rxjs';
|
import { firstValueFrom, map, Observable, of, tap } from 'rxjs';
|
||||||
@@ -13,6 +14,7 @@ import {
|
|||||||
UpdateProfilePayload
|
UpdateProfilePayload
|
||||||
} from './auth.interfaces';
|
} from './auth.interfaces';
|
||||||
import { CookieService } from '../cookie/cookie.service';
|
import { CookieService } from '../cookie/cookie.service';
|
||||||
|
import { TenantService } from '../tenant.service';
|
||||||
|
|
||||||
const AUTH_TOKEN_COOKIE_KEY = 'shopit.auth.token';
|
const AUTH_TOKEN_COOKIE_KEY = 'shopit.auth.token';
|
||||||
const AUTH_USER_SSR_STATE_KEY = makeStateKey<AuthUser>('shopit.auth.user');
|
const AUTH_USER_SSR_STATE_KEY = makeStateKey<AuthUser>('shopit.auth.user');
|
||||||
@@ -22,9 +24,11 @@ const AUTH_USER_SSR_STATE_KEY = makeStateKey<AuthUser>('shopit.auth.user');
|
|||||||
})
|
})
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
private readonly http = inject(HttpClient);
|
private readonly http = inject(HttpClient);
|
||||||
|
private readonly document = inject(DOCUMENT);
|
||||||
private readonly platformId = inject(PLATFORM_ID);
|
private readonly platformId = inject(PLATFORM_ID);
|
||||||
private readonly cookieService = inject(CookieService);
|
private readonly cookieService = inject(CookieService);
|
||||||
private readonly transferState = inject(TransferState);
|
private readonly transferState = inject(TransferState);
|
||||||
|
private readonly tenantService = inject(TenantService);
|
||||||
|
|
||||||
private readonly userState = signal<AuthUser | null>(null);
|
private readonly userState = signal<AuthUser | null>(null);
|
||||||
private readonly tokenState = signal<string | null>(null);
|
private readonly tokenState = signal<string | null>(null);
|
||||||
@@ -41,7 +45,33 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
register(payload: RegisterPayload): Observable<RegisterResponse> {
|
register(payload: RegisterPayload): Observable<RegisterResponse> {
|
||||||
return this.http.post<RegisterResponse>(`${environment.url}register`, payload);
|
const tenantCode = this.tenantService.getTenant()?.codigo;
|
||||||
|
|
||||||
|
return this.http.post<RegisterResponse>(`${environment.url}register`, {
|
||||||
|
...payload,
|
||||||
|
...(tenantCode ? { tenant_codigo: tenantCode } : {})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loginWithGoogle(): void {
|
||||||
|
const tenant = this.tenantService.getTenant();
|
||||||
|
if (!tenant) {
|
||||||
|
throw new Error('No se pudo resolver el tenant activo.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiUrl = new URL(environment.url);
|
||||||
|
const authorizationUrl = new URL('/auth/google/redirect', apiUrl.origin);
|
||||||
|
authorizationUrl.searchParams.set('tenant', tenant.codigo);
|
||||||
|
authorizationUrl.searchParams.set('return_url', this.document.location.origin);
|
||||||
|
|
||||||
|
this.document.location.assign(authorizationUrl.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
completeGoogleLogin(oauthCode: string): Observable<AuthUser> {
|
||||||
|
return this.http.post<LoginResponse>(`${environment.url}auth/google/exchange`, { oauth_code: oauthCode }).pipe(
|
||||||
|
tap((response) => this.applyAuthenticatedState(response.token, response.user)),
|
||||||
|
map((response) => response.user)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateProfile(payload: UpdateProfilePayload): Observable<AuthUser> {
|
updateProfile(payload: UpdateProfilePayload): Observable<AuthUser> {
|
||||||
@@ -75,9 +105,24 @@ export class AuthService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await firstValueFrom(this.loadCurrentUser());
|
try {
|
||||||
if (isPlatformServer(this.platformId)) {
|
const user = await firstValueFrom(this.loadCurrentUser());
|
||||||
this.transferState.set(AUTH_USER_SSR_STATE_KEY, user);
|
|
||||||
|
if (isPlatformServer(this.platformId)) {
|
||||||
|
this.transferState.set(AUTH_USER_SSR_STATE_KEY, user);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// An expired token is an expected state, including while rendering on the
|
||||||
|
// server. Do not let it reject the application initializer: Angular's SSR
|
||||||
|
// pipeline can otherwise attempt to serialize HttpErrorResponse internals
|
||||||
|
// as HTTP headers.
|
||||||
|
if (error instanceof HttpErrorResponse && error.status === 401) {
|
||||||
|
this.clearSession();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,8 @@
|
|||||||
variant="neutral-outline"
|
variant="neutral-outline"
|
||||||
hostClass="w-100 d-block"
|
hostClass="w-100 d-block"
|
||||||
buttonClass="w-100 d-inline-flex align-items-center justify-content-center gap-2 py-2 fw-semibold"
|
buttonClass="w-100 d-inline-flex align-items-center justify-content-center gap-2 py-2 fw-semibold"
|
||||||
[disabled]="true"
|
[disabled]="isSubmitting()"
|
||||||
|
(click)="loginWithGoogle()"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { DOCUMENT } from '@angular/common';
|
import { DOCUMENT, isPlatformBrowser } from '@angular/common';
|
||||||
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, inject, PLATFORM_ID, signal } from '@angular/core';
|
||||||
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
import { Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
|
||||||
import { AuthService } from '../../../../core/services/auth/auth.service';
|
import { AuthService } from '../../../../core/services/auth/auth.service';
|
||||||
import { ButtonComponent } from '../../../../shared/components/button/button.component';
|
import { ButtonComponent } from '../../../../shared/components/button/button.component';
|
||||||
@@ -20,8 +20,10 @@ const EMAIL_MAX_LENGTH = 255;
|
|||||||
export class LoginPageComponent {
|
export class LoginPageComponent {
|
||||||
private readonly formBuilder = inject(FormBuilder);
|
private readonly formBuilder = inject(FormBuilder);
|
||||||
private readonly router = inject(Router);
|
private readonly router = inject(Router);
|
||||||
|
private readonly route = inject(ActivatedRoute);
|
||||||
private readonly authService = inject(AuthService);
|
private readonly authService = inject(AuthService);
|
||||||
private readonly document = inject(DOCUMENT);
|
private readonly document = inject(DOCUMENT);
|
||||||
|
private readonly platformId = inject(PLATFORM_ID);
|
||||||
|
|
||||||
private readonly submittedState = signal(false);
|
private readonly submittedState = signal(false);
|
||||||
private readonly serverErrorState = signal<string | null>(null);
|
private readonly serverErrorState = signal<string | null>(null);
|
||||||
@@ -35,6 +37,18 @@ export class LoginPageComponent {
|
|||||||
protected readonly serverError = this.serverErrorState.asReadonly();
|
protected readonly serverError = this.serverErrorState.asReadonly();
|
||||||
protected readonly isSubmitting = this.isSubmittingState.asReadonly();
|
protected readonly isSubmitting = this.isSubmittingState.asReadonly();
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
if (!isPlatformBrowser(this.platformId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oauthCode = this.route.snapshot.queryParamMap.get('oauth_code');
|
||||||
|
|
||||||
|
if (oauthCode) {
|
||||||
|
this.completeGoogleLogin(oauthCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
goToRegister(): void {
|
goToRegister(): void {
|
||||||
void this.router.navigate(['/register']);
|
void this.router.navigate(['/register']);
|
||||||
}
|
}
|
||||||
@@ -62,6 +76,16 @@ export class LoginPageComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loginWithGoogle(): void {
|
||||||
|
this.serverErrorState.set(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.authService.loginWithGoogle();
|
||||||
|
} catch (error: unknown) {
|
||||||
|
this.serverErrorState.set(this.resolveErrorMessage(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected updateEmail(value: string | number): void {
|
protected updateEmail(value: string | number): void {
|
||||||
this.form.controls.email.setValue(String(value));
|
this.form.controls.email.setValue(String(value));
|
||||||
}
|
}
|
||||||
@@ -107,6 +131,22 @@ export class LoginPageComponent {
|
|||||||
this.document.location.assign(homeUrl);
|
this.document.location.assign(homeUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private completeGoogleLogin(oauthCode: string): void {
|
||||||
|
this.isSubmittingState.set(true);
|
||||||
|
this.serverErrorState.set(null);
|
||||||
|
|
||||||
|
this.authService.completeGoogleLogin(oauthCode).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.isSubmittingState.set(false);
|
||||||
|
this.redirectToHome();
|
||||||
|
},
|
||||||
|
error: (error: unknown) => {
|
||||||
|
this.isSubmittingState.set(false);
|
||||||
|
this.serverErrorState.set(this.resolveErrorMessage(error));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private resolveErrorMessage(error: unknown): string {
|
private resolveErrorMessage(error: unknown): string {
|
||||||
const errorPayload =
|
const errorPayload =
|
||||||
typeof error === 'object' && error !== null && 'error' in error
|
typeof error === 'object' && error !== null && 'error' in error
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ describe('RegisterPageComponent', () => {
|
|||||||
component.form.setValue({
|
component.form.setValue({
|
||||||
nombre_apellido: 'Ada Lovelace',
|
nombre_apellido: 'Ada Lovelace',
|
||||||
email: 'ada@example.com',
|
email: 'ada@example.com',
|
||||||
password: 'secret123',
|
password: 'Secret!123',
|
||||||
password_confirmation: 'secret123'
|
password_confirmation: 'Secret!123'
|
||||||
});
|
});
|
||||||
|
|
||||||
component.onSubmit();
|
component.onSubmit();
|
||||||
@@ -59,8 +59,8 @@ describe('RegisterPageComponent', () => {
|
|||||||
expect(authService.register).toHaveBeenCalledWith({
|
expect(authService.register).toHaveBeenCalledWith({
|
||||||
nombre_apellido: 'Ada Lovelace',
|
nombre_apellido: 'Ada Lovelace',
|
||||||
email: 'ada@example.com',
|
email: 'ada@example.com',
|
||||||
password: 'secret123',
|
password: 'Secret!123',
|
||||||
password_confirmation: 'secret123'
|
password_confirmation: 'Secret!123'
|
||||||
});
|
});
|
||||||
expect(modalService.openSimple).toHaveBeenCalledWith({
|
expect(modalService.openSimple).toHaveBeenCalledWith({
|
||||||
content: 'Tu cuenta fue creada correctamente',
|
content: 'Tu cuenta fue creada correctamente',
|
||||||
@@ -178,8 +178,8 @@ describe('RegisterPageComponent', () => {
|
|||||||
component.form.setValue({
|
component.form.setValue({
|
||||||
nombre_apellido: 'Ada Lovelace',
|
nombre_apellido: 'Ada Lovelace',
|
||||||
email: 'ada@example.com',
|
email: 'ada@example.com',
|
||||||
password: 'secret123',
|
password: 'Secret!123',
|
||||||
password_confirmation: 'secret123'
|
password_confirmation: 'Secret!123'
|
||||||
});
|
});
|
||||||
component.onSubmit();
|
component.onSubmit();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user