-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
#652 login #127
#652 login #127
Changes from all commits
9be1141
303cbe8
d688a2b
fbb0689
f274c79
46bf76a
d0e742c
05ee485
1317f81
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import { TestBed } from '@angular/core/testing'; | ||
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; | ||
import { RouterTestingModule } from '@angular/router/testing'; | ||
import { BehaviorSubject } from 'rxjs'; | ||
import { AuthGuard } from './auth.guard'; | ||
import { AuthService } from './auth.service'; | ||
|
||
describe('AuthGuard', () => { | ||
let guard: AuthGuard; | ||
let mockAuthService: jasmine.SpyObj<AuthService>; | ||
let route: ActivatedRouteSnapshot; | ||
let state: RouterStateSnapshot; | ||
|
||
beforeEach(() => { | ||
mockAuthService = jasmine.createSpyObj(AuthService, ['isLoggedIn$']); | ||
mockAuthService.isLoggedIn$ = new BehaviorSubject<boolean | null>(null); | ||
|
||
TestBed.configureTestingModule({ | ||
imports: [RouterTestingModule], | ||
providers: [{ provide: AuthService, useValue: mockAuthService }], | ||
}); | ||
guard = TestBed.inject(AuthGuard); | ||
}); | ||
|
||
it('should be created', () => { | ||
expect(guard).toBeTruthy(); | ||
}); | ||
|
||
it('should redirect to the login screen when not logged in', () => { | ||
mockAuthService.isLoggedIn$.next(false); | ||
guard.canActivate(route, state).subscribe((res) => expect(res.toString()).toEqual('/login')); | ||
}); | ||
|
||
it('should proceed when logged in', () => { | ||
mockAuthService.isLoggedIn$.next(true); | ||
guard.canActivate(route, state).subscribe((res) => expect(res).toEqual(true)); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { Injectable } from '@angular/core'; | ||
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router'; | ||
import { filter, map, Observable } from 'rxjs'; | ||
import { AuthService } from './auth.service'; | ||
|
||
@Injectable({ | ||
providedIn: 'root', | ||
}) | ||
export class AuthGuard implements CanActivate { | ||
constructor(private authService: AuthService, private router: Router) {} | ||
|
||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> { | ||
return this.authService.isLoggedIn$.pipe( | ||
filter((isLoggedIn) => isLoggedIn !== null), // loading, so short circuit | ||
map((isLoggedIn) => { | ||
if (!isLoggedIn) { | ||
return this.router.parseUrl('/login'); | ||
} else { | ||
return true; | ||
} | ||
}) | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { NgModule } from '@angular/core'; | ||
import { GoogleLoginProvider, SocialAuthServiceConfig, SocialLoginModule } from '@abacritt/angularx-social-login'; | ||
|
||
@NgModule({ | ||
imports: [SocialLoginModule], | ||
providers: [ | ||
{ | ||
provide: 'SocialAuthServiceConfig', | ||
useValue: { | ||
autoLogin: true, | ||
providers: [ | ||
{ | ||
id: GoogleLoginProvider.PROVIDER_ID, | ||
provider: new GoogleLoginProvider( | ||
'257845957103-qdi3hno6v1bf4lli44km2av2gijrs758.apps.googleusercontent.com' | ||
), | ||
}, | ||
], | ||
} as SocialAuthServiceConfig, | ||
}, | ||
], | ||
}) | ||
export class AuthModule {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import { SocialAuthService, SocialUser } from '@abacritt/angularx-social-login'; | ||
import { TestBed } from '@angular/core/testing'; | ||
import { Router } from '@angular/router'; | ||
import { Apollo } from 'apollo-angular'; | ||
import { Observable } from 'rxjs'; | ||
import { AuthService } from './auth.service'; | ||
|
||
describe('AuthService', () => { | ||
let service: AuthService; | ||
let mockSocialAuthService: jasmine.SpyObj<SocialAuthService>; | ||
let mockApollo: jasmine.SpyObj<Apollo>; | ||
let mockRouter: jasmine.SpyObj<Router>; | ||
|
||
beforeEach(() => { | ||
mockSocialAuthService = jasmine.createSpyObj(SocialAuthService, ['signIn', 'signOut']); | ||
mockApollo = jasmine.createSpyObj(Apollo, ['client']); | ||
mockRouter = jasmine.createSpyObj(Router, ['navigateByUrl']); | ||
|
||
TestBed.configureTestingModule({ | ||
providers: [ | ||
{ provide: SocialAuthService, useValue: { ...mockSocialAuthService, authState: new Observable() } }, | ||
{ provide: Apollo, useValue: mockApollo }, | ||
{ provide: Router, useValue: mockRouter }, | ||
], | ||
}); | ||
service = TestBed.inject(AuthService); | ||
}); | ||
|
||
it('should be created', () => { | ||
expect(service).toBeTruthy(); | ||
}); | ||
|
||
it('should route the user back into the app after login', async () => { | ||
const user: SocialUser = {} as SocialUser; | ||
|
||
mockApollo.client.clearStore = jasmine.createSpy(); | ||
mockSocialAuthService.signIn.and.returnValue(new Promise((resolve, reject) => resolve(user))); | ||
|
||
await service.loginWithGoogle(); | ||
|
||
expect(mockRouter.navigateByUrl).toHaveBeenCalledWith('/devices'); | ||
}); | ||
|
||
it('should route the user to the login page and should clear the cache after logout', async () => { | ||
mockApollo.client.clearStore = jasmine.createSpy(); | ||
mockSocialAuthService.signOut.and.returnValue(new Promise((resolve, reject) => resolve())); | ||
|
||
await service.logout(); | ||
|
||
expect(mockRouter.navigateByUrl).toHaveBeenCalledWith('/login'); | ||
expect(mockApollo.client.clearStore).toHaveBeenCalled(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { GoogleLoginProvider, SocialAuthService, SocialUser } from '@abacritt/angularx-social-login'; | ||
import { Injectable } from '@angular/core'; | ||
import { Router } from '@angular/router'; | ||
import { Apollo } from 'apollo-angular'; | ||
import { BehaviorSubject } from 'rxjs'; | ||
|
||
@Injectable({ | ||
providedIn: 'root', | ||
}) | ||
export class AuthService { | ||
user$ = new BehaviorSubject<SocialUser | null>(null); | ||
isLoggedIn$ = new BehaviorSubject<boolean | null>(null); | ||
|
||
constructor(private socialAuthService: SocialAuthService, private router: Router, private apollo: Apollo) { | ||
this.socialAuthService.authState.subscribe((user) => { | ||
this.user$.next(user); | ||
this.isLoggedIn$.next(user !== null); | ||
}); | ||
} | ||
|
||
async loginWithGoogle(): Promise<void> { | ||
await this.socialAuthService.signIn(GoogleLoginProvider.PROVIDER_ID); // TODO:: try option {ux_mode: 'redirect'} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this TODO still valid? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yea, i may want to try this, i need a hand from patrick though, will confirm by next pr |
||
this.router.navigateByUrl('/devices'); | ||
} | ||
|
||
async logout(): Promise<void> { | ||
await this.socialAuthService.signOut(); | ||
this.router.navigateByUrl('/login'); | ||
this.apollo.client.clearStore(); // clear apollo cache | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,12 +2,37 @@ import { NgModule } from '@angular/core'; | |
import { HttpClientModule } from '@angular/common/http'; | ||
import { ApolloModule, APOLLO_OPTIONS } from 'apollo-angular'; | ||
import { HttpLink } from 'apollo-angular/http'; | ||
import { InMemoryCache, ApolloClientOptions } from '@apollo/client/core'; | ||
import { InMemoryCache, ApolloClientOptions, ApolloLink } from '@apollo/client/core'; | ||
import { setContext } from '@apollo/client/link/context'; | ||
import { SocialAuthService } from '@abacritt/angularx-social-login'; | ||
import { firstValueFrom, map, take } from 'rxjs'; | ||
|
||
const uri = '/api'; | ||
export function createApollo(httpLink: HttpLink): ApolloClientOptions<any> { | ||
export function createApollo(httpLink: HttpLink, authService: SocialAuthService): ApolloClientOptions<any> { | ||
const auth = setContext(async (operation, context) => { | ||
const idToken = await firstValueFrom( | ||
authService.authState.pipe( | ||
map((authState) => authState?.idToken), | ||
take(1) | ||
) | ||
); | ||
|
||
// If refreshToken needs to be used see: | ||
// https://apollo-angular.com/docs/recipes/authentication/#waiting-for-a-refreshed-token | ||
|
||
if (!idToken) { | ||
return {}; | ||
} else { | ||
return { | ||
headers: { | ||
idToken, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. small detail, should the header be id_token like we've done in onpoint/jetstream? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nah |
||
}, | ||
}; | ||
} | ||
}); | ||
|
||
return { | ||
link: httpLink.create({ uri }), | ||
link: ApolloLink.from([auth, httpLink.create({ uri })]), | ||
cache: new InMemoryCache(), | ||
}; | ||
} | ||
|
@@ -18,7 +43,7 @@ export function createApollo(httpLink: HttpLink): ApolloClientOptions<any> { | |
{ | ||
provide: APOLLO_OPTIONS, | ||
useFactory: createApollo, | ||
deps: [HttpLink], | ||
deps: [HttpLink, SocialAuthService], | ||
}, | ||
], | ||
}) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { NgModule } from '@angular/core'; | ||
import { RouterModule, Routes } from '@angular/router'; | ||
import { LoginComponent } from './login.component'; | ||
|
||
const routes: Routes = [ | ||
{ | ||
path: '', | ||
component: LoginComponent, | ||
}, | ||
]; | ||
|
||
@NgModule({ | ||
imports: [RouterModule.forChild(routes)], | ||
exports: [RouterModule], | ||
}) | ||
export class LoginRoutingModule {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
<div class="login-container"> | ||
<mat-card class="login-widget"> | ||
<mat-card-header> | ||
<mat-card-title>UDMI</mat-card-title> | ||
<mat-card-subtitle>Device Management Console</mat-card-subtitle> | ||
</mat-card-header> | ||
<mat-card-actions align="end"> | ||
<button class="google-login-button" mat-button (click)="loginWithGoogle()"> | ||
<img class="google-icon-svg" src="assets/images/google-logo.svg" /> | ||
<span>Sign in with Google</span> | ||
</button> | ||
</mat-card-actions> | ||
</mat-card> | ||
</div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it possible that this not be hard coded?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll see
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's possible, but I think it's maybe too much work for this pr? I can add a todo, create a ticket, and tackle this kind of refactor later?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll see how much effort this is, maybe i can do it now, but i have a feeling for deployment, it will need some work to inject these variables in somehow