Skip to content

Commit

Permalink
Implemented login method selection in UI.
Browse files Browse the repository at this point in the history
List of allowed login methods is specified via runtime config
  • Loading branch information
zaychenko-sergei committed Aug 28, 2023
1 parent da23dae commit f3cea99
Show file tree
Hide file tree
Showing 26 changed files with 655 additions and 109 deletions.
7 changes: 5 additions & 2 deletions images/kamu-web-ui/runtime-config.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
{
"apiServerGqlUrl": "http://localhost:8080/graphql",
"featureFlags": {
"enableLogin": true,
"enableLogout": true
"enableLogout": true,
"loginMethods": [
"password",
"oauth_github"
]
}
}
3 changes: 0 additions & 3 deletions src/app/api/auth.api.model.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
export const LOGIN_METHOD_PASSWORD = "password";
export const LOGIN_METHOD_GITHUB = "oauth_github";

export interface PasswordLoginCredentials {
login: string;
password: string;
Expand Down
30 changes: 18 additions & 12 deletions src/app/api/auth.api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,8 @@ import {
} from "./mock/auth.mock";
import { AuthenticationError } from "../common/errors";
import { first } from "rxjs/operators";
import {
GithubLoginCredentials,
LOGIN_METHOD_GITHUB,
LOGIN_METHOD_PASSWORD,
PasswordLoginCredentials,
} from "./auth.api.model";
import { GithubLoginCredentials, PasswordLoginCredentials } from "./auth.api.model";
import { LoginMethod } from "../app-config.model";

describe("AuthApi", () => {
let service: AuthApi;
Expand Down Expand Up @@ -52,12 +48,14 @@ describe("AuthApi", () => {
}

function loginFullyViaGithub(): void {
service.fetchUserInfoAndTokenFromGithubCallackCode(TEST_GITHUB_CODE).subscribe();
service
.fetchUserInfoAndTokenFromGithubCallackCode({ code: TEST_GITHUB_CODE } as GithubLoginCredentials)
.subscribe();

const expectedCredentials: GithubLoginCredentials = { code: TEST_GITHUB_CODE };

const op = controller.expectOne(LoginDocument);
expect(op.operation.variables.login_method).toEqual(LOGIN_METHOD_GITHUB);
expect(op.operation.variables.login_method).toEqual(LoginMethod.GITHUB);
expect(op.operation.variables.login_credentials_json).toEqual(JSON.stringify(expectedCredentials));

op.flush({
Expand All @@ -66,12 +64,17 @@ describe("AuthApi", () => {
}

function loginFullyViaPassword(): void {
service.fetchUserInfoAndTokenFromPasswordLogin(TEST_LOGIN, TEST_PASSWORD).subscribe();
service
.fetchUserInfoAndTokenFromPasswordLogin({
login: TEST_LOGIN,
password: TEST_PASSWORD,
} as PasswordLoginCredentials)
.subscribe();

const expectedCredentials: PasswordLoginCredentials = { login: TEST_LOGIN, password: TEST_PASSWORD };

const op = controller.expectOne(LoginDocument);
expect(op.operation.variables.login_method).toEqual(LOGIN_METHOD_PASSWORD);
expect(op.operation.variables.login_method).toEqual(LoginMethod.PASSWORD);
expect(op.operation.variables.login_credentials_json).toEqual(JSON.stringify(expectedCredentials));

op.flush({
Expand Down Expand Up @@ -108,7 +111,10 @@ describe("AuthApi", () => {

it("should check full login password failure", fakeAsync(() => {
const subscription$ = service
.fetchUserInfoAndTokenFromPasswordLogin(TEST_LOGIN, TEST_PASSWORD)
.fetchUserInfoAndTokenFromPasswordLogin({
login: TEST_LOGIN,
password: TEST_PASSWORD,
} as PasswordLoginCredentials)
.pipe(first())
.subscribe({
next: () => fail("Unexpected success"),
Expand Down Expand Up @@ -150,7 +156,7 @@ describe("AuthApi", () => {

it("should check full login Github failure", fakeAsync(() => {
const subscription$ = service
.fetchUserInfoAndTokenFromGithubCallackCode(TEST_GITHUB_CODE)
.fetchUserInfoAndTokenFromGithubCallackCode({ code: TEST_GITHUB_CODE } as GithubLoginCredentials)
.pipe(first())
.subscribe({
next: () => fail("Unexpected success"),
Expand Down
18 changes: 6 additions & 12 deletions src/app/api/auth.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,8 @@ import {
} from "./kamu.graphql.interface";
import { MutationResult } from "apollo-angular";
import { AuthenticationError } from "../common/errors";
import {
GithubLoginCredentials,
LOGIN_METHOD_GITHUB,
LOGIN_METHOD_PASSWORD,
PasswordLoginCredentials,
} from "./auth.api.model";
import { GithubLoginCredentials, PasswordLoginCredentials } from "./auth.api.model";
import { LoginMethod } from "../app-config.model";

@Injectable({
providedIn: "root",
Expand All @@ -34,14 +30,12 @@ export class AuthApi {
return this.accountChanged$.asObservable();
}

public fetchUserInfoAndTokenFromPasswordLogin(login: string, password: string): Observable<void> {
const passwordCredentials: PasswordLoginCredentials = { login, password };
return this.fetchUserInfoAndTokenFromLoginMethod(LOGIN_METHOD_PASSWORD, JSON.stringify(passwordCredentials));
public fetchUserInfoAndTokenFromPasswordLogin(credentials: PasswordLoginCredentials): Observable<void> {
return this.fetchUserInfoAndTokenFromLoginMethod(LoginMethod.PASSWORD, JSON.stringify(credentials));
}

public fetchUserInfoAndTokenFromGithubCallackCode(code: string): Observable<void> {
const githubCredentials: GithubLoginCredentials = { code };
return this.fetchUserInfoAndTokenFromLoginMethod(LOGIN_METHOD_GITHUB, JSON.stringify(githubCredentials));
public fetchUserInfoAndTokenFromGithubCallackCode(credentials: GithubLoginCredentials): Observable<void> {
return this.fetchUserInfoAndTokenFromLoginMethod(LoginMethod.GITHUB, JSON.stringify(credentials));
}

public fetchUserInfoAndTokenFromLoginMethod(loginMethod: string, loginCredentialsJson: string): Observable<void> {
Expand Down
6 changes: 3 additions & 3 deletions src/app/api/mock/auth.mock.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { GraphQLError } from "graphql";
import { AccountDetailsFragment, FetchAccountInfoMutation, LoginMutation } from "../kamu.graphql.interface";
import AppValues from "src/app/common/app.values";
import { AppConfigLoginInstructions } from "src/app/app-config.model";
import { LOGIN_METHOD_PASSWORD, PasswordLoginCredentials } from "../auth.api.model";
import { AppConfigLoginInstructions, LoginMethod } from "src/app/app-config.model";
import { PasswordLoginCredentials } from "../auth.api.model";

export const TEST_GITHUB_CODE = "12345";
export const TEST_ACCESS_TOKEN_GITHUB = "someTokenViaGithub";
Expand All @@ -17,7 +17,7 @@ const mockPasswordLoginCredentials: PasswordLoginCredentials = {
};

export const mockLoginInstructions: AppConfigLoginInstructions = {
loginMethod: LOGIN_METHOD_PASSWORD,
loginMethod: LoginMethod.PASSWORD,
loginCredentialsJson: JSON.stringify(mockPasswordLoginCredentials),
};

Expand Down
7 changes: 6 additions & 1 deletion src/app/app-config.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ export interface AppConfigLoginInstructions {
loginCredentialsJson: string;
}

export enum LoginMethod {
PASSWORD = "password",
GITHUB = "oauth_github",
}

export interface AppConfigFeatureFlags {
enableLogin: boolean;
enableLogout: boolean;
loginMethods: LoginMethod[];
}
2 changes: 1 addition & 1 deletion src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const routes: Routes = [
{
path: ProjectLinks.URL_LOGIN,
component: LoginComponent,
canLoad: [LoginGuard],
canActivate: [LoginGuard],
},
{
path: ProjectLinks.URL_SEARCH,
Expand Down
11 changes: 5 additions & 6 deletions src/app/app-routing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import { ApolloTestingModule } from "apollo-angular/testing";
import { NO_ERRORS_SCHEMA } from "@angular/core";
import { LoggedUserService } from "./auth/logged-user.service";
import { AppConfigService } from "./app-config.service";
import { LoginService } from "./auth/login/login.service";
import { PageNotFoundComponent } from "./components/page-not-found/page-not-found.component";
import { LoginComponent } from "./auth/login/login.component";

describe("Router", () => {
let router: Router;
Expand All @@ -22,7 +22,7 @@ describe("Router", () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RouterTestingModule.withRoutes(routes), ApolloTestingModule],
declarations: [PageNotFoundComponent],
declarations: [PageNotFoundComponent, LoginComponent],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();

Expand Down Expand Up @@ -90,8 +90,8 @@ describe("Router", () => {
describe("#login routes", () => {
it("login redirects to default page when not allowed in configuration", fakeAsync(() => {
spyOnProperty(appConfigService, "featureFlags", "get").and.returnValue({
enableLogin: false,
enableLogout: true,
loginMethods: [],
});

promiseWithCatch(router.navigate([ProjectLinks.URL_LOGIN]));
Expand All @@ -111,14 +111,13 @@ describe("Router", () => {
flush();
}));

it("login initiates GitHub redirect when allowed and not logged in", fakeAsync(() => {
it("login opens Login component when allowed and not logged in", fakeAsync(() => {
spyOnProperty(loggedUserService, "isAuthenticated", "get").and.returnValue(false);
const gotoGithubSpy = spyOn(LoginService, "gotoGithub");

promiseWithCatch(router.navigate([ProjectLinks.URL_LOGIN]));
tick();

expect(gotoGithubSpy).toHaveBeenCalledWith();
expect(location.path()).toBe("/" + ProjectLinks.URL_LOGIN);
flush();
}));
});
Expand Down
4 changes: 2 additions & 2 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import _ from "lodash";
import { isMobileView, promiseWithCatch } from "./common/app.helpers";
import { AppConfigService } from "./app-config.service";
import { LoggedUserService } from "./auth/logged-user.service";
import { AppConfigFeatureFlags } from "./app-config.model";
import { AppConfigFeatureFlags, LoginMethod } from "./app-config.model";

export const ALL_URLS_WITHOUT_HEADER: string[] = [ProjectLinks.URL_LOGIN, ProjectLinks.URL_GITHUB_CALLBACK];

Expand All @@ -32,8 +32,8 @@ export class AppComponent extends BaseComponent implements OnInit {
name: AppValues.DEFAULT_USERNAME,
};
public static readonly DEFAULT_FEATURE_FLAGS: AppConfigFeatureFlags = {
enableLogin: true,
enableLogout: true,
loginMethods: [LoginMethod.GITHUB, LoginMethod.PASSWORD],
};

public readonly APP_LOGO = `/${AppValues.APP_LOGO}`;
Expand Down
3 changes: 2 additions & 1 deletion src/app/auth/github-callback/github.callback.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ActivatedRoute } from "@angular/router";
import { of } from "rxjs";
import { AuthApi } from "src/app/api/auth.api";
import { GithubCallbackComponent } from "./github.callback";
import { GithubLoginCredentials } from "src/app/api/auth.api.model";

describe("GithubCallbackComponent", () => {
let component: GithubCallbackComponent;
Expand Down Expand Up @@ -47,7 +48,7 @@ describe("GithubCallbackComponent", () => {
).and.returnValue(of(undefined));
component.ngOnInit();

expect(fetchUserInfoAndTokenSpy).toHaveBeenCalledWith(GITHUB_TEST_CODE);
expect(fetchUserInfoAndTokenSpy).toHaveBeenCalledWith({ code: GITHUB_TEST_CODE } as GithubLoginCredentials);
expect(navigateToHomeSpy).toHaveBeenCalledWith();
});
});
13 changes: 8 additions & 5 deletions src/app/auth/github-callback/github.callback.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import { NavigationService } from "./../../services/navigation.service";
import { ChangeDetectionStrategy, Component, OnInit } from "@angular/core";
import { ActivatedRoute, Params } from "@angular/router";
import { AuthApi } from "../../api/auth.api";
import { BaseComponent } from "src/app/common/base.component";
import { LoginService } from "../login/login.service";
import { GithubLoginCredentials } from "src/app/api/auth.api.model";

@Component({
selector: "app-github-callback",
template: "",
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class GithubCallbackComponent extends BaseComponent implements OnInit {
constructor(private route: ActivatedRoute, private navigationService: NavigationService, private authApi: AuthApi) {
constructor(
private route: ActivatedRoute,
private navigationService: NavigationService,
private loginService: LoginService,
) {
super();
}

Expand All @@ -20,9 +25,7 @@ export class GithubCallbackComponent extends BaseComponent implements OnInit {
}
this.trackSubscription(
this.route.queryParams.subscribe((param: Params) => {
this.authApi
.fetchUserInfoAndTokenFromGithubCallackCode(param.code as string)
.subscribe(() => this.navigationService.navigateToHome());
this.loginService.githubLogin({ code: param.code as string } as GithubLoginCredentials);
}),
);
}
Expand Down
17 changes: 10 additions & 7 deletions src/app/auth/guards/login.guard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { ApolloTestingModule } from "apollo-angular/testing";
import { LoginGuard } from "./login.guard";
import { AppConfigService } from "src/app/app-config.service";
import ProjectLinks from "src/app/project-links";
import { AppConfigFeatureFlags } from "src/app/app-config.model";
import { AppConfigFeatureFlags, LoginMethod } from "src/app/app-config.model";
import { ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router";

describe("LoginGuard", () => {
let guard: LoginGuard;
Expand All @@ -30,30 +31,32 @@ describe("LoginGuard", () => {
[
{
route: ProjectLinks.URL_LOGIN,
featureFlags: { enableLogin: true, enableLogout: true },
featureFlags: { enableLogout: true, loginMethods: [LoginMethod.PASSWORD] },
expectedResult: true,
},
{
route: ProjectLinks.URL_LOGIN,
featureFlags: { enableLogin: false, enableLogout: true },
featureFlags: { enableLogout: true, loginMethods: [] },
expectedResult: false,
},
{
route: ProjectLinks.URL_SEARCH,
featureFlags: { enableLogin: true, enableLogout: true },
featureFlags: { enableLogout: true, loginMethods: [LoginMethod.PASSWORD] },
expectedResult: true,
},
{
route: ProjectLinks.URL_SEARCH,
featureFlags: { enableLogin: false, enableLogout: true },
featureFlags: { enableLogout: true, loginMethods: [] },
expectedResult: true,
},
].forEach((testCase: TestCase) => {
it(`should check route ${testCase.route} with login ${
testCase.featureFlags.enableLogin ? "enabled" : "disabled"
testCase.featureFlags.loginMethods.length > 0 ? "enabled" : "disabled"
}`, () => {
spyOnProperty(appConfigService, "featureFlags", "get").and.returnValue(testCase.featureFlags);
const result = guard.canLoad({ path: testCase.route });
const result = guard.canActivate(new ActivatedRouteSnapshot(), {
url: "/" + testCase.route,
} as RouterStateSnapshot);
expect(result).toEqual(testCase.expectedResult);
});
});
Expand Down
11 changes: 6 additions & 5 deletions src/app/auth/guards/login.guard.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
import { NavigationService } from "src/app/services/navigation.service";
import { Injectable } from "@angular/core";
import { CanLoad, Route } from "@angular/router";
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from "@angular/router";
import { AppConfigService } from "src/app/app-config.service";
import ProjectLinks from "src/app/project-links";
import { LoggedUserService } from "../logged-user.service";

@Injectable({
providedIn: "root",
})
export class LoginGuard implements CanLoad {
export class LoginGuard implements CanActivate {
constructor(
private navigationService: NavigationService,
private appConfigService: AppConfigService,
private loggedUserService: LoggedUserService,
) {}

public canLoad(route: Route): boolean {
if (route.path === ProjectLinks.URL_LOGIN && !this.canLogin()) {
public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
// URLs start from /
if (state.url.slice(1) === ProjectLinks.URL_LOGIN && !this.canLogin()) {
this.navigationService.navigateToHome();
return false;
}
return true;
}

private canLogin(): boolean {
return this.appConfigService.featureFlags.enableLogin && !this.loggedUserService.isAuthenticated;
return this.appConfigService.featureFlags.loginMethods.length > 0 && !this.loggedUserService.isAuthenticated;
}
}
Loading

0 comments on commit f3cea99

Please sign in to comment.