Skip to content

Commit

Permalink
wip(gatsby-cli): add login, logout, whoami commands (#28251)
Browse files Browse the repository at this point in the history
* add first wip implementation of login command

* add logout and whoami commands

* update urls

* wrap commands in experimental flag
  • Loading branch information
gillkyle authored Nov 30, 2020
1 parent f9bd368 commit d18b199
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 0 deletions.
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)
}

0 comments on commit d18b199

Please sign in to comment.