Skip to content

Commit

Permalink
Merge pull request #12 from makimenko/login
Browse files Browse the repository at this point in the history
feature: Improve authentication (avoid re-login on refresh)
  • Loading branch information
makimenko authored Nov 29, 2023
2 parents 69a71c2 + 66a0130 commit ae7777f
Show file tree
Hide file tree
Showing 12 changed files with 185 additions and 129 deletions.
20 changes: 11 additions & 9 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Component, inject, OnInit} from '@angular/core';
import {AuthService} from "./general/service/auth.service";
import {ActivatedRoute, Router} from "@angular/router";
import {AuthService} from './general/service/auth.service';
import {ActivatedRoute, Router} from '@angular/router';

@Component({
selector: 'app-root',
Expand All @@ -14,14 +14,16 @@ export class AppComponent implements OnInit {
private auth: AuthService,
private router: Router
) {
console.log("AppComponent.constructor")
console.log('AppComponent.constructor');
}

ngOnInit() {
if (this.auth.userSignedIn) {
this.router.navigate(["/manager"])
} else {
this.router.navigate(["/login"])
}
async ngOnInit() {
// console.log("AppComponent.ngOnInit");
// if (!await this.auth.checkIfUserAuthenticated()) {
// this.router.navigate(['/login']);
// }
// console.log("AppComponent.ngOnInit end");
}


}
6 changes: 5 additions & 1 deletion src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { LoginHomeComponent } from './login-home/login-home.component';
import {MatGridListModule} from "@angular/material/grid-list";
import {MatCardModule} from "@angular/material/card";
import {MatButtonModule} from "@angular/material/button";
import {DynamicScriptLoaderService} from './general/service/dynamic-script-loader.service';
import {MatProgressBarModule} from '@angular/material/progress-bar';


@NgModule({
Expand All @@ -34,7 +36,8 @@ import {MatButtonModule} from "@angular/material/button";
HttpClientModule,
MatGridListModule,
MatCardModule,
MatButtonModule
MatButtonModule,
MatProgressBarModule
],
declarations: [
AppComponent,
Expand All @@ -43,6 +46,7 @@ import {MatButtonModule} from "@angular/material/button";
providers: [
DiagramService,
AuthService,
DynamicScriptLoaderService,
LoggedInGuard,
UserPreferenceService,
GoogleDriveService,
Expand Down
11 changes: 8 additions & 3 deletions src/app/data-access/service/google-drive.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {Injectable} from '@angular/core';
import {AuthService} from '../../general/service/auth.service';
import {HttpClient} from '@angular/common/http';
import {DiagramMetadata} from '../model/diagram-item.model';
import {environment} from '../../../environments/environment';

declare let gapi: any;

Expand Down Expand Up @@ -49,7 +50,11 @@ export class GoogleDriveService {

public async init(): Promise<void> {
if (!this.initialized) {
// console.log('GoogleDriveService.init');
console.log('GoogleDriveService.init token', this.auth.accessToken);
gapi.client.setToken({access_token: this.auth.accessToken});
gapi.client.setApiKey(environment.gapi.api_key);
gapi.client.load('drive', 'v3');

await gapi.client.load('drive', 'v3');
this.initialized = true;
}
Expand Down Expand Up @@ -99,7 +104,7 @@ export class GoogleDriveService {
const endpoint = 'https://www.googleapis.com/upload/drive/v3/files/' + id + '?uploadType=multipart&fields=id';
return this.http.patch(endpoint, form, {
headers: {
Authorization: this.auth.getAuthorizationHeader()
Authorization: await this.auth.getAuthorizationHeader()
}
}).toPromise();
} else {
Expand All @@ -108,7 +113,7 @@ export class GoogleDriveService {
const endpoint = 'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id';
return this.http.post(endpoint, form, {
headers: {
Authorization: this.auth.getAuthorizationHeader()
Authorization: await this.auth.getAuthorizationHeader()
}
}).toPromise();
}
Expand Down
192 changes: 106 additions & 86 deletions src/app/general/service/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,137 +1,157 @@
import {EventEmitter, Injectable} from '@angular/core';
import {environment} from '../../../environments/environment';
import TokenClient = google.accounts.oauth2.TokenClient;
import {DynamicScriptLoaderService} from './dynamic-script-loader.service';
import {GoogleDriveService} from '../../data-access/service/google-drive.service';

export type Profile = {
name: string,
email: string,
};

const AUTH_KEY = 'vect.AuthService.auth';
declare let gapi: any;
const GOOGLE_PROFILE_URL = `https://www.googleapis.com/oauth2/v3/userinfo`;


@Injectable()
export class AuthService {

public authenticatedEvent = new EventEmitter<Profile>();

tokenClient!: TokenClient;
accessToken?: string;

authInited = false;
gapiInited = false;
userSignedIn = false;
inited = false;
userAuthenticated = false;

private profile: any;
public name!: string;
public email!: string;
private authenticated!: EventEmitter<any>;
public profile?: Profile;

constructor() {
this.handleAuthResponse = this.handleAuthResponse.bind(this);
constructor(
protected dynamicScriptLoader: DynamicScriptLoaderService
) {
this.handleTokenResponse = this.handleTokenResponse.bind(this);
this.handleProfileResponse = this.handleProfileResponse.bind(this);
}


/**
* Callback after Google Identity Services are loaded.
*/
googleOAuthInit() {
public async checkIfUserAuthenticated(): Promise<boolean> {
console.log('AuthService.checkIfUserAuthenticated before', this.userAuthenticated);
if (!this.userAuthenticated) {
await this.init();
const storedAccessToken = localStorage.getItem(AUTH_KEY);
if (storedAccessToken) {
await this.requestProfile(storedAccessToken);
this.accessToken = storedAccessToken;
}
}
console.log('AuthService.checkIfUserAuthenticated after', this.userAuthenticated);
return this.userAuthenticated;
}


protected async init(): Promise<void> {
if (!this.inited) {
await this.initGoogleScripts();
console.log('AuthService.initialize google scripts loaded');
this.initTokenClient();
}
}

protected initGoogleScripts(): Promise<any> {
console.log('AuthService.initGoogleScripts');
const p1 = this.dynamicScriptLoader.loadScript('https://accounts.google.com/gsi/client');
const p2 = this.dynamicScriptLoader.loadScript('https://apis.google.com/js/api.js');
const p3 = this.dynamicScriptLoader.loadScript('https://apis.google.com/js/client:plusone.js');
return Promise.all([p1, p2, p3]);
}


protected initTokenClient(): void {
console.log('AuthService.initTokenClient');
this.tokenClient = google.accounts.oauth2.initTokenClient({
client_id: environment.gapi.client_id,
scope: environment.gapi.scope,
callback: this.handleAuthResponse
callback: this.handleTokenResponse
});
this.authInited = true;
console.log("GoogleUtils.gisLoaded inited")
this.inited = true;
}


async handleAuthResponse(res: any) {
private async handleTokenResponse(res: any): Promise<void> {
console.log('AuthService.handleTokenResponse', res);
if (res.error !== undefined) {
this.userSignedIn = false;
this.userAuthenticated = false;
throw (res);
}
console.log("GoogleUtils.handleAuthClick resp", res);
this.requestProfile(res.access_token)

if (res && res.access_token) {
gapi.client.setApiKey(environment.gapi.api_key);
gapi.client.load('drive', 'v3');
// await gapi.client.init({
// apiKey: environment.gapi.api_key,
// discoveryDocs: environment.gapi.discoveryDocs,
// });
this.gapiInited = true;
}
await this.requestProfile(res.access_token);

}
// wrong location?
// gapi.client.setApiKey(environment.gapi.api_key);
// gapi.client.load('drive', 'v3');

this.accessToken = res.access_token;
localStorage.setItem(AUTH_KEY, res.access_token);
}
}

requestProfile(accessToken: any) {
console.log("getUserProfileData", accessToken)
let promise = new Promise(function (resolve, reject) {
let request = new XMLHttpRequest();
const url = `https://www.googleapis.com/oauth2/v3/userinfo`;
request.addEventListener("loadend", function () {

const response = JSON.parse(this.responseText);
console.log("getUserProfileData loadend response", response)

if (this.status === 200) {
resolve(response);
} else {
// @ts-ignore
reject(this, response);
}
});
request.open("GET", url, true);
request.setRequestHeader('Authorization', `Bearer ${accessToken}`);
request.send();
});

console.log("getUserProfileData then");
async requestProfile(accessToken: string): Promise<void> {
console.log('AuthService.requestProfile', accessToken);

promise.then(
this.handleProfileResponse, function (errorMessage) {
console.error(errorMessage);
});
const res = await fetch(GOOGLE_PROFILE_URL, {
method: 'get',
headers: new Headers({
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json'
})
});

if (res.ok) {
const profileResponse = await res.json();
await this.handleProfileResponse(profileResponse);
}
console.log('AuthService.requestProfile end');
}

handleProfileResponse(profileResponse: any) {
this.profile = profileResponse;
this.email = profileResponse.email;
this.name = profileResponse.name;
this.userSignedIn = true;
this.authenticated.emit(this.profile);
console.log("getUserProfileData response", profileResponse);

private async handleProfileResponse(profileResponse: any) {
console.log('AuthService.handleProfileResponse response', profileResponse);
this.profile = {
name: profileResponse.name,
email: profileResponse.email
};
this.userAuthenticated = true;
console.log('AuthService.handleProfileResponse authenticatedEvent.emit', this.profile);
this.authenticatedEvent.emit(this.profile);
}


/**
* Sign in the user upon button click.
*/
handleAuthClick() {
const token = gapi.client.getToken()
handleAuthClick(): void {
const token = gapi.client.getToken();
if (token) {
console.log("AuthService.handleAuthClick Skip")
console.log('AuthService.handleAuthClick Skip');
// Skip display of account chooser and consent dialog for an existing session.
this.tokenClient.requestAccessToken({prompt: '', login_hint: 'Super Hint!'});
this.tokenClient.requestAccessToken({prompt: '', state: AUTH_KEY});
} else {
console.log("AuthService.handleAuthClick Prompt the user to select")
console.log('AuthService.handleAuthClick Prompt the user to select');
// Prompt the user to select a Google Account and ask for consent to share their data
// when establishing a new session.
this.tokenClient.requestAccessToken({prompt: 'consent'});
this.tokenClient.requestAccessToken({prompt: 'consent', state: AUTH_KEY});
}
}


initialize(authenticated: EventEmitter<any>) {
this.authenticated = authenticated;
this.googleOAuthInit();
}

checkIfUserAuthenticated() {
console.log("AuthService.checkIfUserAuthenticated", this.userSignedIn)

return this.userSignedIn;
public async getAuthorizationHeader(): Promise<string> {
if (await this.checkIfUserAuthenticated()) {
return 'Bearer ' + this.accessToken;
} else {
throw new Error('User is not authenticated');
}
}

public getAuthorizationHeader(): string {
const token = gapi.auth.getToken();
// @ts-ignore
return token.token_type + ' ' + token.access_token;
public get allowToSignIn(): boolean {
return this.inited;
}

}
Expand Down
23 changes: 23 additions & 0 deletions src/app/general/service/dynamic-script-loader.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Injectable } from '@angular/core';

@Injectable({
providedIn: 'root'
})
export class DynamicScriptLoaderService {

public loadScript(url: string): Promise<void> {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.onload = (): any => {
resolve();
};
script.onerror = () => {
reject(new Error(`Script load error for ${url}`));
};
document.head.appendChild(script);
});
}

}
8 changes: 4 additions & 4 deletions src/app/general/service/logged-in.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ export class LoggedInGuard implements CanActivate {
constructor(
private authService: AuthService
) {
console.log("LoggedInGuard.constructor")
console.log("LoggedInGuard.constructor");
}

public async canActivate(): Promise<boolean> {
const canActivate = this.authService.checkIfUserAuthenticated()
console.log("LoggedInGuard.canActivate", canActivate);
return canActivate;
const userAuthenticated = await this.authService.checkIfUserAuthenticated();
console.log("LoggedInGuard.canActivate", userAuthenticated);
return userAuthenticated;
}

}
2 changes: 1 addition & 1 deletion src/app/login-home/login-home.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
</p>
</mat-card-content>
<mat-card-actions align="end">
<button mat-button color="warn" mat-raised-button #btnFocus=matButton [autofocus]="btnFocus.focus()" (click)="handleAuthClick()">Google Login</button>
<button [disabled]="!authService.allowToSignIn" mat-button color="warn" mat-raised-button #btnFocus=matButton [autofocus]="btnFocus.focus()" (click)="handleAuthClick()">Google Login</button>
</mat-card-actions>
</mat-card>
</mat-grid-tile>
Expand Down
Loading

0 comments on commit ae7777f

Please sign in to comment.