Skip to content

Use PouchDB for handling auth flow #1515

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

Merged
merged 14 commits into from
Jul 26, 2018
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"mime": "^2.3.1",
"ngx-img": "^10.15.0",
"pouchdb": "^7.0.0",
"pouchdb-authentication": "^1.1.3",
"pouchdb-find": "^7.0.0",
"rxjs": "^6.2.0",
"zone.js": "~0.8.26"
Expand Down
6 changes: 4 additions & 2 deletions src/app/home/home.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { interval, Subject, forkJoin } from 'rxjs';
import { tap, switchMap, takeUntil } from 'rxjs/operators';
import { findDocuments } from '../shared/mangoQueries';
import { debug } from '../debug-operator';
import { PouchAuthService } from '../shared/database';

@Component({
templateUrl: './home.component.html',
Expand Down Expand Up @@ -48,7 +49,8 @@ export class HomeComponent implements OnInit, AfterViewInit, OnDestroy {
constructor(
private couchService: CouchService,
private router: Router,
private userService: UserService
private userService: UserService,
private pouchAuthService: PouchAuthService
) {
this.userService.userChange$.pipe(takeUntil(this.onDestroy$))
.subscribe(() => {
Expand Down Expand Up @@ -116,7 +118,7 @@ export class HomeComponent implements OnInit, AfterViewInit, OnDestroy {

logoutClick() {
this.userService.endSessionLog().pipe(switchMap(() => {
const obsArr = [ this.couchService.delete('_session', { withCredentials: true }) ];
const obsArr = [ this.pouchAuthService.logout() ];
const localAdminName = this.userService.getConfig().adminName.split('@')[0];
if (localAdminName === this.userService.get().name) {
obsArr.push(
Expand Down
63 changes: 30 additions & 33 deletions src/app/login/login-form.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,18 @@ import { PlanetMessageService } from '../shared/planet-message.service';
import { environment } from '../../environments/environment';
import { ValidatorService } from '../validators/validator.service';
import { SyncService } from '../shared/sync.service';
import { PouchAuthService } from '../shared/database';

const registerForm = {
name: [],
password: [ '', Validators.compose([
Validators.required,
CustomValidators.matchPassword('repeatPassword', false)
]) ],
]) ],
repeatPassword: [ '', Validators.compose([
Validators.required,
CustomValidators.matchPassword('password', true)
]) ]
]) ]
};

const loginForm = {
Expand All @@ -42,7 +43,8 @@ export class LoginFormComponent {
private formBuilder: FormBuilder,
private planetMessageService: PlanetMessageService,
private validatorService: ValidatorService,
private syncService: SyncService
private syncService: SyncService,
private pouchAuthService: PouchAuthService
) {
registerForm.name = [ '', [
Validators.required,
Expand Down Expand Up @@ -89,16 +91,22 @@ export class LoginFormComponent {
return this.router.navigateByUrl(this.returnUrl);
}

createUser({ name, password }: {name: string, password: string}) {
this.couchService.put('_users/org.couchdb.user:' + name,
{ 'name': name, 'password': password, 'roles': [], 'type': 'user', 'isUserAdmin': false, joinDate: Date.now() })
.pipe(switchMap(() => {
return this.couchService.put('shelf/org.couchdb.user:' + name, { });
})).subscribe((response: any) => {
this.planetMessageService.showMessage('User created: ' + response.id.replace('org.couchdb.user:', ''));
this.welcomeNotification(response.id);
this.login(this.userForm.value, true);
}, error => this.planetMessageService.showAlert('An error occurred please try again'));
createUser({ name, password }: { name: string, password: string }) {
const metadata = {
isUserAdmin: false,
joinDate: Date.now()
};

this.pouchAuthService.signup(name, password, metadata).pipe(
switchMap(() => this.couchService.put('shelf/org.couchdb.user:' + name, {}))
).subscribe(
res => {
this.planetMessageService.showMessage('User created: ' + res.id.replace('org.couchdb.user:', ''));
this.welcomeNotification(res.id);
this.login(this.userForm.value, true);
},
err => this.planetMessageService.showAlert('An error occurred please try again')
);
}

createParentSession({ name, password }) {
Expand All @@ -107,26 +115,15 @@ export class LoginFormComponent {
{ withCredentials: true, domain: this.userService.getConfig().parentDomain });
}

login({ name, password }: {name: string, password: string}, isCreate: boolean) {
this.couchService.post('_session', { 'name': name, 'password': password }, { withCredentials: true })
.pipe(switchMap((data) => {
// Navigate into app
if (isCreate) {
return from(this.router.navigate( [ 'users/update/' + name ]));
} else {
return from(this.reRoute());
}
}),
switchMap(this.createSession(name, password)),
switchMap((sessionData) => {
if (isCreate) {
const adminName = this.userService.getConfig().adminName.split('@')[0];
return this.sendNotifications(adminName, name);
}
return of(sessionData);
})
).subscribe((res) => {
}, (error) => this.planetMessageService.showAlert('Username and/or password do not match'));
login({ name, password }: { name: string, password: string }, isCreate: boolean) {
this.pouchAuthService.login(name, password).pipe(
switchMap(() => isCreate ? from(this.router.navigate([ 'users/update/' + name ])) : from(this.reRoute())),
switchMap(this.createSession(name, password)),
switchMap((sessionData) => {
const adminName = this.userService.getConfig().adminName.split('@')[0];
return isCreate ? this.sendNotifications(adminName, name) : of(sessionData);
})
).subscribe(() => {}, (error) => this.planetMessageService.showAlert('Username and/or password do not match'));
}

sendNotifications(userName, addedMember) {
Expand Down
34 changes: 17 additions & 17 deletions src/app/shared/auth-guard.service.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { Injectable } from '@angular/core';
import { CouchService } from './couchdb.service';
import { UserService } from './user.service';
import { Observable, of } from 'rxjs';
import { switchMap, map } from 'rxjs/operators';

import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { PouchAuthService } from './database';

@Injectable()
export class AuthService {

constructor(private couchService: CouchService, private userService: UserService, private router: Router) {}
constructor(private userService: UserService, private router: Router,
private pouchAuthService: PouchAuthService) { }

private checkUser(url: any): Observable<boolean> {
return this.couchService
.get('_session', { withCredentials: true })
.pipe(switchMap((res: any) => {
return this.pouchAuthService.getSessionInfo().pipe(
switchMap(res => {
if (res.userCtx.name) {
// If user already matches one on the user service, do not make additional call to CouchDB
if (res.userCtx.name === this.userService.get().name) {
Expand All @@ -26,9 +25,8 @@ export class AuthService {
this.router.navigate([ '/login' ], { queryParams: { returnUrl: url }, replaceUrl: true });
return of(false);
}),
map((isLoggedIn) => {
return isLoggedIn;
}));
map(isLoggedIn => isLoggedIn)
);
}

// For main app (which requires login). Uses canActivateChild to check on every route
Expand All @@ -39,13 +37,15 @@ export class AuthService {

// For login route will redirect to main app if there is an active session
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
return this.couchService.get('_session', { withCredentials: true }).pipe(map(res => {
if (res.userCtx.name) {
this.router.navigate([ '' ]);
return false;
}
return true;
}));
return this.pouchAuthService.getSessionInfo().pipe(
map(res => {
if (res.userCtx.name) {
this.router.navigate([ '' ]);
return false;
}
return true;
})
);
}

}
4 changes: 3 additions & 1 deletion src/app/shared/database/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { PouchService } from './pouch.service';
import { PouchAuthService } from './pouch-auth.service';

export { PouchService } from './pouch.service';
export const SHARED_SERVICES = [ PouchService ];
export { PouchAuthService } from './pouch-auth.service';
export const SHARED_SERVICES = [ PouchService, PouchAuthService ];
48 changes: 48 additions & 0 deletions src/app/shared/database/pouch-auth.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Injectable, Inject } from '@angular/core';
import { from, throwError, Observable } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { PouchService } from './pouch.service';

interface SessionInfo {
userCtx: {
name: String;
roles: String[];
};
}
@Injectable()
export class PouchAuthService {
private authDB;

constructor(private pouchService: PouchService) {
this.authDB = this.pouchService.getAuthDB();
}

getSessionInfo(): Observable<SessionInfo> {
return from(this.authDB.getSession()).pipe(
catchError(this.handleError)
) as Observable<SessionInfo>;
}

login(username, password) {
return from(this.authDB.logIn(username, password)).pipe(
catchError(this.handleError)
);
}

signup(username, password, metadata?) {
return from(this.authDB.signUp(username, password, { metadata })).pipe(
catchError(this.handleError)
);
}

logout() {
return from(this.authDB.logOut()).pipe(
catchError(this.handleError)
);
}

private handleError(err) {
console.error('An error occured while signing in the user', err);
return throwError(err.message || err);
}
}
16 changes: 16 additions & 0 deletions src/app/shared/database/pouch.service.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { Injectable } from '@angular/core';
import PouchDB from 'pouchdb';
import PouchDBAuth from 'pouchdb-authentication';
import PouchDBFind from 'pouchdb-find';
import { throwError, from } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { environment } from '../../../environments/environment';

PouchDB.plugin(PouchDBAuth);
PouchDB.plugin(PouchDBFind);

@Injectable()
export class PouchService {
private baseUrl = environment.couchAddress;
private localDBs;
private authDB;
private databases = [];

constructor() {
Expand All @@ -25,6 +28,15 @@ export class PouchService {
});
this.localDBs[db] = pouchDB;
});

// test is a placeholder temp database
// we need a central remote database
// since we will have different levels of authentication (manager, intern)
// we will have to create corresponding documents in couchdb and we can sync
// we can decide that when the user is being created for the first time?
this.authDB = new PouchDB(this.baseUrl + 'test', {
skip_setup: true
});
}

// @TODO: handle edge cases like offline, duplicate, duplications
Expand Down Expand Up @@ -61,6 +73,10 @@ export class PouchService {
return this.localDBs[db];
}

getAuthDB() {
return this.authDB;
}

private handleError(err) {
console.error('An error occurred in PouchDB', err);
return throwError(err.message || err);
Expand Down