Add API token management for CLI authentication
- Add reviq auth login --token <token> command for CLI authentication - Create /account/api-tokens page for token management (superuser only) - Add me.apiTokens endpoints (list, create, delete) - Require superuser status and trusted session for token creation - Show API Tokens nav link only for superusers Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,17 +1,22 @@
|
||||
import type { LocalContext } from "../../context.js";
|
||||
import { buildCommand } from "@stricli/core";
|
||||
import { readConfig } from "../../utils/config.js";
|
||||
import { generateToken, hashToken } from "../../utils/token.js";
|
||||
import { createApiClient } from "../../utils/api-client.js";
|
||||
import { readConfig, writeConfig } from "../../utils/config.js";
|
||||
|
||||
interface LoginFlags {
|
||||
email: string;
|
||||
token: string;
|
||||
"api-url"?: string;
|
||||
}
|
||||
|
||||
interface LoginStatusOutput {
|
||||
status: "pending" | "completed" | "expired";
|
||||
}
|
||||
|
||||
/**
|
||||
* Login to RevIQ with an API token
|
||||
*
|
||||
* To get an API token:
|
||||
* 1. Log in to the web dashboard
|
||||
* 2. Go to Account Settings > API Tokens
|
||||
* 3. Create a new token and copy it
|
||||
* 4. Run: reviq auth login --token <your-token>
|
||||
*/
|
||||
async function login(this: LocalContext, flags: LoginFlags): Promise<void> {
|
||||
const apiUrl = flags["api-url"] ?? "http://localhost:9861";
|
||||
|
||||
@@ -23,117 +28,31 @@ async function login(this: LocalContext, flags: LoginFlags): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Starting login flow...\n");
|
||||
|
||||
// Generate a unique callback token for this login request
|
||||
const callbackToken = generateToken();
|
||||
const callbackTokenHash = hashToken(callbackToken);
|
||||
console.log("Validating API token...\n");
|
||||
|
||||
try {
|
||||
// Create login request
|
||||
const createResponse = await fetch(
|
||||
`${apiUrl}/api/v1/rpc/auth.createLoginRequest`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ email: flags.email }),
|
||||
},
|
||||
);
|
||||
// Create a temporary API client with the provided token
|
||||
const api = createApiClient(apiUrl, flags.token);
|
||||
|
||||
if (!createResponse.ok) {
|
||||
const text = await createResponse.text();
|
||||
console.error(`Error creating login request: ${text}`);
|
||||
this.process.exit(1);
|
||||
}
|
||||
// Validate the token by fetching the user's auth status
|
||||
const authStatus = await api.me.authStatus();
|
||||
|
||||
// Construct the login URL
|
||||
const loginUrl = new URL(`${apiUrl}/login`);
|
||||
loginUrl.searchParams.set("email", flags.email);
|
||||
loginUrl.searchParams.set("cli_callback", callbackTokenHash);
|
||||
// Save credentials
|
||||
await writeConfig({
|
||||
apiUrl,
|
||||
token: flags.token,
|
||||
email: authStatus.user.email,
|
||||
});
|
||||
|
||||
console.log("Opening browser for authentication...");
|
||||
console.log(`\nIf the browser doesn't open, visit:`);
|
||||
console.log(` ${loginUrl.toString()}\n`);
|
||||
|
||||
// Try to open the browser
|
||||
const openCommand =
|
||||
process.platform === "darwin"
|
||||
? "open"
|
||||
: process.platform === "win32"
|
||||
? "start"
|
||||
: "xdg-open";
|
||||
|
||||
try {
|
||||
const proc = Bun.spawn([openCommand, loginUrl.toString()], {
|
||||
stdout: "ignore",
|
||||
stderr: "ignore",
|
||||
});
|
||||
await proc.exited;
|
||||
} catch {
|
||||
// Ignore errors opening browser - user can use the URL
|
||||
}
|
||||
|
||||
console.log("Waiting for login to complete...");
|
||||
console.log("(Press Ctrl+C to cancel)\n");
|
||||
|
||||
// Poll for completion
|
||||
const maxAttempts = 120; // 2 minutes at 1 second intervals
|
||||
let attempts = 0;
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
attempts++;
|
||||
|
||||
try {
|
||||
const statusResponse = await fetch(
|
||||
`${apiUrl}/api/v1/rpc/auth.loginIfRequestIsCompleted`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CLI-Callback-Token": callbackToken,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (statusResponse.ok) {
|
||||
const status = (await statusResponse.json()) as LoginStatusOutput;
|
||||
|
||||
if (status.status === "completed") {
|
||||
// Login completed - we should have received a token
|
||||
// For now, we'll need the API to return the token
|
||||
console.log("Login completed successfully!");
|
||||
|
||||
// TODO: The API needs to return the session token when login completes
|
||||
// For now, this is a placeholder
|
||||
console.log(
|
||||
"\nNote: Browser-based login flow requires API integration.",
|
||||
);
|
||||
console.log("Use 'reviq bootstrap' to create initial credentials.");
|
||||
return;
|
||||
}
|
||||
if (status.status === "expired") {
|
||||
console.error("Login request expired. Please try again.");
|
||||
this.process.exit(1);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore polling errors and continue
|
||||
}
|
||||
|
||||
// Show progress indicator
|
||||
process.stdout.write(".");
|
||||
}
|
||||
|
||||
console.log("\n\nLogin timed out. Please try again.");
|
||||
this.process.exit(1);
|
||||
console.log(`Logged in as ${authStatus.user.email}`);
|
||||
console.log("Credentials saved to ~/.config/reviq/credentials.json");
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Error:",
|
||||
"Login failed:",
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
console.log("\nMake sure your API token is valid.");
|
||||
console.log("You can create a new token at: /account/api-tokens");
|
||||
this.process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -142,10 +61,10 @@ export const loginCommand = buildCommand({
|
||||
func: login,
|
||||
parameters: {
|
||||
flags: {
|
||||
email: {
|
||||
token: {
|
||||
kind: "parsed",
|
||||
parse: String,
|
||||
brief: "Email address to login with",
|
||||
brief: "API token from the web dashboard",
|
||||
},
|
||||
"api-url": {
|
||||
kind: "parsed",
|
||||
@@ -156,8 +75,13 @@ export const loginCommand = buildCommand({
|
||||
},
|
||||
},
|
||||
docs: {
|
||||
brief: "Login to RevIQ",
|
||||
fullDescription:
|
||||
"Opens a browser to complete authentication and stores the credentials locally.",
|
||||
brief: "Login to RevIQ with an API token",
|
||||
fullDescription: `Authenticates with RevIQ using an API token.
|
||||
|
||||
To get an API token:
|
||||
1. Log in to the web dashboard at http://localhost:9861
|
||||
2. Go to Account Settings > API Tokens
|
||||
3. Create a new token and copy it
|
||||
4. Run: reviq auth login --token <your-token>`,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user