Skip to content
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

wip(gatsby-cli): add login, logout, whoami commands #28251

Merged
merged 18 commits into from
Nov 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions packages/gatsby-cli/src/create-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import report from "./reporter"
import { setStore } from "./reporter/redux"
import { getLocalGatsbyVersion } from "./util/version"
import { initStarter } from "./init-starter"
import { login } from "./login"
import { logout } from "./logout"
import { whoami } from "./whoami"
import { recipesHandler } from "./recipes"
import { getPackageManager, setPackageManager } from "./util/package-manager"
import reporter from "./reporter"
Expand Down Expand Up @@ -411,6 +414,32 @@ function buildLocalCommands(cli: yargs.Argv, isLocalSite: boolean): void {
}),
handler: getCommandHandler(`plugin`),
})

if (process.env.GATSBY_EXPERIMENTAL_CLOUD_CLI) {
cli.command({
command: `login`,
describe: `Log in to Gatsby Cloud.`,
handler: handlerP(async () => {
await login()
}),
})

cli.command({
command: `logout`,
describe: `Sign out of Gatsby Cloud.`,
handler: handlerP(async () => {
await logout()
}),
})

cli.command({
command: `whoami`,
describe: `Gives the username of the current logged in user.`,
handler: handlerP(async () => {
await whoami()
}),
})
}
}

function isLocalGatsbySite(): boolean {
Expand Down
109 changes: 109 additions & 0 deletions packages/gatsby-cli/src/login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import fetch from "node-fetch"
import opn from "better-opn"
import reporter from "./reporter"
import { getToken, setToken } from "./util/manage-token"

interface ITicket {
verified: boolean
token?: string | null
expiration?: string | null
}

const createTicket = async (): Promise<string> => {
let ticketId
try {
const ticketResponse = await fetch(
`https://auth.gatsbyjs.com/auth/tickets/create`,
{
method: `post`,
}
)
const ticketJson = await ticketResponse.json()
ticketId = ticketJson.ticketId
} catch (e) {
reporter.panic(
`We had trouble connecting to Gatsby Cloud to create a login session.
Please try again later, and if it continues to have trouble connecting file an issue.`
)
}

return ticketId
}

const getTicket = async (ticketId: string): Promise<ITicket> => {
let ticket: ITicket = {
verified: false,
}
try {
const ticketResponse = await fetch(
`https://auth.gatsbyjs.com/auth/tickets/${ticketId}`
)
const ticketJson = await ticketResponse.json()
ticket = ticketJson
} catch (e) {
reporter.error(e)
}

return ticket
}

const handleOpenBrowser = (url): void => {
// TODO: this will break if run from the CLI
// for ideas see https://github.com/netlify/cli/blob/908f285fb80f04bf2635da73381c94387b9c8b0d/src/utils/open-browser.js
console.log(``)
reporter.info(`Opening Gatsby Cloud for you to login from, copy this`)
reporter.info(`url into your browser if it doesn't open automatically:`)
console.log(``)
console.log(url)
opn(url)
}

/**
* Main function that logs in to Gatsby Cloud using Gatsby Cloud's authentication service.
*/
export async function login(): Promise<void> {
const tokenFromStore = await getToken()

if (tokenFromStore) {
reporter.info(`You are already logged in!`)
return
}

const webUrl = `https://gatsbyjs.com`
reporter.info(`Logging into your Gatsby Cloud account...`)

// Create "ticket" for auth (like an expiring session)
const ticketId = await createTicket()

// Open browser for authentication
const authUrl = `${webUrl}/dashboard/login?authType=EXTERNAL_AUTH&ticketId=${ticketId}&noredirect=1`

await handleOpenBrowser(authUrl)

// Poll until the ticket has been verified, and should have the token attached
function pollForTicket(): Promise<ITicket> {
return new Promise(function (resolve): void {
// eslint-disable-next-line consistent-return
async function verify(): Promise<void> {
const ticket = await getTicket(ticketId)
const timeoutId = setTimeout(verify, 3000)
if (ticket.verified) {
clearTimeout(timeoutId)
return resolve(ticket)
}
}

verify()
})
}

console.log(``)
reporter.info(`Waiting for login from Gatsby Cloud...`)

const ticket = await pollForTicket()

if (ticket?.token && ticket?.expiration) {
await setToken(ticket.token, ticket.expiration)
}
reporter.info(`You have been logged in!`)
}
10 changes: 10 additions & 0 deletions packages/gatsby-cli/src/logout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import reporter from "./reporter"
import { setToken } from "./util/manage-token"

/**
* Main function that logs out of Gatsby Cloud by removing the token from the config store.
*/
export async function logout(): Promise<void> {
await setToken(null, ``)
reporter.info(`You have been logged out of Gatsby Cloud from this device.`)
}
23 changes: 23 additions & 0 deletions packages/gatsby-cli/src/util/manage-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { getConfigStore } from "gatsby-core-utils"
import report from "../reporter"

const tokenKey = `cli.token`
const tokenExpirationKey = `cli.tokenExpiration`

const getExpiration = (): string => getConfigStore().get(tokenExpirationKey)
export const getToken = async (): Promise<string> => {
const expiration = await getExpiration()
const tokenHasExpired = new Date() > new Date(expiration)
if (tokenHasExpired) {
report.warn(`Your token has expired, you may need to login again`)
}
return getConfigStore().get(tokenKey)
}

export const setToken = (token: string | null, expiration: string): void => {
const store = getConfigStore()

store.set(tokenKey, token)
// we would be able to decode an expiration off the JWT, but the auth service isn't set up to attach it to the token
store.set(tokenExpirationKey, expiration)
}
43 changes: 43 additions & 0 deletions packages/gatsby-cli/src/whoami.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import fetch from "node-fetch"
import reporter from "./reporter"
import { getToken } from "./util/manage-token"

const getUsername = async (token: string): Promise<string> => {
let currentUsername
const query = `query {
currentUser {
name
}
}`
try {
const usernameResponse = await fetch(`https://api.gatsbyjs.com/graphql`, {
method: `post`,
body: JSON.stringify({ query }),
headers: {
Authorization: `Bearer ${token}`,
"content-type": `application/json`,
},
})
const resJson = await usernameResponse.json()
currentUsername = resJson.data.currentUser.name
} catch (e) {
reporter.error(e)
}

return currentUsername
}

/**
* Reports the username of the logged in user if they are logged in.
*/
export async function whoami(): Promise<void> {
const tokenFromStore = await getToken()

if (!tokenFromStore) {
reporter.info(`You are not currently logged in!`)
return
}

const username = await getUsername(tokenFromStore)
reporter.info(username)
}