Add auth, nav, and home page

This commit is contained in:
Daniel J. Summers 2020-06-13 08:42:16 -05:00
parent b1e2ff4813
commit d678707dc5
18 changed files with 393 additions and 12 deletions

View File

@ -500,6 +500,20 @@
"resolved": "https://registry.npmjs.org/@angular/router/-/router-9.1.9.tgz", "resolved": "https://registry.npmjs.org/@angular/router/-/router-9.1.9.tgz",
"integrity": "sha512-4u+CWMPB4hCkAsFCEzC94YEWT0wVozqGkc/Dortt2hFaqvZpIegg6iJVZlDxuyDjzFYBPnnbTDdgiTTA8ckfuA==" "integrity": "sha512-4u+CWMPB4hCkAsFCEzC94YEWT0wVozqGkc/Dortt2hFaqvZpIegg6iJVZlDxuyDjzFYBPnnbTDdgiTTA8ckfuA=="
}, },
"@auth0/auth0-spa-js": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@auth0/auth0-spa-js/-/auth0-spa-js-1.9.0.tgz",
"integrity": "sha512-p+9m13k8MMpIthoWso7pXxaEZq5tAon8GPUPU+Wu4hgDQyJZIzqtCMHcQT0qdzfK/KoZwmoifj4aOvJxKO9I4Q==",
"requires": {
"abortcontroller-polyfill": "^1.4.0",
"browser-tabs-lock": "^1.2.8",
"core-js": "^3.6.4",
"es-cookie": "^1.3.2",
"fast-text-encoding": "^1.0.1",
"promise-polyfill": "^8.1.3",
"unfetch": "^4.1.0"
}
},
"@babel/code-frame": { "@babel/code-frame": {
"version": "7.10.1", "version": "7.10.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz",
@ -1940,6 +1954,11 @@
"through": ">=2.2.7 <3" "through": ">=2.2.7 <3"
} }
}, },
"abortcontroller-polyfill": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/abortcontroller-polyfill/-/abortcontroller-polyfill-1.4.0.tgz",
"integrity": "sha512-3ZFfCRfDzx3GFjO6RAkYx81lPGpUS20ISxux9gLxuKnqafNcFQo59+IoZqpO2WvQlyc287B62HDnDdNYRmlvWA=="
},
"accepts": { "accepts": {
"version": "1.3.7", "version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
@ -2617,6 +2636,11 @@
"integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
"dev": true "dev": true
}, },
"browser-tabs-lock": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/browser-tabs-lock/-/browser-tabs-lock-1.2.8.tgz",
"integrity": "sha512-Xrj33YUTltPDoGrD1KnaAn5ZuxnnlJFcIW9srVTPHbMNPd9MlcnBCWaGV0STlvGKu8Ok0ad5qxyx5sIwFTr/Ig=="
},
"browserify-aes": { "browserify-aes": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
@ -3520,8 +3544,7 @@
"core-js": { "core-js": {
"version": "3.6.4", "version": "3.6.4",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.4.tgz", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.4.tgz",
"integrity": "sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw==", "integrity": "sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw=="
"dev": true
}, },
"core-js-compat": { "core-js-compat": {
"version": "3.6.5", "version": "3.6.5",
@ -4535,6 +4558,11 @@
"string.prototype.trimright": "^2.1.1" "string.prototype.trimright": "^2.1.1"
} }
}, },
"es-cookie": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/es-cookie/-/es-cookie-1.3.2.tgz",
"integrity": "sha512-UTlYYhXGLOy05P/vKVT2Ui7WtC7NiRzGtJyAKKn32g5Gvcjn7KAClLPWlipCtxIus934dFg9o9jXiBL0nP+t9Q=="
},
"es-to-primitive": { "es-to-primitive": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
@ -4906,6 +4934,11 @@
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"dev": true "dev": true
}, },
"fast-text-encoding": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.2.tgz",
"integrity": "sha512-5rQdinSsycpzvAoHga2EDn+LRX1d5xLFsuNG0Kg61JrAT/tASXcLL0nf/33v+sAxlQcfYmWbTURa1mmAf55jGw=="
},
"fastparse": { "fastparse": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz",
@ -9419,6 +9452,11 @@
"integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=",
"dev": true "dev": true
}, },
"promise-polyfill": {
"version": "8.1.3",
"resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.1.3.tgz",
"integrity": "sha512-MG5r82wBzh7pSKDRa9y+vllNHz3e3d4CNj1PQE4BQYxLme0gKYYBm9YENq+UkEikyZ0XbiGWxYlVw3Rl9O/U8g=="
},
"promise-retry": { "promise-retry": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-1.1.1.tgz", "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-1.1.1.tgz",
@ -12078,6 +12116,11 @@
"integrity": "sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ==", "integrity": "sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ==",
"dev": true "dev": true
}, },
"unfetch": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.1.0.tgz",
"integrity": "sha512-crP/n3eAPUJxZXM9T80/yv0YhkTEx2K1D3h7D1AJM6fzsWZrxdyRuLN0JH/dkZh1LNH8LxCnBzoPFCPbb2iGpg=="
},
"unicode-canonical-property-names-ecmascript": { "unicode-canonical-property-names-ecmascript": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",

View File

@ -21,6 +21,7 @@
"@angular/platform-browser": "~9.1.9", "@angular/platform-browser": "~9.1.9",
"@angular/platform-browser-dynamic": "~9.1.9", "@angular/platform-browser-dynamic": "~9.1.9",
"@angular/router": "~9.1.9", "@angular/router": "~9.1.9",
"@auth0/auth0-spa-js": "^1.9.0",
"rxjs": "~6.5.4", "rxjs": "~6.5.4",
"tslib": "^1.10.0", "tslib": "^1.10.0",
"zone.js": "~0.10.2" "zone.js": "~0.10.2"

View File

@ -1,8 +1,10 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router'; import { Routes, RouterModule } from '@angular/router'
import { HomeComponent } from './pages/home/home.component'
const routes: Routes = [
const routes: Routes = []; { path: '', component: HomeComponent }
]
@NgModule({ @NgModule({
imports: [RouterModule.forRoot(routes)], imports: [RouterModule.forRoot(routes)],

View File

@ -5,10 +5,12 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
import { AppRoutingModule } from './app-routing.module' import { AppRoutingModule } from './app-routing.module'
import { AppComponent } from './app.component' import { AppComponent } from './app.component'
import { SharedModule } from './shared/shared.module' import { SharedModule } from './shared/shared.module'
import { HomeComponent } from './pages/home/home.component'
@NgModule({ @NgModule({
declarations: [ declarations: [
AppComponent AppComponent,
HomeComponent
], ],
imports: [ imports: [
BrowserModule, BrowserModule,

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing'
import { AuthService } from './auth.service'
describe('AuthService', () => {
let service: AuthService
beforeEach(() => {
TestBed.configureTestingModule({})
service = TestBed.inject(AuthService)
})
it('should be created', () => {
expect(service).toBeTruthy()
})
})

View File

@ -0,0 +1,126 @@
import { Injectable } from '@angular/core'
import { Router } from '@angular/router'
import createAuth0Client, { GetUserOptions } from '@auth0/auth0-spa-js'
import Auth0Client from '@auth0/auth0-spa-js/dist/typings/Auth0Client'
import { BehaviorSubject, combineLatest, from, Observable, of, throwError } from 'rxjs'
import { catchError, concatMap, shareReplay, tap } from 'rxjs/operators'
@Injectable({
providedIn: 'root'
})
export class AuthService {
// Create an observable of Auth0 instance of client
auth0Client$ = (from(
createAuth0Client({
domain: "djs-consulting.auth0.com",
client_id: "Of2s0RQCQ3mt3dwIkOBY5h85J9sXbF2n",
redirect_uri: `${window.location.origin}`
})
) as Observable<Auth0Client>).pipe(
shareReplay(1), // Every subscription receives the same shared value
catchError(err => throwError(err))
)
// Define observables for SDK methods that return promises by default
// For each Auth0 SDK method, first ensure the client instance is ready
// concatMap: Using the client instance, call SDK method; SDK returns a promise
// from: Convert that resulting promise into an observable
isAuthenticated$ = this.auth0Client$.pipe(
concatMap((client: Auth0Client) => from(client.isAuthenticated())),
tap(res => this.loggedIn = res)
)
handleRedirectCallback$ = this.auth0Client$.pipe(
concatMap((client: Auth0Client) => from(client.handleRedirectCallback()))
)
// Create subject and public observable of user profile data
private userProfileSubject$ = new BehaviorSubject<any>(null)
userProfile$ = this.userProfileSubject$.asObservable()
// Create a local property for login status
loggedIn = false
constructor(private router: Router) {
// On initial load, check authentication state with authorization server
// Set up local auth streams if user is already authenticated
this.localAuthSetup()
// Handle redirect from Auth0 login
this.handleAuthCallback()
}
// When calling, options can be passed if desired
// https://auth0.github.io/auth0-spa-js/classes/auth0client.html#getuser
getUser$(options?: GetUserOptions): Observable<any> {
return this.auth0Client$.pipe(
concatMap((client: Auth0Client) => from(client.getUser(options))),
tap(user => this.userProfileSubject$.next(user))
)
}
private localAuthSetup() {
// This should only be called on app initialization
// Set up local authentication streams
const checkAuth$ = this.isAuthenticated$.pipe(
concatMap((loggedIn: boolean) => {
if (loggedIn) {
// If authenticated, get user and set in app
// NOTE: you could pass options here if needed
return this.getUser$()
}
// If not authenticated, return stream that emits 'false'
return of(loggedIn)
})
)
checkAuth$.subscribe()
}
login(redirectPath: string = '/') {
// A desired redirect path can be passed to login method
// (e.g., from a route guard)
// Ensure Auth0 client instance exists
this.auth0Client$.subscribe((client: Auth0Client) => {
// Call method to log in
client.loginWithRedirect({
redirect_uri: `${window.location.origin}`,
appState: { target: redirectPath }
})
})
}
private handleAuthCallback() {
// Call when app reloads after user logs in with Auth0
const params = window.location.search
if (params.includes('code=') && params.includes('state=')) {
let targetRoute: string // Path to redirect to after login processsed
const authComplete$ = this.handleRedirectCallback$.pipe(
// Have client, now call method to handle auth callback redirect
tap(cbRes => {
// Get and set target redirect route from callback results
targetRoute = cbRes.appState && cbRes.appState.target ? cbRes.appState.target : '/';
}),
concatMap(() => {
// Redirect callback complete; get user and login status
return combineLatest([
this.getUser$(),
this.isAuthenticated$
])
})
)
// Subscribe to authentication completion observable
// Response will be an array of user and login status
authComplete$.subscribe(([user, loggedIn]) => {
// Redirect to target route after callback processing
this.router.navigate([targetRoute])
})
}
}
logout() {
// Ensure Auth0 client instance exists
this.auth0Client$.subscribe((client: Auth0Client) => {
// Call method to log out
client.logout({
client_id: "Of2s0RQCQ3mt3dwIkOBY5h85J9sXbF2n",
returnTo: `${window.location.origin}`
})
})
}
}

View File

@ -0,0 +1,13 @@
<main role="main" class="mpj-main-content">
<p>&nbsp;</p>
<p>
myPrayerJournal is a place where individuals can record their prayer requests, record that they prayed for them,
update them as God moves in the situation, and record a final answer received on that request. It also allows
individuals to review their answered prayers.
</p>
<p>
This site is open and available to the general public. To get started, simply click the &ldquo;Log On&rdquo; link
above, and log on with either a Microsoft or Google account. You can also learn more about the site at the
&ldquo;Docs&rdquo; link, also above.
</p>
</main>

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing'
import { HomeComponent } from './home.component'
describe('HomeComponent', () => {
let component: HomeComponent
let fixture: ComponentFixture<HomeComponent>
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ HomeComponent ]
})
.compileComponents()
}))
beforeEach(() => {
fixture = TestBed.createComponent(HomeComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should create', () => {
expect(component).toBeTruthy()
})
})

View File

@ -0,0 +1,15 @@
import { Component } from '@angular/core'
/**
* Home Page.
*
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @version 3
*/
@Component({
selector: 'app-home',
templateUrl: './home.component.html'
})
export class HomeComponent {
constructor() { }
}

View File

@ -2,12 +2,22 @@ import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { MatToolbarModule } from '@angular/material/toolbar' import { MatToolbarModule } from '@angular/material/toolbar'
import { NavigationComponent } from './ui/navigation/navigation.component'
import { TopHeaderComponent } from './ui/top-header/top-header.component' import { TopHeaderComponent } from './ui/top-header/top-header.component'
/**
* myPrayerJournal Shared Module.
*
* This module contains UI components designed to be used throughout the application.
*
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @version 3
*/
@NgModule({ @NgModule({
declarations: [TopHeaderComponent], declarations: [
NavigationComponent,
TopHeaderComponent
],
imports: [ imports: [
CommonModule, CommonModule,
MatToolbarModule MatToolbarModule

View File

@ -0,0 +1,9 @@
<ng-container *ngIf="auth.loggedIn">
<a routerLink="/journal">Journal</a>
<a routerLink="/requests/active">Active</a>
<a *ngIf="hasSnoozed" routerLink="/requests/snoozed">Snoozed</a>
<a routerLink="/requests/answered">Answered</a>
<a href="/user/log-off" (click)="logOff($event)">Log Off</a>
</ng-container>
<a *ngIf="!auth.loggedIn" href="/user/log-on" (click)="logOn($event)">Log On</a>
<a href="https://docs.prayerjournal.me" target="_blank">Docs</a>

View File

@ -0,0 +1,11 @@
a,
a:link,
a:visited
color: white
font-size: .9rem
text-transform: uppercase
text-decoration: none
padding: 0 1rem
a:hover
border-bottom: solid 1px white

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NavigationComponent } from './navigation.component';
describe('NavigationComponent', () => {
let component: NavigationComponent;
let fixture: ComponentFixture<NavigationComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ NavigationComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(NavigationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,41 @@
import { Component } from '@angular/core'
import { AuthService } from 'src/app/auth.service'
/**
* myPrayerJournal Navigation.
*
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @version 3
*/
@Component({
selector: 'app-navigation',
templateUrl: './navigation.component.html',
styleUrls: ['./navigation.component.sass']
})
export class NavigationComponent {
// TODO: get this from the store's state
hasSnoozed = false
constructor(public auth: AuthService) { }
/**
* Start the log on process.
*
* @param e The click event (used to stop the default action)
*/
logOn(e: Event) {
e.preventDefault()
this.auth.login()
}
/**
* Log the user off.
*
* @param e The click event (used to stop the default action)
*/
logOff(e: Event) {
e.preventDefault()
this.auth.logout()
}
}

View File

@ -6,6 +6,6 @@
</a> </a>
</span> </span>
<span class="spacer"></span> <span class="spacer"></span>
<span>Links go here</span> <app-navigation></app-navigation>
</mat-toolbar-row> </mat-toolbar-row>
</mat-toolbar> </mat-toolbar>

View File

@ -1,5 +1,11 @@
import { Component, OnInit } from '@angular/core' import { Component, OnInit } from '@angular/core'
/**
* myPrayerJournal Top Header.
*
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @version 3
*/
@Component({ @Component({
selector: 'app-top-header', selector: 'app-top-header',
templateUrl: './top-header.component.html', templateUrl: './top-header.component.html',

View File

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>MyPrayerJournal</title> <title>myPrayerJournal</title>
<base href="/"> <base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="icon" type="image/x-icon" href="favicon.ico">

View File

@ -1 +1,37 @@
/* You can add global styles to this file, and also import other style files */ p
margin-bottom: 0
footer
border-top: solid 1px lightgray
margin: 1rem -1rem 0
padding: 0 1rem
footer p
margin: 0
.mpj-full-page-card
font-size: 1rem
line-height: 1.25rem
.mpj-main-content
max-width: 61rem
padding: 0 .5rem
margin: auto
.mpj-request-text
white-space: pre-line
p.mpj-request-text
margin-top: 0
.mpj-text-center
text-align: center
.mpj-text-nowrap
white-space: nowrap
.mpj-text-right
text-align: right
.mpj-muted-text
color: rgba(0, 0, 0, .6)
.mpj-valign-top
vertical-align: top
.mpj-narrow
max-width: 40rem
margin: auto
.mpj-skinny
max-width: 20rem
margin: auto
.mpj-full-width
width: 100%