Skip to content

Commit

Permalink
Merge pull request #127 from Buildings-IOT/faucetsdn#652-Login
Browse files Browse the repository at this point in the history
  • Loading branch information
mspratt-biot committed Apr 21, 2022
2 parents 9f40332 + 1317f81 commit be525f7
Show file tree
Hide file tree
Showing 27 changed files with 2,212 additions and 972 deletions.
2,696 changes: 1,757 additions & 939 deletions udmif/web/package-lock.json

Large diffs are not rendered by default.

27 changes: 14 additions & 13 deletions udmif/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,17 @@
"src/**/*.{html,ts,json,scss}": "prettier --single-quote --arrow-parens always --trailing-comma es5 --print-width 120 --write"
},
"dependencies": {
"@angular/animations": "~13.2.0",
"@angular/cdk": "^13.2.4",
"@angular/common": "~13.2.0",
"@angular/compiler": "~13.2.0",
"@angular/core": "~13.2.0",
"@angular/forms": "~13.2.0",
"@angular/material": "^13.2.4",
"@angular/platform-browser": "~13.2.0",
"@angular/platform-browser-dynamic": "~13.2.0",
"@angular/router": "~13.2.0",
"@abacritt/angularx-social-login": "^1.0.2",
"@angular/animations": "~13.3.3",
"@angular/cdk": "^13.3.3",
"@angular/common": "~13.3.3",
"@angular/compiler": "~13.3.3",
"@angular/core": "~13.3.3",
"@angular/forms": "~13.3.3",
"@angular/material": "^13.3.3",
"@angular/platform-browser": "~13.3.3",
"@angular/platform-browser-dynamic": "~13.3.3",
"@angular/router": "~13.3.3",
"@apollo/client": "^3.5.9",
"apollo-angular": "^3.0.0",
"graphql": "^16.3.0",
Expand All @@ -37,9 +38,9 @@
"zone.js": "~0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "~13.2.5",
"@angular/cli": "~13.2.5",
"@angular/compiler-cli": "~13.2.0",
"@angular-devkit/build-angular": "~13.3.3",
"@angular/cli": "~13.3.3",
"@angular/compiler-cli": "~13.3.3",
"@types/jasmine": "~3.10.0",
"@types/lodash-es": "^4.17.6",
"@types/node": "^12.11.1",
Expand Down
13 changes: 12 additions & 1 deletion udmif/web/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from './auth/auth.guard';
import { LoginGuard } from './login/login.guard';

const routes: Routes = [
{ path: '', redirectTo: 'devices', pathMatch: 'full' },
{ path: 'devices', loadChildren: () => import('./devices/devices.module').then((m) => m.DevicesModule) },
{
path: 'login',
loadChildren: () => import('./login/login.module').then((m) => m.LoginModule),
canActivate: [LoginGuard],
},
{
path: 'devices',
loadChildren: () => import('./devices/devices.module').then((m) => m.DevicesModule),
canActivate: [AuthGuard],
},
{
path: '**',
loadChildren: () => import('./page-not-found/page-not-found.module').then((m) => m.PageNotFoundModule),
Expand Down
5 changes: 2 additions & 3 deletions udmif/web/src/app/app.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
import { NavigationModule } from './navigation/navigation.module';
import { AppModule } from './app.module';

describe('AppComponent', () => {
let component: AppComponent;
Expand All @@ -10,7 +9,7 @@ describe('AppComponent', () => {

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RouterTestingModule, NavigationModule],
imports: [AppModule],
declarations: [AppComponent],
}).compileComponents();
});
Expand Down
3 changes: 2 additions & 1 deletion udmif/web/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NavigationModule } from './navigation/navigation.module';
import { GraphQLModule } from './graphql/graphql.module';
import { AuthModule } from './auth/auth.module';

@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, AppRoutingModule, BrowserAnimationsModule, NavigationModule, GraphQLModule],
imports: [BrowserModule, AppRoutingModule, BrowserAnimationsModule, NavigationModule, GraphQLModule, AuthModule],
bootstrap: [AppComponent],
})
export class AppModule {}
38 changes: 38 additions & 0 deletions udmif/web/src/app/auth/auth.guard.spec.ts
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));
});
});
24 changes: 24 additions & 0 deletions udmif/web/src/app/auth/auth.guard.ts
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;
}
})
);
}
}
23 changes: 23 additions & 0 deletions udmif/web/src/app/auth/auth.module.ts
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 {}
53 changes: 53 additions & 0 deletions udmif/web/src/app/auth/auth.service.spec.ts
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();
});
});
31 changes: 31 additions & 0 deletions udmif/web/src/app/auth/auth.service.ts
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'}
this.router.navigateByUrl('/devices');
}

async logout(): Promise<void> {
await this.socialAuthService.signOut();
this.router.navigateByUrl('/login');
this.apollo.client.clearStore(); // clear apollo cache
}
}
3 changes: 1 addition & 2 deletions udmif/web/src/app/device/device.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { TestBed } from '@angular/core/testing';
import { ApolloTestingController, ApolloTestingModule } from 'apollo-angular/testing';
import { GraphQLModule } from '../graphql/graphql.module';
import { GET_DEVICE } from './device.gql';
import { DeviceQueryResponse } from './device';
import { DeviceService } from './device.service';
Expand All @@ -11,7 +10,7 @@ describe('DeviceService', () => {

beforeEach(() => {
TestBed.configureTestingModule({
imports: [ApolloTestingModule, GraphQLModule],
imports: [ApolloTestingModule],
});
service = TestBed.inject(DeviceService);
controller = TestBed.inject(ApolloTestingController);
Expand Down
3 changes: 1 addition & 2 deletions udmif/web/src/app/devices/devices.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { TestBed } from '@angular/core/testing';
import { ApolloTestingController, ApolloTestingModule } from 'apollo-angular/testing';
import { GraphQLModule } from '../graphql/graphql.module';
import { GET_DEVICES } from './devices.gql';
import { DevicesQueryResponse } from './devices';
import { DevicesService } from './devices.service';
Expand All @@ -11,7 +10,7 @@ describe('DevicesService', () => {

beforeEach(() => {
TestBed.configureTestingModule({
imports: [ApolloTestingModule, GraphQLModule],
imports: [ApolloTestingModule],
});
service = TestBed.inject(DevicesService);
controller = TestBed.inject(ApolloTestingController);
Expand Down
33 changes: 29 additions & 4 deletions udmif/web/src/app/graphql/graphql.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
};
}
});

return {
link: httpLink.create({ uri }),
link: ApolloLink.from([auth, httpLink.create({ uri })]),
cache: new InMemoryCache(),
};
}
Expand All @@ -18,7 +43,7 @@ export function createApollo(httpLink: HttpLink): ApolloClientOptions<any> {
{
provide: APOLLO_OPTIONS,
useFactory: createApollo,
deps: [HttpLink],
deps: [HttpLink, SocialAuthService],
},
],
})
Expand Down
16 changes: 16 additions & 0 deletions udmif/web/src/app/login/login-routing.module.ts
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 {}
14 changes: 14 additions & 0 deletions udmif/web/src/app/login/login.component.html
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>
Loading

0 comments on commit be525f7

Please sign in to comment.