diff --git a/images/kamu-web-ui/runtime-config.json b/images/kamu-web-ui/runtime-config.json index 611452b1c..0a79abcee 100644 --- a/images/kamu-web-ui/runtime-config.json +++ b/images/kamu-web-ui/runtime-config.json @@ -1,7 +1,10 @@ { "apiServerGqlUrl": "http://localhost:8080/graphql", "featureFlags": { - "enableLogin": true, - "enableLogout": true + "enableLogout": true, + "loginMethods": [ + "password", + "oauth_github" + ] } } \ No newline at end of file diff --git a/src/app/api/auth.api.model.ts b/src/app/api/auth.api.model.ts index cb247e908..8e5afa59c 100644 --- a/src/app/api/auth.api.model.ts +++ b/src/app/api/auth.api.model.ts @@ -1,6 +1,3 @@ -export const LOGIN_METHOD_PASSWORD = "password"; -export const LOGIN_METHOD_GITHUB = "oauth_github"; - export interface PasswordLoginCredentials { login: string; password: string; diff --git a/src/app/api/auth.api.spec.ts b/src/app/api/auth.api.spec.ts index 559d119fd..94b85f590 100644 --- a/src/app/api/auth.api.spec.ts +++ b/src/app/api/auth.api.spec.ts @@ -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; @@ -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({ @@ -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({ @@ -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"), @@ -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"), diff --git a/src/app/api/auth.api.ts b/src/app/api/auth.api.ts index 00b2ca950..c5d74e523 100644 --- a/src/app/api/auth.api.ts +++ b/src/app/api/auth.api.ts @@ -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", @@ -34,14 +30,12 @@ export class AuthApi { return this.accountChanged$.asObservable(); } - public fetchUserInfoAndTokenFromPasswordLogin(login: string, password: string): Observable { - const passwordCredentials: PasswordLoginCredentials = { login, password }; - return this.fetchUserInfoAndTokenFromLoginMethod(LOGIN_METHOD_PASSWORD, JSON.stringify(passwordCredentials)); + public fetchUserInfoAndTokenFromPasswordLogin(credentials: PasswordLoginCredentials): Observable { + return this.fetchUserInfoAndTokenFromLoginMethod(LoginMethod.PASSWORD, JSON.stringify(credentials)); } - public fetchUserInfoAndTokenFromGithubCallackCode(code: string): Observable { - const githubCredentials: GithubLoginCredentials = { code }; - return this.fetchUserInfoAndTokenFromLoginMethod(LOGIN_METHOD_GITHUB, JSON.stringify(githubCredentials)); + public fetchUserInfoAndTokenFromGithubCallackCode(credentials: GithubLoginCredentials): Observable { + return this.fetchUserInfoAndTokenFromLoginMethod(LoginMethod.GITHUB, JSON.stringify(credentials)); } public fetchUserInfoAndTokenFromLoginMethod(loginMethod: string, loginCredentialsJson: string): Observable { diff --git a/src/app/api/mock/auth.mock.ts b/src/app/api/mock/auth.mock.ts index c73cb55fe..8ee4bb0cc 100644 --- a/src/app/api/mock/auth.mock.ts +++ b/src/app/api/mock/auth.mock.ts @@ -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"; @@ -17,7 +17,7 @@ const mockPasswordLoginCredentials: PasswordLoginCredentials = { }; export const mockLoginInstructions: AppConfigLoginInstructions = { - loginMethod: LOGIN_METHOD_PASSWORD, + loginMethod: LoginMethod.PASSWORD, loginCredentialsJson: JSON.stringify(mockPasswordLoginCredentials), }; diff --git a/src/app/app-config.model.ts b/src/app/app-config.model.ts index 80677f673..58ddbb7a9 100644 --- a/src/app/app-config.model.ts +++ b/src/app/app-config.model.ts @@ -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[]; } diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 277f399d5..4e3d5a1d3 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -24,7 +24,7 @@ export const routes: Routes = [ { path: ProjectLinks.URL_LOGIN, component: LoginComponent, - canLoad: [LoginGuard], + canActivate: [LoginGuard], }, { path: ProjectLinks.URL_SEARCH, diff --git a/src/app/app-routing.spec.ts b/src/app/app-routing.spec.ts index beb3d11a8..b9b6abae9 100644 --- a/src/app/app-routing.spec.ts +++ b/src/app/app-routing.spec.ts @@ -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; @@ -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(); @@ -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])); @@ -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(); })); }); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 25a39aa63..0aabe698f 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -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]; @@ -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}`; diff --git a/src/app/auth/github-callback/github.callback.spec.ts b/src/app/auth/github-callback/github.callback.spec.ts index 5aadf1b17..389a20a9a 100644 --- a/src/app/auth/github-callback/github.callback.spec.ts +++ b/src/app/auth/github-callback/github.callback.spec.ts @@ -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; @@ -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(); }); }); diff --git a/src/app/auth/github-callback/github.callback.ts b/src/app/auth/github-callback/github.callback.ts index 6d8fff6f7..85c2ddbb8 100644 --- a/src/app/auth/github-callback/github.callback.ts +++ b/src/app/auth/github-callback/github.callback.ts @@ -1,8 +1,9 @@ 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", @@ -10,7 +11,11 @@ import { BaseComponent } from "src/app/common/base.component"; 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(); } @@ -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); }), ); } diff --git a/src/app/auth/guards/login.guard.spec.ts b/src/app/auth/guards/login.guard.spec.ts index 328e418f3..3f88aaa0d 100644 --- a/src/app/auth/guards/login.guard.spec.ts +++ b/src/app/auth/guards/login.guard.spec.ts @@ -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; @@ -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); }); }); diff --git a/src/app/auth/guards/login.guard.ts b/src/app/auth/guards/login.guard.ts index 6071f9a65..921c99777 100644 --- a/src/app/auth/guards/login.guard.ts +++ b/src/app/auth/guards/login.guard.ts @@ -1,6 +1,6 @@ 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"; @@ -8,15 +8,16 @@ 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; } @@ -24,6 +25,6 @@ export class LoginGuard implements CanLoad { } private canLogin(): boolean { - return this.appConfigService.featureFlags.enableLogin && !this.loggedUserService.isAuthenticated; + return this.appConfigService.featureFlags.loginMethods.length > 0 && !this.loggedUserService.isAuthenticated; } } diff --git a/src/app/auth/logged-user-service.spec.ts b/src/app/auth/logged-user-service.spec.ts index 915f35b1f..2654ea2f8 100644 --- a/src/app/auth/logged-user-service.spec.ts +++ b/src/app/auth/logged-user-service.spec.ts @@ -20,6 +20,7 @@ import { first } from "rxjs/operators"; import { MaybeNull } from "../common/app.types"; import AppValues from "../common/app.values"; import { AppConfigService } from "../app-config.service"; +import { GithubLoginCredentials, PasswordLoginCredentials } from "../api/auth.api.model"; describe("LoggedUserService", () => { let controller: ApolloTestingController; @@ -61,7 +62,9 @@ describe("LoggedUserService", () => { } function loginFullyViaGithub(): void { - authApi.fetchUserInfoAndTokenFromGithubCallackCode(TEST_GITHUB_CODE).subscribe(); + authApi + .fetchUserInfoAndTokenFromGithubCallackCode({ code: TEST_GITHUB_CODE } as GithubLoginCredentials) + .subscribe(); const op = controller.expectOne(LoginDocument); op.flush({ @@ -70,7 +73,12 @@ describe("LoggedUserService", () => { } function loginFullyViaPassword(): void { - authApi.fetchUserInfoAndTokenFromPasswordLogin(TEST_LOGIN, TEST_PASSWORD).subscribe(); + authApi + .fetchUserInfoAndTokenFromPasswordLogin({ + login: TEST_LOGIN, + password: TEST_PASSWORD, + } as PasswordLoginCredentials) + .subscribe(); const op = controller.expectOne(LoginDocument); op.flush({ diff --git a/src/app/auth/login/login.component.html b/src/app/auth/login/login.component.html index 26c2dbfd7..1a311ee91 100644 --- a/src/app/auth/login/login.component.html +++ b/src/app/auth/login/login.component.html @@ -1,34 +1,93 @@ - -