Skip to content

Commit

Permalink
Allow to login with a deploy preview (#1435)
Browse files Browse the repository at this point in the history
Signed-off-by: Alexis Rico <[email protected]>
  • Loading branch information
SferaDev authored Apr 9, 2024
1 parent c82087e commit c4d08b8
Show file tree
Hide file tree
Showing 8 changed files with 65 additions and 25 deletions.
12 changes: 12 additions & 0 deletions cli/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
33 changes: 24 additions & 9 deletions cli/src/auth-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 = {
Expand All @@ -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;
Expand All @@ -55,15 +56,15 @@ 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();
});

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 = {
Expand All @@ -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 = {
Expand All @@ -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 = {
Expand All @@ -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));

Expand All @@ -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');
});
});
20 changes: 13 additions & 7 deletions cli/src/auth-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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<string>((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`
Expand Down
4 changes: 2 additions & 2 deletions cli/src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,7 @@ export abstract class BaseCommand<T extends typeof Command> extends Command {
}
}

async obtainKey() {
async obtainKey(webHost: string) {
const { decision } = await this.prompt({
type: 'select',
name: 'decision',
Expand All @@ -551,7 +551,7 @@ export abstract class BaseCommand<T extends typeof Command> 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',
Expand Down
6 changes: 4 additions & 2 deletions cli/src/commands/auth/login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down
11 changes: 8 additions & 3 deletions cli/src/commands/auth/login.ts
Original file line number Diff line number Diff line change
@@ -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<typeof Login> {
static description = 'Authenticate with Xata';
Expand All @@ -13,6 +13,9 @@ export default class Login extends BaseCommand<typeof Login> {
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'
})
Expand Down Expand Up @@ -42,11 +45,13 @@ export default class Login extends BaseCommand<typeof Login> {
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');
}
Expand Down
2 changes: 1 addition & 1 deletion cli/src/commands/init/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ export default class Init extends BaseCommand<typeof Init> {
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;
Expand Down
2 changes: 1 addition & 1 deletion cli/src/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export function buildProfile(base: Partial<Credential> & { 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'
};
}

0 comments on commit c4d08b8

Please sign in to comment.