Skip to content

Commit

Permalink
feat(cli): add auto login flow (tryabby#57)
Browse files Browse the repository at this point in the history
* feat(cli): add auto login flow

* feat(web): improve generate-token page

* fix(cli): remove console.log

* feat(cli): add init function

* fix build and add cli docs
  • Loading branch information
cstrnt authored Aug 29, 2023
1 parent 98e8ce5 commit eb88278
Show file tree
Hide file tree
Showing 11 changed files with 269 additions and 24 deletions.
10 changes: 10 additions & 0 deletions apps/docs/pages/reference/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ Authenticate yourself with the A/BBY API.
| --------------- | ------------- | ----------- | -------- |
| `-t`, `--token` | Your API key. | `undefined` ||

### `init`

Create a new config file in the current directory. The config file will be named `abby.config.ts`.

#### Options

| Flag | Description | Default | Required |
| ---------------- | ---------------------------- | ---------------------- | -------- |
| `-c`, `--config` | The path to the config file. | Your current directory ||

### `push`

Push the changes from your local config to the A/BBY API.
Expand Down
9 changes: 7 additions & 2 deletions apps/web/src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default withAuth(
const pathName = req.nextUrl.pathname;

// basic auth check for /profile
if (pathName === "/profile") return token !== null;
if (pathName.startsWith("/profile")) return token !== null;

if (!pathName.startsWith("/projects")) return true;
const projectId = req.nextUrl.pathname.split("/")[2];
Expand All @@ -41,5 +41,10 @@ export default withAuth(
) as NextMiddleware;

export const config = {
matcher: ["/projects/:path*", "/marketing", "/profile"],
matcher: [
"/projects/:path*",
"/marketing",
"/profile",
"/profile/generate-token",
],
};
56 changes: 56 additions & 0 deletions apps/web/src/pages/profile/generate-token.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useQuery } from "@tanstack/react-query";
import { Layout } from "components/Layout";
import { GetServerSideProps, InferGetServerSidePropsType } from "next";
import { createContext } from "server/trpc/context";
import { appRouter } from "server/trpc/router/_app";
import { NextPageWithLayout } from "../_app";
import Logo from "components/Logo";
import { match } from "ts-pattern";

const GenerateTokenPage: NextPageWithLayout<
InferGetServerSidePropsType<typeof getServerSideProps>
> = ({ token, callbackUrl }) => {
const { status } = useQuery(["generate-token"], () => {
const url = new URL(callbackUrl as string);
url.searchParams.set("token", token);
return fetch(url);
});

return (
<main className="absolute left-1/2 top-1/2 h-[400px] w-[300px] -translate-x-1/2 -translate-y-1/2 rounded-xl border border-gray-200 p-8 text-center">
<div className="flex h-full w-full flex-col items-center justify-between py-16">
<Logo />
<div>
{match(status)
.with("loading", () => <h1>Loading...</h1>)
.with("error", () => <h1>Something went wrong!</h1>)
.with("success", () => <h1>You can safely close this tab now!</h1>)
.exhaustive()}
</div>
</div>
</main>
);
};

GenerateTokenPage.getLayout = (page) => <Layout hideSidebar>{page}</Layout>;

export const getServerSideProps = (async (ctx) => {
const trpc = appRouter.createCaller(await createContext(ctx as any));

const token = await trpc.apikey.createApiKey({
name: "CLI Token",
});

if (typeof ctx.query.callbackUrl != "string") {
throw new Error("Missing callbackUrl");
}

return {
props: {
token,
callbackUrl: ctx.query.callbackUrl,
},
};
}) satisfies GetServerSideProps;

export default GenerateTokenPage;
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { toast } from "react-hot-toast";
import { BsX } from "react-icons/bs";
import { getSSRTrpc } from "server/trpc/helpers";
import { trpc } from "utils/trpc";
import { NextPageWithLayout } from "./_app";
import { NextPageWithLayout } from "../_app";

const CreateApiKeyModal = ({
isOpen,
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @tryabby/cli

## 0.3.0

### Minor Changes

- add auto login flow

## 0.1.0

### Minor Changes
Expand Down
7 changes: 6 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tryabby/cli",
"version": "0.2.2",
"version": "0.3.0",
"private": false,
"main": "./dist/index.js",
"bin": {
Expand All @@ -22,12 +22,15 @@
"chalk": "^5.2.0",
"clear": "^0.1.0",
"commander": "^10.0.1",
"cors": "^2.8.5",
"deepmerge": "^4.3.1",
"dotenv": "^16.0.3",
"esbuild": "0.18.17",
"figlet": "^1.6.0",
"msw": "^1.2.2",
"node-fetch": "^3.3.1",
"polka": "^0.5.2",
"portfinder": "^1.0.32",
"prettier": "^3.0.0",
"tsup": "^6.5.0",
"unconfig": "^0.3.10",
Expand All @@ -36,8 +39,10 @@
},
"devDependencies": {
"@tryabby/core": "workspace:^",
"@types/cors": "^2.8.13",
"@types/figlet": "^1.5.6",
"@types/node": "^20.3.1",
"@types/polka": "^0.5.4",
"nodemon": "^2.0.22",
"ts-node": "^10.9.1",
"tsconfig": "workspace:*",
Expand Down
40 changes: 35 additions & 5 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { ABBY_BASE_URL, getTokenFilePath } from "./consts";
import { pullAndMerge } from "./pull";
import { push } from "./push";
import { ConfigOption, HostOption } from "./sharedOptions";
import { multiLineLog } from "./util";
import { multiLineLog, startServerAndGetToken } from "./util";
import { initAbbyConfig } from "./init";

const program = new Command();

Expand All @@ -18,15 +19,23 @@ program.name("abby-cli").description("CLI Tool for Abby").version("0.0.1");

program
.command("login")
.addOption(HostOption)
.option("-t, --token <token>", "token")
.action(async ({ token }) => {
if (typeof token === "string") {
await writeTokenFile(token);
.action(async ({ token, host }: { token?: string; host?: string }) => {
let tokenToUse = token;

// the token parameter is optional, if not given we start a login flow
if (typeof token !== "string") {
tokenToUse = await startServerAndGetToken(host);
}

if (typeof tokenToUse === "string") {
await writeTokenFile(tokenToUse);
console.log(chalk.green(`Token successfully written to ${getTokenFilePath()}`));
} else {
console.log(
chalk.red(`You need to provide a token to log in.`),
chalk.green(`\nYou can get one at ${ABBY_BASE_URL}profile`)
chalk.green(`\nYou can get one at ${ABBY_BASE_URL}/profile`)
);
}
});
Expand Down Expand Up @@ -112,4 +121,25 @@ program
}
});

program
.command("init")
.description("create your local config file")
.addOption(ConfigOption)
.action(async (options: { config?: string }) => {
try {
const configPath = options.config ?? "./abby.config.ts";
await initAbbyConfig({ path: configPath });
console.log(chalk.green(`Config file created successfully at ${configPath}`));
} catch (e) {
console.log(
chalk.red(
e instanceof Error
? e.message
: "Something went wrong. Please check your internet connection"
)
);
return;
}
});

program.parse(process.argv);
20 changes: 20 additions & 0 deletions packages/cli/src/init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { AbbyConfigFile } from "@tryabby/core";
import fs from "fs/promises";
import * as prettier from "prettier";

export async function initAbbyConfig({ path }: { path: string }) {
const config: AbbyConfigFile = {
projectId: "<YOUR_PROJECT_ID>",
currentEnvironment: "<YOUR_ENVIRONMENT>",
environments: [],
};

const fileContent = `
import { defineConfig } from '@tryabby/core';
export default defineConfig(${JSON.stringify(config, null, 2)});
`;

await fs.writeFile(path, await prettier.format(fileContent, { parser: "typescript" }));
}
42 changes: 42 additions & 0 deletions packages/cli/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { abbyConfigSchema } from "@tryabby/core";
import { loadConfig } from "unconfig";
import { config as loadEnv } from "dotenv";
import path from "path";
import portFinder from "portfinder";
import polka from "polka";
import { ABBY_BASE_URL } from "./consts";
import cors from "cors";

export async function loadLocalConfig(configPath?: string) {
loadEnv();
Expand Down Expand Up @@ -42,3 +46,41 @@ export async function loadLocalConfig(configPath?: string) {
export function multiLineLog(...args: any[]) {
console.log(args.join("\n"));
}

export async function startServerAndGetToken(host?: string) {
const freePort = await portFinder.getPortPromise();

const url = new URL(host ?? ABBY_BASE_URL);
url.pathname = "/profile/generate-token";
url.searchParams.set("callbackUrl", `http://localhost:${freePort}`);
console.log(`Please open the following URL in your Browser: ${url}`);

return new Promise<string>(async (resolve) => {
const server = polka()
.use(
cors({
origin: "*",
methods: ["GET"],
})
)
.get("/", (req, res) => {
const token = req.query.token;
if (typeof token !== "string") {
res.statusCode = 400;
res.end("Invalid token");
return;
}
res.statusCode = 200;
res.end();

server.server?.close();
resolve(token);
})
.listen(freePort);

process.on("SIGTERM", () => {
server.server?.closeAllConnections();
server.server?.close();
});
});
}
14 changes: 8 additions & 6 deletions packages/core/src/shared/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ export const abbyConfigSchema = z.object({
apiUrl: z.string().optional(),
currentEnvironment: z.string().optional(),
environments: z.array(z.string()),
tests: z.record(
z.object({
variants: z.array(z.string()),
})
),
flags: z.record(flagValueStringSchema),
tests: z
.record(
z.object({
variants: z.array(z.string()),
})
)
.optional(),
flags: z.record(flagValueStringSchema).optional(),
settings: z
.object({
flags: z
Expand Down
Loading

0 comments on commit eb88278

Please sign in to comment.