diff --git a/cli/CONTRIBUTING.md b/cli/CONTRIBUTING.md index b13de644c..823da70d0 100644 --- a/cli/CONTRIBUTING.md +++ b/cli/CONTRIBUTING.md @@ -39,3 +39,15 @@ where `08lcul` is your local workspace id and `dev` is the name of your local re ``` xata init --profile local ``` + +# Logging in with a deploy preview + +To log in with a deploy preview, you can use the following command: + +``` +xata auth login --profile deploy-preview --host staging --web-host https://xata-p9lbsnxlc-xata.vercel.app +``` + +where `https://xata-p9lbsnxlc-xata.vercel.app` is the deploy preview URL. + +Alternatively you can set the backend of the frontend url with `XATA_WEB_URL` env variable when running any command. diff --git a/cli/src/auth-server.test.ts b/cli/src/auth-server.test.ts index def154abe..6a37123c9 100644 --- a/cli/src/auth-server.test.ts +++ b/cli/src/auth-server.test.ts @@ -4,14 +4,15 @@ import url from 'url'; import { describe, expect, test, vi } from 'vitest'; import { generateKeys, generateURL, handler } from './auth-server.js'; +const host = 'https://app.xata.io'; const port = 1234; const { publicKey, privateKey, passphrase } = generateKeys(); describe('generateURL', () => { test('generates a URL', async () => { - const uiURL = generateURL(port, publicKey); + const uiURL = generateURL(host, port, publicKey); - expect(uiURL.startsWith('https://app.xata.io/new-api-key?')).toBe(true); + expect(uiURL.startsWith(`${host}/new-api-key?`)).toBe(true); const parsed = url.parse(uiURL, true); const { pub, name, redirect } = parsed.query; @@ -25,7 +26,7 @@ describe('generateURL', () => { describe('handler', () => { test('405s if the method is not GET', async () => { const callback = vi.fn(); - const httpHandler = handler(publicKey, privateKey, passphrase, callback); + const httpHandler = handler(host, publicKey, privateKey, passphrase, callback); const req = { method: 'POST', url: '/' } as unknown as IncomingMessage; const res = { @@ -42,7 +43,7 @@ describe('handler', () => { test('redirects if the path is /new', async () => { const callback = vi.fn(); - const httpHandler = handler(publicKey, privateKey, passphrase, callback); + const httpHandler = handler(host, publicKey, privateKey, passphrase, callback); const writeHead = vi.fn(); const req = { method: 'GET', url: '/new', socket: { localPort: 9999 } } as unknown as IncomingMessage; @@ -55,7 +56,7 @@ describe('handler', () => { const [status, headers] = writeHead.mock.calls[0]; expect(status).toEqual(302); - expect(String(headers.location).startsWith('https://app.xata.io/new-api-key?pub=')).toBeTruthy(); + expect(String(headers.location).startsWith(`${host}/new-api-key?pub=`)).toBeTruthy(); expect(String(headers.location).includes('9999')).toBeTruthy(); expect(res.end).toHaveBeenCalledWith(); expect(callback).not.toHaveBeenCalled(); @@ -63,7 +64,7 @@ describe('handler', () => { test('404s if the path is not the root path', async () => { const callback = vi.fn(); - const httpHandler = handler(publicKey, privateKey, passphrase, callback); + const httpHandler = handler(host, publicKey, privateKey, passphrase, callback); const req = { method: 'GET', url: '/foo' } as unknown as IncomingMessage; const res = { @@ -80,7 +81,7 @@ describe('handler', () => { test('returns 400 if resource is called with the wrong parameters', async () => { const callback = vi.fn(); - const httpHandler = handler(publicKey, privateKey, passphrase, callback); + const httpHandler = handler(host, publicKey, privateKey, passphrase, callback); const req = { method: 'GET', url: '/' } as unknown as IncomingMessage; const res = { @@ -97,7 +98,7 @@ describe('handler', () => { test('hadles errors correctly', async () => { const callback = vi.fn(); - const httpHandler = handler(publicKey, privateKey, passphrase, callback); + const httpHandler = handler(host, publicKey, privateKey, passphrase, callback); const req = { method: 'GET', url: '/?key=malformed-key' } as unknown as IncomingMessage; const res = { @@ -115,7 +116,7 @@ describe('handler', () => { test('receives the API key if everything is fine', async () => { const callback = vi.fn(); - const httpHandler = handler(publicKey, privateKey, passphrase, callback); + const httpHandler = handler(host, publicKey, privateKey, passphrase, callback); const apiKey = 'abcdef1234'; const encryptedKey = crypto.publicEncrypt(publicKey, Buffer.from(apiKey)); @@ -139,4 +140,18 @@ describe('handler', () => { expect(req.destroy).toHaveBeenCalled(); expect(callback).toHaveBeenCalledWith(apiKey); }); + + test("Uses a different host if it's provided", async () => { + const host2 = `https://xata-${Math.random()}.test.io`; + const uiURL = generateURL(host2, port, publicKey); + + expect(uiURL.startsWith(`${host2}/new-api-key?`)).toBe(true); + + const parsed = url.parse(uiURL, true); + const { pub, name, redirect } = parsed.query; + + expect(pub).toBeDefined(); + expect(name).toEqual('Xata CLI'); + expect(redirect).toEqual('http://localhost:1234'); + }); }); diff --git a/cli/src/auth-server.ts b/cli/src/auth-server.ts index 9a154960a..17cc11ef1 100644 --- a/cli/src/auth-server.ts +++ b/cli/src/auth-server.ts @@ -10,7 +10,13 @@ import url, { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -export function handler(publicKey: string, privateKey: string, passphrase: string, callback: (apiKey: string) => void) { +export function handler( + webHost: string, + publicKey: string, + privateKey: string, + passphrase: string, + callback: (apiKey: string) => void +) { return (req: http.IncomingMessage, res: http.ServerResponse) => { try { if (req.method !== 'GET') { @@ -22,7 +28,7 @@ export function handler(publicKey: string, privateKey: string, passphrase: strin if (parsedURL.pathname === '/new') { const port = req.socket.localPort ?? 80; res.writeHead(302, { - location: generateURL(port, publicKey) + location: generateURL(webHost, port, publicKey) }); res.end(); return; @@ -58,14 +64,14 @@ function renderSuccessPage(req: http.IncomingMessage, res: http.ServerResponse, res.end(html.replace('data-color-mode=""', `data-color-mode="${colorMode}"`)); } -export function generateURL(port: number, publicKey: string) { +export function generateURL(webHost: string, port: number, publicKey: string) { const pub = publicKey .replace(/\n/g, '') .replace('-----BEGIN PUBLIC KEY-----', '') .replace('-----END PUBLIC KEY-----', ''); const name = 'Xata CLI'; const redirect = `http://localhost:${port}`; - const url = new URL('https://app.xata.io/new-api-key'); + const url = new URL(`${webHost}/new-api-key`); url.searchParams.append('pub', pub); url.searchParams.append('name', name); url.searchParams.append('redirect', redirect); @@ -90,19 +96,19 @@ export function generateKeys() { return { publicKey, privateKey, passphrase }; } -export async function createAPIKeyThroughWebUI() { +export async function createAPIKeyThroughWebUI(webHost: string) { const { publicKey, privateKey, passphrase } = generateKeys(); return new Promise((resolve) => { const server = http.createServer( - handler(publicKey, privateKey, passphrase, (apiKey) => { + handler(webHost, publicKey, privateKey, passphrase, (apiKey) => { resolve(apiKey); server.close(); }) ); server.listen(() => { const { port } = server.address() as AddressInfo; - const openURL = generateURL(port, publicKey); + const openURL = generateURL(webHost, port, publicKey); console.log( `We are opening your default browser. If your browser doesn't open automatically, please copy and paste the following URL into your browser: ${chalk.bold( `http://localhost:${port}/new` diff --git a/cli/src/base.ts b/cli/src/base.ts index 983a999df..d0fdb46b9 100644 --- a/cli/src/base.ts +++ b/cli/src/base.ts @@ -538,7 +538,7 @@ export abstract class BaseCommand extends Command { } } - async obtainKey() { + async obtainKey(webHost: string) { const { decision } = await this.prompt({ type: 'select', name: 'decision', @@ -551,7 +551,7 @@ export abstract class BaseCommand extends Command { if (!decision) this.exit(2); if (decision === 'create') { - return createAPIKeyThroughWebUI(); + return createAPIKeyThroughWebUI(webHost); } else if (decision === 'existing') { const { key } = await this.prompt({ type: 'password', diff --git a/cli/src/commands/auth/login.test.ts b/cli/src/commands/auth/login.test.ts index c61a17542..544cab940 100644 --- a/cli/src/commands/auth/login.test.ts +++ b/cli/src/commands/auth/login.test.ts @@ -142,7 +142,9 @@ describe('auth login', () => { expect(fs.mkdir).toHaveBeenCalledWith(dirname(credentialsFilePath), { recursive: true }); expect(fs.writeFile).toHaveBeenCalledWith( credentialsFilePath, - ini.stringify({ default: { apiKey: '1234abcdef' } }), + ini.stringify({ + default: { apiKey: '1234abcdef', name: 'default', web: 'https://app.xata.io', host: 'production' } + }), { mode: 0o600 } @@ -173,7 +175,7 @@ describe('auth login', () => { expect(fs.mkdir).toHaveBeenCalledWith(dirname(credentialsFilePath), { recursive: true }); expect(fs.writeFile).toHaveBeenCalledWith( credentialsFilePath, - ini.stringify({ default: { apiKey: 'foobar', api: 'production' } }), + ini.stringify({ default: { apiKey: 'foobar', name: 'default', web: 'https://app.xata.io', host: 'production' } }), { mode: 0o600 } diff --git a/cli/src/commands/auth/login.ts b/cli/src/commands/auth/login.ts index 92829de13..4772d4218 100644 --- a/cli/src/commands/auth/login.ts +++ b/cli/src/commands/auth/login.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core'; import { parseProviderString } from '@xata.io/client'; import { BaseCommand } from '../../base.js'; -import { hasProfile, setProfile } from '../../credentials.js'; +import { buildProfile, hasProfile, setProfile } from '../../credentials.js'; export default class Login extends BaseCommand { static description = 'Authenticate with Xata'; @@ -13,6 +13,9 @@ export default class Login extends BaseCommand { host: Flags.string({ description: 'Xata API host provider' }), + 'web-host': Flags.string({ + description: 'Xata web host url (app.xata.io)' + }), 'api-key': Flags.string({ description: 'Xata API key to use for authentication' }) @@ -42,11 +45,13 @@ export default class Login extends BaseCommand { this.error('Invalid host provider, expected either "production", "staging" or "{apiUrl},{workspacesUrl}"'); } - const key = flags['api-key'] ?? (await this.obtainKey()); + const web = flags['web-host']; + const newProfile = buildProfile({ name: profile.name, api: flags.host, web }); + const key = flags['api-key'] ?? (await this.obtainKey(newProfile.web)); await this.verifyAPIKey({ ...profile, apiKey: key, host }); - await setProfile(profile.name, { apiKey: key, api: flags.host }); + await setProfile(profile.name, { ...newProfile, apiKey: key }); this.success('All set! you can now start using xata'); } diff --git a/cli/src/commands/init/index.ts b/cli/src/commands/init/index.ts index 5733624ba..a26e643f3 100644 --- a/cli/src/commands/init/index.ts +++ b/cli/src/commands/init/index.ts @@ -395,7 +395,7 @@ export default class Init extends BaseCommand { let apiKey = profile.apiKey; if (!apiKey) { - apiKey = await createAPIKeyThroughWebUI(); + apiKey = await createAPIKeyThroughWebUI(profile.web); this.apiKeyLocation = 'new'; // Any following API call must use this API key process.env.XATA_API_KEY = apiKey; diff --git a/cli/src/credentials.ts b/cli/src/credentials.ts index 643dfbf1a..1ddb5a816 100644 --- a/cli/src/credentials.ts +++ b/cli/src/credentials.ts @@ -90,7 +90,7 @@ export function buildProfile(base: Partial & { name: string }): Prof return { name: base.name, apiKey: base.apiKey ?? process.env.XATA_API_KEY ?? '', - web: base.web ?? process.env.XATA_WEB_URL ?? '', + web: base.web ?? process.env.XATA_WEB_URL ?? 'https://app.xata.io', host: parseProviderString(base.api ?? process.env.XATA_API_PROVIDER) ?? 'production' }; }