Skip to content
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

Merged
merged 9 commits into from
Apr 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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'

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?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll see

Copy link
Author

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?

Copy link
Author

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

),
},
],
} 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'}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this TODO still valid?

Copy link
Author

Choose a reason for hiding this comment

The 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
}
}
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,

Choose a reason for hiding this comment

The 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?

Copy link
Author

Choose a reason for hiding this comment

The 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(),
};
}
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