Skip to content
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
18 changes: 15 additions & 3 deletions src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ type Login = {
pending?: PendingLogin;
/** Set on Ctrl-C, so its cleanup doesn't read as a logout. */
cancelled?: boolean;
/** Set once the login is saved, so a later Ctrl-C doesn't call it cancelled. */
saved?: SavedLogin;
};

type SavedLogin = Pick<StoredLogin, "store" | "storeReason"> & { token: string };
Expand Down Expand Up @@ -214,9 +216,18 @@ export async function logIn(options: { browser?: boolean } = {}): Promise<string
process.once("SIGINT", async () => {
login.cancelled = true;
console.error();
// Waits for a save in progress, which holds the lock.
if (login.pending) await setPendingLogin(login.pending, undefined);
recordLogin({ result: "cancelled", resumed: login.resumed, startedAt: login.startedAt });
await endCommand("Login cancelled.", 130);
const { resumed, startedAt, saved } = login;
const result = saved ? "success" : "cancelled";
recordLogin({
result,
resumed,
startedAt,
store: saved?.store,
storeReason: saved?.storeReason,
});
await endCommand(saved ? "Logged in to Prismic." : "Login cancelled.", 130);
});

let outcome: LoginOutcome;
Expand Down Expand Up @@ -464,13 +475,14 @@ async function saveLogin(
: await prepareLogin(lock);
const session = { host: login.host, ...login.auth0, ...tokens };
await writeCredentials(lock, { ...store, session });
login.saved = { ...store, token: tokens.accessToken };
// The new login is saved first, so a failed revoke only leaves the
// previous session to expire.
const previous = stored?.login?.session;
if (previous?.refreshToken) {
await revokeRefreshToken(previous.refreshToken, previous).catch(() => {});
}
return { ...store, token: tokens.accessToken };
return login.saved;
});
}

Expand Down
4 changes: 3 additions & 1 deletion test/auth0.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { randomBytes } from "node:crypto";
import { createServer, type ServerResponse } from "node:http";
import type { AddressInfo } from "node:net";
import { setTimeout as sleep } from "node:timers/promises";

// A fake Auth0 tenant for login tests. A real device login needs a person to
// approve it in a browser, so automated tests can't use Auth0 itself.

const INTERVAL = 0.1;

type FakeAuth0Response = { status: number; body?: unknown };
type FakeAuth0Response = { status: number; body?: unknown; delayMs?: number };

export type FakeAuth0 = {
auth0Domain: string;
Expand Down Expand Up @@ -48,6 +49,7 @@ export async function createFakeAuth0(options: { accessToken: string }): Promise

const response = responses.get(path)?.shift();
if (response) {
await sleep(response.delayMs ?? 0);
send(res, response.status, response.body);
return;
}
Expand Down
17 changes: 17 additions & 0 deletions test/login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,23 @@ it.skipIf(process.platform === "win32")(
},
);

it.skipIf(process.platform === "win32")(
"reports a login that saved before Ctrl-C as saved",
async ({ expect, prismic, login, auth0 }) => {
// The new login revokes this session while it still holds the lock after saving.
await login({ refreshToken: auth0.issueRefreshToken() });
auth0.respondOnce("/oauth/revoke", { status: 200, delayMs: 2000 });
const proc = prismic("login");
const output = captureOutput(proc);
await approveLogin(expect, output, auth0);
await expect.poll(() => auth0.requestsTo("/oauth/revoke")).toHaveLength(1);
proc.kill("SIGINT");
expect((await proc).exitCode).toBe(130);
expect(output()).toContain("Logged in to Prismic.");
expect(output()).not.toContain("Login cancelled.");
},
);

it("makes another command wait for the login in progress", async ({
expect,
prismic,
Expand Down
Loading