From 86e6f595677b9525f272ce4833e15c120d2fd7d9 Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 18 Dec 2024 07:02:08 +0300 Subject: [PATCH] feat: add providers OAuth --- src/api/v1/authApiService.ts | 38 +++++- .../OAuthProviders/OAuthProviders.css | 77 +++++++++++ .../OAuthProviders/OAuthProviders.html | 16 +++ .../OAuthProviders/OAuthProviders.ts | 126 ++++++++++++++++++ 4 files changed, 255 insertions(+), 2 deletions(-) create mode 100644 src/components/OAuthProviders/OAuthProviders.css create mode 100644 src/components/OAuthProviders/OAuthProviders.html create mode 100644 src/components/OAuthProviders/OAuthProviders.ts diff --git a/src/api/v1/authApiService.ts b/src/api/v1/authApiService.ts index c6ed596..249325a 100644 --- a/src/api/v1/authApiService.ts +++ b/src/api/v1/authApiService.ts @@ -1,8 +1,14 @@ import {Injectable} from "@angular/core"; import ApiService, {AvailableVersion} from "@api/api.service"; import {LoginRequest} from "@api/v1/loginRequest"; -import {catchError, of} from "rxjs"; -import {AuthRoles} from "@model/AuthRoles"; +import {catchError, map, Observable, of} from "rxjs"; +import {AuthRoles} from "@model/authRoles"; +import {AvailableOAuthProvidersResponse} from "@api/v1/availableProvidersResponse"; +import {OAuthProvider} from "@model/oAuthProvider"; + +export interface OAuthProviderData extends AvailableOAuthProvidersResponse { + icon: string; +} @Injectable() export default class AuthApiService extends ApiService { @@ -51,4 +57,32 @@ export default class AuthApiService extends ApiService { }) ); } + + private getProviderIcon(provider: OAuthProvider): string { + switch (provider) { + case OAuthProvider.Google: + return 'assets/icons/google.svg'; + case OAuthProvider.Yandex: + return 'assets/icons/yandex.svg'; + case OAuthProvider.MailRu: + return 'assets/icons/mailru.svg'; + default: + return ''; + } + } + + public availableProviders(): Observable { + let request = this.createRequestBuilder() + .setEndpoint('AvailableProviders') + .setWithCredentials() + .build; + + return this.get>(request).pipe( + map(data => { + return data.map((provider) => ({ + ...provider, + icon: this.getProviderIcon(provider.provider), + }) as OAuthProviderData); + })); + } } diff --git a/src/components/OAuthProviders/OAuthProviders.css b/src/components/OAuthProviders/OAuthProviders.css new file mode 100644 index 0000000..1d16347 --- /dev/null +++ b/src/components/OAuthProviders/OAuthProviders.css @@ -0,0 +1,77 @@ +.provider-container { + display: flex; + gap: 16px; + flex-wrap: wrap; + justify-content: center; + margin: 0; + padding: 0; +} + +.provider-container a { + display: inline-block; + text-align: center; + transition: transform 0.3s ease-in-out, filter 0.3s ease; + border-radius: 50%; + padding: 8px; +} + +.provider-icon { + object-fit: contain; + user-select: none; + cursor: pointer; + transition: transform 0.3s ease-in-out, box-shadow 0.3s ease, filter 0.3s ease; + border-radius: 50%; +} + +.provider-container a:hover .provider-icon { + transform: scale(1.1); /* Slight zoom-in effect on hover */ + box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); /* Adding shadow for effect */ +} + +.provider-container .provider-item.disabled { + pointer-events: none; /* Disables click */ + opacity: 0.5; /* Dims the icon to indicate it is disabled */ +} + +.provider-container .provider-item.disabled .provider-icon { + filter: grayscale(100%) contrast(100%); /* Desaturates image if disabled */ +} + +.provider-item { + width: 48px; + height: 48px; + position: relative; +} + +.provider-item.provider-unlink { + filter: grayscale(50%) contrast(60%); + transition: filter 0.3s ease; +} + +.provider-item.provider-unlink:hover { + filter: grayscale(0%) contrast(100%); +} + +.provider-item.provider-unlink::after { + content: '×'; + position: absolute; + top: 10%; + left: 90%; + transform: translate(-100%, 0%) scale(0.0); + font-size: 24px; + color: white; + background-color: red; + width: 24px; + height: 24px; + display: flex; + justify-content: center; + align-items: center; + border-radius: 50%; + cursor: pointer; + + transition: transform 0.15s ease; +} + +.provider-item.provider-unlink:hover::after { + transform: translate(-50%, -50%) scale(1.0); +} diff --git a/src/components/OAuthProviders/OAuthProviders.html b/src/components/OAuthProviders/OAuthProviders.html new file mode 100644 index 0000000..fa20880 --- /dev/null +++ b/src/components/OAuthProviders/OAuthProviders.html @@ -0,0 +1,16 @@ +@if (providers.length !== 0) { +
+
+

{{ message }}

+ +
+ @for (provider of providers; track $index) { + + + + } +
+
+} diff --git a/src/components/OAuthProviders/OAuthProviders.ts b/src/components/OAuthProviders/OAuthProviders.ts new file mode 100644 index 0000000..ccbaed9 --- /dev/null +++ b/src/components/OAuthProviders/OAuthProviders.ts @@ -0,0 +1,126 @@ +import {Component, Inject, Input, OnInit} from '@angular/core'; +import AuthApiService, {OAuthProviderData} from "@api/v1/authApiService"; +import {OAuthProvider} from "@model/oAuthProvider"; +import {ToastrService} from "ngx-toastr"; +import { + MAT_DIALOG_DATA, MatDialog, + MatDialogActions, + MatDialogContent, + MatDialogRef, + MatDialogTitle +} from "@angular/material/dialog"; +import {MatButton} from "@angular/material/button"; + +interface AvailableOAuthProviders extends OAuthProviderData { + disabled: boolean; +} + +@Component({ + selector: 'app-delete-confirm-dialog', + template: ` +

Удалить провайдера?

+ +

Вы уверены, что хотите удалить провайдера {{ data.provider.name }}?

+
+ + + + + `, + imports: [ + MatDialogTitle, + MatDialogContent, + MatDialogActions, + MatButton + ] +}) +export class DeleteConfirmDialog { + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: any + ) { + } + + onConfirm(): void { + this.dialogRef.close(true); + } + + onCancel(): void { + this.dialogRef.close(false); + } +} + +@Component({ + selector: 'OAuthProviders', + imports: [], + templateUrl: './OAuthProviders.html', + styleUrl: './OAuthProviders.css', + providers: [AuthApiService] +}) +export class OAuthProviders implements OnInit { + protected providers: AvailableOAuthProviders[] = []; + protected _activeProvidersId: OAuthProvider[] = []; + + @Input() message: string = 'Вы можете войти в аккаунт через'; + @Input() activeProviders: string[] = []; + + @Input() set activeProvidersId(data: OAuthProvider[]) { + this._activeProvidersId = data; + this.updateDisabledProviders(); + } + + @Input() canUnlink: boolean = false; + + constructor(authApi: AuthApiService, private notify: ToastrService, private dialog: MatDialog) { + authApi.availableProviders().subscribe(providers => this.updateDisabledProviders(providers)); + } + + private updateDisabledProviders(data: OAuthProviderData[] | null = null) { + this.providers = (data ?? this.providers).map(provider => { + return { + ...provider, + disabled: this._activeProvidersId.includes(provider.provider) || this.activeProviders.includes(provider.providerName) + }; + }); + } + + ngOnInit(): void { + window.addEventListener('message', (event) => { + if (event.data && event.data.success === false) { + console.error(event.data.message); + this.notify.error(event.data.message, 'OAuth ошибка'); + } else { + this.activeProvidersId.push(event.data.provider); + this.updateDisabledProviders(); + } + }); + } + + protected openOAuth(provider: AvailableOAuthProviders) { + console.log(provider.redirect); + const oauthWindow = window.open( + provider.redirect, + '_blank', + ); + + if (!oauthWindow) { + this.notify.error('Не удалось открыть OAuth окно'); + return; + } + } + + protected confirmDelete(provider: AvailableOAuthProviders) { + const dialogRef = this.dialog.open(DeleteConfirmDialog, {data: {provider}}); + + dialogRef.afterClosed().subscribe(result => { + if (result) { + this.deleteProvider(provider); + } + }); + } + + protected deleteProvider(provider: AvailableOAuthProviders) { + // todo: remove provider + } +}