From ed5cba467706aab50943cd9fe0bed73f9954dd6b Mon Sep 17 00:00:00 2001 From: Marek Lesko Date: Tue, 28 Oct 2025 19:43:46 +0000 Subject: [PATCH] feat: implement OAuth2 authentication flow with Google and PocketId, add callback handling --- Web/src/app/app.component.ts | 11 +- .../authentication/authentication.routes.ts | 6 +- .../callback/callback.component.html | 27 ++++ .../callback/callback.component.ts | 32 +++++ .../side-login/side-login.component.html | 65 ++-------- .../side-login/side-login.component.ts | 32 ++--- .../app/services/authentication.service.ts | 116 ++++++++++++++++++ 7 files changed, 200 insertions(+), 89 deletions(-) create mode 100755 Web/src/app/pages/authentication/callback/callback.component.html create mode 100755 Web/src/app/pages/authentication/callback/callback.component.ts create mode 100644 Web/src/app/services/authentication.service.ts diff --git a/Web/src/app/app.component.ts b/Web/src/app/app.component.ts index d1dd17e..dddf63b 100755 --- a/Web/src/app/app.component.ts +++ b/Web/src/app/app.component.ts @@ -13,15 +13,6 @@ import { OAuthService } from 'angular-oauth2-oidc'; export class AppComponent { title = 'Digitálny asistent PAS'; constructor(private readonly as: OAuthService, private readonly router: Router) { - this.as.configure({ - issuer: 'https://accounts.google.com', - redirectUri: window.location.origin + '/login', - clientId: '1000025801082-09ikmt61a9c9vbdjhpdab9b0ui3vdnij.apps.googleusercontent.com', - dummyClientSecret: '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', - scope: 'openid profile email', - responseType: 'code', - strictDiscoveryDocumentValidation: false, - timeoutFactor: 0.01, - }); + } } diff --git a/Web/src/app/pages/authentication/authentication.routes.ts b/Web/src/app/pages/authentication/authentication.routes.ts index 7a09af2..24cb9e6 100755 --- a/Web/src/app/pages/authentication/authentication.routes.ts +++ b/Web/src/app/pages/authentication/authentication.routes.ts @@ -9,7 +9,7 @@ import { AppMaintenanceComponent } from './maintenance/maintenance.component'; import { AppSideForgotPasswordComponent } from './side-forgot-password/side-forgot-password.component'; import { AppSideLoginComponent } from './side-login/side-login.component'; import { AppSideRegisterComponent } from './side-register/side-register.component'; -import { AppSideTwoStepsComponent } from './side-two-steps/side-two-steps.component'; +import { CallbackComponent } from './callback/callback.component'; export const AuthenticationRoutes: Routes = [ { @@ -52,8 +52,8 @@ export const AuthenticationRoutes: Routes = [ component: AppSideRegisterComponent, }, { - path: 'side-two-steps', - component: AppSideTwoStepsComponent, + path: 'callback', + component: CallbackComponent, }, ], }, diff --git a/Web/src/app/pages/authentication/callback/callback.component.html b/Web/src/app/pages/authentication/callback/callback.component.html new file mode 100755 index 0000000..02eb282 --- /dev/null +++ b/Web/src/app/pages/authentication/callback/callback.component.html @@ -0,0 +1,27 @@ +
+
+
+
+ + +
+ login +
+
+
+
+
+
+

Authentikácia úspešná!

+ @if(!profile) { + Načítavam Vaše údaje... + } + @else{ + {{profile.email}} + profile picture + } +
+
+
+
+
\ No newline at end of file diff --git a/Web/src/app/pages/authentication/callback/callback.component.ts b/Web/src/app/pages/authentication/callback/callback.component.ts new file mode 100755 index 0000000..22c33a3 --- /dev/null +++ b/Web/src/app/pages/authentication/callback/callback.component.ts @@ -0,0 +1,32 @@ +import { Component } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { CoreService } from '../../../services/core.service'; +import { MaterialModule } from '../../../material.module'; +import { BrandingComponent } from '../../../layouts/full/vertical/sidebar/branding.component'; +import { AuthenticationService } from '../../../services/authentication.service'; +import { JsonPipe, NgIf } from '@angular/common'; +import { NgScrollbarModule } from "ngx-scrollbar"; + +@Component({ + selector: 'app-callback', + imports: [RouterModule, MaterialModule, BrandingComponent, JsonPipe, NgIf, NgScrollbarModule], + templateUrl: './callback.component.html', +}) +export class CallbackComponent { + options: any; + profile: any; + + constructor(private settings: CoreService, private as: AuthenticationService) { + this.options = this.settings.getOptions(); + + // Handle the OAuth2 callback and load user profile + this.as + .handleCallback() + .then(_ => { + console.log('Login successful'); + this.profile = this.as.profile; + }).catch(err => { + console.error('Error handling callback', err); + }); + } +} diff --git a/Web/src/app/pages/authentication/side-login/side-login.component.html b/Web/src/app/pages/authentication/side-login/side-login.component.html index 21c56a2..d143aaa 100755 --- a/Web/src/app/pages/authentication/side-login/side-login.component.html +++ b/Web/src/app/pages/authentication/side-login/side-login.component.html @@ -23,63 +23,16 @@ Prihlásiť sa pomocou Google - + + +
+ google + Prihlásiť sa pomocou PocketId +
+
- - - + diff --git a/Web/src/app/pages/authentication/side-login/side-login.component.ts b/Web/src/app/pages/authentication/side-login/side-login.component.ts index 7a1c2c2..4f489e1 100755 --- a/Web/src/app/pages/authentication/side-login/side-login.component.ts +++ b/Web/src/app/pages/authentication/side-login/side-login.component.ts @@ -4,7 +4,7 @@ import { FormGroup, FormControl, Validators, FormsModule, ReactiveFormsModule } import { Router, RouterModule } from '@angular/router'; import { MaterialModule } from '../../../material.module'; import { BrandingComponent } from '../../../layouts/full/vertical/sidebar/branding.component'; -import { OAuthService } from 'angular-oauth2-oidc'; +import { AuthenticationService } from '../../../services/authentication.service'; @Component({ selector: 'app-side-login', @@ -14,30 +14,22 @@ import { OAuthService } from 'angular-oauth2-oidc'; export class AppSideLoginComponent { options: any; - constructor(private settings: CoreService, private router: Router, private readonly as: OAuthService) { + constructor(private settings: CoreService, private router: Router, private readonly as: AuthenticationService) { this.options = this.settings.getOptions(); } - form = new FormGroup({ - uname: new FormControl('', [Validators.required, Validators.minLength(6)]), - password: new FormControl('', [Validators.required]), - }); - - get f() { - return this.form.controls; - } - googleLogin() { - console.warn('Google login initiated'); - this.as.loadDiscoveryDocumentAndLogin() - .then(_ =>{} - // this.as.initLoginFlowInPopup() - ); + this.as.configureAndLogin({ + issuer: 'https://accounts.google.com', + clientId: '1000025801082-09ikmt61a9c9vbdjhpdab9b0ui3vdnij.apps.googleusercontent.com', + dummyClientSecret: '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', + }); } - - submit() { - // console.log(this.form.value); - this.router.navigate(['/dashboards/dashboard1']); + pocketLogin() { + this.as.configureAndLogin({ + issuer: 'https://identity.lesko.me', + clientId: '21131567-fea1-42a2-8907-21abd874eff8', + }); } } diff --git a/Web/src/app/services/authentication.service.ts b/Web/src/app/services/authentication.service.ts new file mode 100644 index 0000000..5fc5b28 --- /dev/null +++ b/Web/src/app/services/authentication.service.ts @@ -0,0 +1,116 @@ +import { Injectable } from '@angular/core'; +import { AuthConfig, OAuthService } from 'angular-oauth2-oidc'; + +const CONFIG_KEY = 'oauth_config_v1'; + +@Injectable({ providedIn: 'root' }) +export class AuthenticationService { + + private config: Partial = { + redirectUri: window.location.origin + '/authentication/callback', + scope: 'openid profile email', + responseType: 'code', + requireHttps: false, + strictDiscoveryDocumentValidation: false, + timeoutFactor: 0.01, + }; + + public profile: any = null; + + constructor(private oauthService: OAuthService) { } + + saveConfig(cfg: Partial) { + try { + localStorage.setItem(CONFIG_KEY, JSON.stringify(cfg)); + } catch { + // ignore} + } + } + + loadConfig(): Partial | null { + const raw = localStorage.getItem(CONFIG_KEY); + if (!raw) return null; + try { + return JSON.parse(raw) as Partial; + } catch { + return null; + } + } + // Configure the library and persist configuration + configure(cfg: Partial) { + this.config = { ...this.config, ...cfg }; + this.saveConfig(this.config); + this.oauthService.configure(this.config); + } + + // Configure and immediately start login flow + configureAndLogin(cfg: Partial) { + this.configure(cfg); + this.startLogin(); + } + + // Restore configuration from storage and apply to OAuthService + restoreConfiguration(): boolean { + const cfg = this.loadConfig() ?? this.config; + if (!cfg) return false; + this.oauthService.configure(cfg); + return true; + } + + // Start login flow using discovery document + Authorization Code (PKCE) + startLogin(cfg?: Partial): Promise { + if (cfg) this.configure(cfg); + return this.oauthService + .loadDiscoveryDocument() + .then(() => { + // initCodeFlow will redirect to the provider + this.oauthService.initCodeFlow(); + }); + } + + // Call this on the callback route to process the redirect and obtain tokens + profile + handleCallback(): Promise { + this.restoreConfiguration(); + // Ensure discovery document loaded, then process code flow, then load profile + return this.oauthService + .loadDiscoveryDocumentAndTryLogin() + .then((isLoggedIn: boolean) => { + if (!isLoggedIn && !this.oauthService.hasValidAccessToken()) { + return Promise.reject('No valid token after callback'); + } + return this.oauthService.loadUserProfile(); + }).then(profile => { + this.profile = profile["info"]; + }); + } + + // Convenience helpers + get events() { + return this.oauthService.events; + } + + hasValidAccessToken(): boolean { + return this.oauthService.hasValidAccessToken(); + } + + getAccessToken(): string { + return this.oauthService.getAccessToken(); + } + + getIdToken(): string { + return this.oauthService.getIdToken(); + } + + getIdentityClaims(): object | null { + return this.oauthService.getIdentityClaims() as object | null; + } + + logout(destroyLocalSession = false) { + if (destroyLocalSession) { + try { + localStorage.removeItem(CONFIG_KEY); + } catch { } + } + this.oauthService.logOut(); + } +} \ No newline at end of file