Skip to content

Commit

Permalink
Feat/login improv error (#188)
Browse files Browse the repository at this point in the history
* feat: login command improved error handling and explanation

* chore: changeset
  • Loading branch information
field123 authored Feb 5, 2024
1 parent 3a6cdf3 commit aa305e7
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 70 deletions.
5 changes: 5 additions & 0 deletions .changeset/quiet-plums-develop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"composable-cli": patch
---

login command improved error handling and explanation
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import { encodeObjectToQueryString } from "../../util/encode-object-to-query-str"
import { EpccRequester } from "../../util/command"
import { resolveHostFromRegion } from "../../util/resolve-region"
import { Region } from "../../lib/stores/region-schema"

export async function authenticateGrantTypePassword(
requester: EpccRequester,
username: string,
password: string,
region: Region,
): Promise<unknown> {
const body = {
grant_type: "password",
username,
password,
}

const response = await requester(`/oauth/access_token`, {
const url = new URL(`/oauth/access_token`, resolveHostFromRegion(region))

const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Expand Down
89 changes: 61 additions & 28 deletions packages/composable-cli/src/commands/login/login-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,27 @@ import {
} from "../../util/conf-store/store-credentials"
import { isAuthenticated } from "../../util/check-authenticated"
import { trackCommandHandler } from "../../util/track-command-handler"
import { EpccRequester } from "../../util/command"
import { createFixedRequester } from "../../util/command"
import {
Credentials,
credentialsResponseSchema,
} from "../../lib/authentication/credentials-schema"
import { welcomeNote } from "../ui/alert"
import { outputContent, outputToken } from "../output"
import { renderError, renderWarning } from "../ui"
import { renderError, renderInfo, renderWarning } from "../ui"
import { Listr } from "listr2"
import { UserProfile } from "../../lib/epcc-user-profile-schema"
import { processUnknownError } from "../../util/process-unknown-error"
import { CLITaskError } from "../../lib/error/cli-task-error"
import { Region } from "../../lib/stores/region-schema"

/**
* Region prompts
*/
const regionPrompts = {
type: "list",
name: "region",
message: "What region do you want to authenticated with?",
message: "What region is your store hosted in?",
choices: [
{ name: "us-east (free trial users)", value: "us-east" },
{ name: "eu-west", value: "eu-west" },
Expand Down Expand Up @@ -109,11 +111,14 @@ export function createAuthenticationMiddleware(

const isInsightsCommand = args._.some((arg) => arg === "insights")

const isConfigCommand = args._.some((arg) => arg === "config")

if (
isAuthenticated(store) ||
!args.interactive ||
isAuthCommand ||
isInsightsCommand
isInsightsCommand ||
isConfigCommand
) {
return
}
Expand All @@ -139,7 +144,6 @@ export function createLoginCommandHandler(
LoginCommandArguments
> {
const { store } = ctx
const requester = ctx.requester

return async function loginCommandHandler(args) {
const alreadyLoggedIn = isAuthenticated(store)
Expand Down Expand Up @@ -167,16 +171,27 @@ export function createLoginCommandHandler(
}
}

renderInfo({
body: outputContent`Elastic Path stores are hosted across multiple regions, please select the region where your store is hosted.\n\nYou can find out your stores region by looking at the version of Commerce Manager you're using. e.g. ${outputToken.green(
"region",
)}.cm.elasticpath.com where ${outputToken.green(
"region",
)} is either useast or euwest`.value,
})

const regionAnswers = await inquirer.prompt(regionPrompts, {
...(args.region ? { region: args.region } : {}),
})

renderInfo({
body: outputContent`Authenticate with your Elastic Path account credentials.`
.value,
})
const { username, password } = await promptUsernamePasswordLogin(args)

const loginTask = createLoginTask({
username,
password,
requester,
store,
region: regionAnswers.region,
})
Expand Down Expand Up @@ -239,14 +254,12 @@ export function createLoginCommandHandler(
function createLoginTask({
username,
password,
requester,
region,
store,
}: {
username: string
password: string
region: "eu-west" | "us-east"
requester: CommandContext["requester"]
region: Region
store: Conf
}) {
return new Listr<LoginTaskContext>(
Expand All @@ -259,17 +272,22 @@ function createLoginTask({
title: "Authenticating",
task: async (innerCtx) => {
const result = await authenticateUserPassword(
requester,
username,
password,
region,
)

if (!result.success) {
renderError({
headline: "Failed to authenticate",
body: "There was a problem logging you in. Make sure that your username and password are correct.",
})
throw new Error("Failed to authenticate")
throw new CLITaskError({
message: result.message,
description: result.name,
taskName: "Authenticating",
code: "request_failure",
})
}

innerCtx.credentials = result.data
Expand All @@ -280,13 +298,19 @@ function createLoginTask({
skip: (innerCtx) => !innerCtx.credentials,
task: async (innerCtx, innerTask) => {
if (!innerCtx.credentials) {
throw new Error(
"No credentials found can't complete Fetching your profile task.",
)
throw new CLITaskError({
message:
"No credentials found can't complete Fetching your profile task.",
taskName: innerTask.title,
code: "missing_expected_context_data",
})
}

const userProfileResponse = await epccUserProfile(
requester,
createFixedRequester(
region,
innerCtx.credentials.access_token,
),
innerCtx.credentials.access_token,
)

Expand All @@ -296,9 +320,12 @@ function createLoginTask({
"Successfully authenticated but failed to load user profile",
body: `Their was a problem loading your user profile ${userProfileResponse.error.code} - ${userProfileResponse.error.message}.`,
})
throw new Error(
"Successfully authenticated but failed to load user profile",
)
throw new CLITaskError({
message: userProfileResponse.error.message,
description: userProfileResponse.error.code,
taskName: innerTask.title,
code: "request_failure",
})
}
innerCtx.profile = userProfileResponse.data.data
innerTask.output = `Successfully authenticated as ${userProfileResponse.data.data.email}`
Expand All @@ -307,17 +334,23 @@ function createLoginTask({
{
title: "Storing credentials",
skip: (innerCtx) => !innerCtx.credentials,
task: async (innerCtx) => {
task: async (innerCtx, innerTask) => {
if (!innerCtx.profile) {
throw new Error(
"No profile found can't complete Storing credentials task.",
)
throw new CLITaskError({
message:
"Expected to find profile on context but it was not defined can't complete Storing credentials task.",
taskName: innerTask.title,
code: "missing_expected_context_data",
})
}

if (!innerCtx.credentials) {
throw new Error(
"No credentials found can't complete Storing credentials task.",
)
throw new CLITaskError({
message:
"Expected to find credentials on context but it was not defined can't complete Storing credentials task.",
taskName: innerTask.title,
code: "missing_expected_context_data",
})
}

handleClearCredentials(store)
Expand All @@ -341,9 +374,9 @@ function createLoginTask({
}

async function authenticateUserPassword(
requester: EpccRequester,
username: string,
password: string,
region: Region,
): Promise<
| { success: true; data: Credentials }
| {
Expand All @@ -355,9 +388,9 @@ async function authenticateUserPassword(
> {
try {
const credentialsResp = await authenticateGrantTypePassword(
requester,
username,
password,
region,
)

const parsedCredentialsResp =
Expand Down Expand Up @@ -409,7 +442,7 @@ async function promptUsernamePasswordLogin(
[
{
type: "string",
message: "Enter your username",
message: "Enter your email address",
name: "username",
},
{
Expand Down
29 changes: 0 additions & 29 deletions packages/composable-cli/src/commands/ui/error/error.tsx

This file was deleted.

33 changes: 33 additions & 0 deletions packages/composable-cli/src/lib/error/cli-task-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export type CLITaskErrorOptions = {
message: string
code: CLITaskErrorCode
taskName: string
description?: string
}

export type CLITaskErrorCode =
| "missing_expected_context_data"
| "request_failure"

export class CLITaskError extends Error {
/**
* Unique error code for the error.
*/
public code: CLITaskErrorCode
/**
* The name of the task that failed.
*/
public taskName: string
/**
* A description of the error that is more details than the message you want to show to the user.
*/
public description: string | undefined

constructor({ message, code, taskName, description }: CLITaskErrorOptions) {
super(message)
this.name = "CLITaskError"
this.code = code
this.taskName = taskName
this.description = description
}
}
7 changes: 7 additions & 0 deletions packages/composable-cli/src/types/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,14 @@ export type CommandResult<TData, TError> =

export type CommandContext = {
store: Conf
/**
* Requester makes fetch requests with authentication headers and host values preset
* e.g. requester("/v2/user") will make a request to the configured host with the authenticated users access token
*/
requester: typeof fetch
/**
* rawRequester makes fetch requests without any authentication headers or host values preset
*/
rawRequester: typeof fetch
posthog?: {
client: PostHog
Expand Down
Loading

0 comments on commit aa305e7

Please sign in to comment.