Skip to content

Commit

Permalink
Interpreting login instructions at startup
Browse files Browse the repository at this point in the history
Auth login API publishes custom method, driven by app config.
Account component shows full name and avatar, if known.
GraphQL caching by User customized to use name instead of id
  • Loading branch information
zaychenko-sergei committed Aug 28, 2023
1 parent 54cfb68 commit 039be6c
Show file tree
Hide file tree
Showing 18 changed files with 147 additions and 103 deletions.
1 change: 1 addition & 0 deletions src/app/api/account.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AccountDetailsFragment } from "./kamu.graphql.interface";

@Injectable({ providedIn: "root" })
export class AccountApi {
// TODO: issue a GraphQL query
public getAccountInfoByName(name: string): Observable<AccountDetailsFragment> {
return of({ ...mockAccountDetails, login: name });
}
Expand Down
30 changes: 13 additions & 17 deletions src/app/api/auth.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,21 @@ export class AuthApi {

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

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

public fetchUserInfoFromAccessToken(accessToken: string): Observable<void> {
return this.fetchAccountInfoGQL.mutate({ accessToken }).pipe(
map((result: MutationResult<FetchAccountInfoMutation>) => {
public fetchUserInfoAndTokenFromLoginMethod(loginMethod: string, loginCredentialsJson: string): Observable<void> {
return this.loginGQL.mutate({ login_method: loginMethod, login_credentials_json: loginCredentialsJson }).pipe(
map((result: MutationResult<LoginMutation>) => {
if (result.data) {
const data: FetchAccountInfoMutation = result.data;
this.accountChanged$.next(data.auth.accountInfo);
const data: LoginMutation = result.data;
this.accessTokenObtained$.next(data.auth.login.accessToken);
this.accountChanged$.next(data.auth.login.accountInfo);
} else {
throw new AuthenticationError(result.errors ?? []);
}
Expand All @@ -58,17 +59,12 @@ export class AuthApi {
);
}

private fetchUserInfoAndTokenFromLoginMethod<TCredentials>(
loginMethod: string,
loginCredentials: TCredentials,
): Observable<void> {
const loginCredentialsJson: string = JSON.stringify(loginCredentials);
return this.loginGQL.mutate({ login_method: loginMethod, login_credentials_json: loginCredentialsJson }).pipe(
map((result: MutationResult<LoginMutation>) => {
public fetchUserInfoFromAccessToken(accessToken: string): Observable<void> {
return this.fetchAccountInfoGQL.mutate({ accessToken }).pipe(
map((result: MutationResult<FetchAccountInfoMutation>) => {
if (result.data) {
const data: LoginMutation = result.data;
this.accessTokenObtained$.next(data.auth.login.accessToken);
this.accountChanged$.next(data.auth.login.accountInfo);
const data: FetchAccountInfoMutation = result.data;
this.accountChanged$.next(data.auth.accountInfo);
} else {
throw new AuthenticationError(result.errors ?? []);
}
Expand Down
18 changes: 9 additions & 9 deletions src/app/api/dataset.api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
mockGetMetadataBlockQuery,
TEST_BLOCK_HASH,
TEST_DATASET_NAME,
TEST_USER_NAME,
} from "./mock/dataset.mock";
import {
mockCommitEventResponse,
Expand All @@ -29,6 +28,7 @@ import {
GetMetadataBlockQuery,
} from "./kamu.graphql.interface";
import { MaybeNullOrUndefined } from "../common/app.types";
import { TEST_LOGIN } from "./mock/auth.mock";

describe("DatasetApi", () => {
let service: DatasetApi;
Expand All @@ -54,7 +54,7 @@ describe("DatasetApi", () => {
it("should query dataset main data", () => {
service
.getDatasetMainData({
accountName: TEST_USER_NAME,
accountName: TEST_LOGIN,
datasetName: TEST_DATASET_NAME,
})
.subscribe((res: GetDatasetMainDataQuery) => {
Expand All @@ -65,7 +65,7 @@ describe("DatasetApi", () => {
});

const op = controller.expectOne(GetDatasetMainDataDocument);
expect(op.operation.variables.accountName).toEqual(TEST_USER_NAME);
expect(op.operation.variables.accountName).toEqual(TEST_LOGIN);
expect(op.operation.variables.datasetName).toEqual(TEST_DATASET_NAME);

op.flush({
Expand Down Expand Up @@ -105,7 +105,7 @@ describe("DatasetApi", () => {
it("should extract dataset history", () => {
service
.getDatasetHistory({
accountName: TEST_USER_NAME,
accountName: TEST_LOGIN,
datasetName: TEST_DATASET_NAME,
numRecords: 20,
numPage: 1,
Expand All @@ -117,7 +117,7 @@ describe("DatasetApi", () => {
});

const op = controller.expectOne(GetDatasetHistoryDocument);
expect(op.operation.variables.accountName).toEqual(TEST_USER_NAME);
expect(op.operation.variables.accountName).toEqual(TEST_LOGIN);
expect(op.operation.variables.datasetName).toEqual(TEST_DATASET_NAME);

op.flush({
Expand All @@ -126,7 +126,7 @@ describe("DatasetApi", () => {
});

it("should extract datasets by account name", () => {
service.fetchDatasetsByAccountName(TEST_USER_NAME).subscribe((res: DatasetsByAccountNameQuery) => {
service.fetchDatasetsByAccountName(TEST_LOGIN).subscribe((res: DatasetsByAccountNameQuery) => {
expect(res.datasets.byAccountName.totalCount).toEqual(
mockDatasetsByAccountNameQuery.datasets.byAccountName.totalCount,
);
Expand All @@ -136,7 +136,7 @@ describe("DatasetApi", () => {
});

const op = controller.expectOne(DatasetsByAccountNameDocument);
expect(op.operation.variables.accountName).toEqual(TEST_USER_NAME);
expect(op.operation.variables.accountName).toEqual(TEST_LOGIN);

op.flush({
data: mockDatasetsByAccountNameQuery,
Expand All @@ -147,7 +147,7 @@ describe("DatasetApi", () => {
const blockByHash = mockGetMetadataBlockQuery.datasets.byOwnerAndName?.metadata.chain.blockByHash;
service
.getBlockByHash({
accountName: TEST_USER_NAME,
accountName: TEST_LOGIN,
datasetName: TEST_DATASET_NAME,
blockHash: TEST_BLOCK_HASH,
})
Expand All @@ -161,7 +161,7 @@ describe("DatasetApi", () => {
});

const op = controller.expectOne(GetMetadataBlockDocument);
expect(op.operation.variables.accountName).toEqual(TEST_USER_NAME);
expect(op.operation.variables.accountName).toEqual(TEST_LOGIN);
expect(op.operation.variables.datasetName).toEqual(TEST_DATASET_NAME);
expect(op.operation.variables.blockHash).toEqual(TEST_BLOCK_HASH);
op.flush({
Expand Down
17 changes: 15 additions & 2 deletions src/app/api/mock/auth.mock.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
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";

export const TEST_GITHUB_CODE = "12345";
export const TEST_ACCESS_TOKEN_GITHUB = "someTokenViaGithub";
export const TEST_ACCESS_TOKEN_PASSWORD = "someTokenViaPassword";
export const TEST_LOGIN = "foo";
export const TEST_PASSWORD = "bar";
export const TEST_USER_NAME = "Test User";

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

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

export const mockAccountDetails: AccountDetailsFragment = {
login: "test-user",
name: "Test User",
login: TEST_LOGIN,
name: TEST_USER_NAME,
avatarUrl: AppValues.DEFAULT_AVATAR_URL,
};

Expand Down
1 change: 0 additions & 1 deletion src/app/api/mock/dataset.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
import { DataSchemaFormat } from "../kamu.graphql.interface";
import { DatasetsAccountResponse } from "src/app/interface/dataset.interface";

export const TEST_USER_NAME = "test-user";
export const TEST_DATASET_NAME = "test-dataset";
export const TEST_BLOCK_HASH = "zW1hNbxPz28K1oLNGbddudUzKKLT9LDPh8chjksEo6HcDev";

Expand Down
15 changes: 15 additions & 0 deletions src/app/app-config.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export interface AppConfig {
apiServerGqlUrl: string;
featureFlags: AppConfigFeatureFlags;
loginInstructions?: AppConfigLoginInstructions;
}

export interface AppConfigLoginInstructions {
loginMethod: string;
loginCredentialsJson: string;
}

export interface AppConfigFeatureFlags {
enableLogin: boolean;
enableLogout: boolean;
}
28 changes: 5 additions & 23 deletions src/app/app-config.service.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,5 @@
import { Injectable } from "@angular/core";
import { AccountDetailsFragment } from "./api/kamu.graphql.interface";
import { FeatureFlags } from "./common/feature-flags.model";

interface AppConfig {
apiServerGqlUrl: string;
featureFlags: {
enableLogin: boolean;
enableLogout: boolean;
};
loggedUser?: AppConfigLoggedUser;
}

interface AppConfigLoggedUser {
login: string;
name: string;
avatarUrl?: string;
}
import { AppConfig, AppConfigFeatureFlags, AppConfigLoginInstructions } from "./app-config.model";

@Injectable({
providedIn: "root",
Expand All @@ -31,23 +15,21 @@ export class AppConfigService {
return this.appConfig.apiServerGqlUrl;
}

get featureFlags(): FeatureFlags {
get featureFlags(): AppConfigFeatureFlags {
if (!this.appConfig) {
this.appConfig = AppConfigService.loadAppConfig();
}

return this.appConfig.featureFlags;
}

get loggedUser(): AccountDetailsFragment | null {
get loginInstructions(): AppConfigLoginInstructions | null {
if (!this.appConfig) {
this.appConfig = AppConfigService.loadAppConfig();
}

if (this.appConfig.loggedUser) {
return {
...this.appConfig.loggedUser,
} as AccountDetailsFragment;
if (this.appConfig.loginInstructions) {
return this.appConfig.loginInstructions;
} else {
return null;
}
Expand Down
6 changes: 3 additions & 3 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import { MaybeNull } from "./common/app.types";
import _ from "lodash";
import { isMobileView, promiseWithCatch } from "./common/app.helpers";
import { AppConfigService } from "./app-config.service";
import { FeatureFlags } from "./common/feature-flags.model";
import { LoggedUserService } from "./auth/logged-user.service";
import { AppConfigFeatureFlags } from "./app-config.model";

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

Expand All @@ -31,7 +31,7 @@ export class AppComponent extends BaseComponent implements OnInit {
login: "",
name: AppValues.DEFAULT_USERNAME,
};
public static readonly DEFAULT_FEATURE_FLAGS: FeatureFlags = {
public static readonly DEFAULT_FEATURE_FLAGS: AppConfigFeatureFlags = {
enableLogin: true,
enableLogout: true,
};
Expand All @@ -41,7 +41,7 @@ export class AppComponent extends BaseComponent implements OnInit {
public isMobileView = false;
public isHeaderVisible = true;

public featureFlags: FeatureFlags = AppComponent.DEFAULT_FEATURE_FLAGS;
public featureFlags: AppConfigFeatureFlags = AppComponent.DEFAULT_FEATURE_FLAGS;
public loggedUserInfo: AccountDetailsFragment = AppComponent.ANONYMOUS_ACCOUNT_INFO;

@HostListener("window:resize")
Expand Down
12 changes: 10 additions & 2 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import { ToastrModule } from "ngx-toastr";
import { LoggedUserService } from "./auth/logged-user.service";
import { firstValueFrom } from "rxjs";
import { LoginService } from "./auth/login/login.service";
import { logError } from "./common/app.helpers";

const Services = [
{
Expand Down Expand Up @@ -93,7 +94,14 @@ const Services = [
provide: APOLLO_OPTIONS,
useFactory: (httpLink: HttpLink, appConfig: AppConfigService) => {
return {
cache: new InMemoryCache(),
cache: new InMemoryCache({
typePolicies: {
User: {
// For now we are faking account IDs on the server, so they are a bad caching field
keyFields: ["name"],
},
},
}),
link: httpLink.create({
uri: appConfig.apiServerGqlUrl,
}),
Expand All @@ -115,7 +123,7 @@ const Services = [
provide: APP_INITIALIZER,
useFactory: (loggedUserService: LoggedUserService) => {
return (): Promise<void> => {
return firstValueFrom(loggedUserService.initialize());
return firstValueFrom(loggedUserService.initialize()).catch((e) => logError(e));
};
},
deps: [LoggedUserService],
Expand Down
4 changes: 2 additions & 2 deletions src/app/auth/account/account.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
</ng-template>
</div>
<span class="vcard-username d-block p-2" itemprop="additionalName">
{{ accountName }}
{{ user?.name }}
</span>
<div class="my-3 text-center me-4">
<button
Expand Down Expand Up @@ -125,7 +125,7 @@ <h2 class="text-start pt-3 px-3 h6 organization">Organizations</h2>
<ng-container *ngIf="isAccountViewTypeDatasets">
<app-datasets-tab
[datasets]="datasets"
[accountName]="accountName"
[accountName]="user?.login"
[accountViewType]="accountViewType"
[pageInfo]="pageInfo"
></app-datasets-tab>
Expand Down
4 changes: 2 additions & 2 deletions src/app/auth/account/account.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NavigationService } from "src/app/services/navigation.service";
import { mockAccountDetails } from "./../../api/mock/auth.mock";
import { TEST_LOGIN, mockAccountDetails } from "./../../api/mock/auth.mock";
import { AccountTabs } from "./account.constants";
import { MatButtonToggleModule } from "@angular/material/button-toggle";
import { MatIconModule } from "@angular/material/icon";
Expand All @@ -25,7 +25,7 @@ describe("AccountComponent", () => {
page: 2,
});
const mockParams = new BehaviorSubject({
[ProjectLinks.URL_PARAM_ACCOUNT_NAME]: "test-user",
[ProjectLinks.URL_PARAM_ACCOUNT_NAME]: TEST_LOGIN,
});
beforeEach(async () => {
await TestBed.configureTestingModule({
Expand Down
Loading

0 comments on commit 039be6c

Please sign in to comment.