Skip to content
12 changes: 12 additions & 0 deletions packages/cli/src/cli/commands/auth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Command } from "commander";
import { getPasswordLoginCommand } from "./password-login.js";
import { getAuthPullCommand } from "./pull.js";
import { getAuthPushCommand } from "./push.js";

export function getAuthCommand(): Command {
return new Command("auth")
.description("Manage app authentication settings")
.addCommand(getPasswordLoginCommand())
.addCommand(getAuthPullCommand())
.addCommand(getAuthPushCommand());
}
73 changes: 73 additions & 0 deletions packages/cli/src/cli/commands/auth/password-login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { dirname, join } from "node:path";
import type { Command } from "commander";
import type { CLIContext, RunCommandResult } from "@/cli/types.js";
import { Base44Command, runTask } from "@/cli/utils/index.js";
import { InvalidInputError } from "@/core/errors.js";
import { readProjectConfig } from "@/core/project/index.js";
import {
DEFAULT_AUTH_CONFIG,
hasAnyLoginMethod,
readAuthConfig,
writeAuthConfig,
} from "@/core/resources/auth-config/index.js";

function validateAction(
action: string,
): asserts action is "enable" | "disable" {
if (action !== "enable" && action !== "disable") {
throw new InvalidInputError(
`Invalid action "${action}". Must be "enable" or "disable".`,
{
hints: [
{
message: "Enable password auth: base44 auth password-login enable",
command: "base44 auth password-login enable",
},
{
message:
"Disable password auth: base44 auth password-login disable",
command: "base44 auth password-login disable",
},
],
},
);
}
}

async function passwordLoginAction(
{ log }: CLIContext,
action: string,
): Promise<RunCommandResult> {
validateAction(action);

const shouldEnable = action === "enable";
const { project } = await readProjectConfig();

const configDir = dirname(project.configPath);
const authDir = join(configDir, project.authDir);

const updated = await runTask("Updating local auth config", async () => {
const current = (await readAuthConfig(authDir)) ?? DEFAULT_AUTH_CONFIG;
const merged = { ...current, enableUsernamePassword: shouldEnable };
await writeAuthConfig(authDir, merged);
return merged;
});

if (!shouldEnable && !hasAnyLoginMethod(updated)) {
log.warn(
"Disabling password auth will leave no login methods enabled. Users will be locked out.",
);
}

const newStatus = shouldEnable ? "enabled" : "disabled";
return {
outroMessage: `Username & password authentication ${newStatus} in local config. Run \`base44 auth push\` or \`base44 deploy\` to apply.`,
};
}

export function getPasswordLoginCommand(): Command {
return new Base44Command("password-login")
.description("Enable or disable username & password authentication")
.argument("<enable|disable>", "enable or disable password authentication")
.action(passwordLoginAction);
}
54 changes: 54 additions & 0 deletions packages/cli/src/cli/commands/auth/pull.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { dirname, join } from "node:path";
import type { Command } from "commander";
import type { CLIContext, RunCommandResult } from "@/cli/types.js";
import { Base44Command, runTask } from "@/cli/utils/index.js";
import { readProjectConfig } from "@/core/project/index.js";
import {
pullAuthConfig,
writeAuthConfig,
} from "@/core/resources/auth-config/index.js";

async function pullAuthAction({ log }: CLIContext): Promise<RunCommandResult> {
const { project } = await readProjectConfig();

const configDir = dirname(project.configPath);
const authDir = join(configDir, project.authDir);

const remoteConfig = await runTask(
"Fetching auth config from Base44",
async () => {
return await pullAuthConfig();
},
{
successMessage: "Auth config fetched successfully",
errorMessage: "Failed to fetch auth config",
},
);

const { written } = await runTask(
"Syncing auth config file",
async () => {
return await writeAuthConfig(authDir, remoteConfig);
},
{
successMessage: "Auth config file synced successfully",
errorMessage: "Failed to sync auth config file",
},
);

if (written) {
log.success("Auth config written to local file");
} else {
log.info("Auth config is already up to date");
}

return {
outroMessage: `Pulled auth config to ${authDir} (overwrites local file)`,
};
}

export function getAuthPullCommand(): Command {
return new Base44Command("pull")
.description("Pull auth config from Base44 to local file")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might worth adding some text about overwriting existing file

.action(pullAuthAction);
}
71 changes: 71 additions & 0 deletions packages/cli/src/cli/commands/auth/push.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { confirm, isCancel } from "@clack/prompts";
import type { Command } from "commander";
import type { CLIContext, RunCommandResult } from "@/cli/types.js";
import { Base44Command, runTask } from "@/cli/utils/index.js";
import { InvalidInputError } from "@/core/errors.js";
import { readProjectConfig } from "@/core/project/index.js";
import {
hasAnyLoginMethod,
pushAuthConfig,
} from "@/core/resources/auth-config/index.js";

interface PushAuthOptions {
yes?: boolean;
}

async function pushAuthAction(
{ isNonInteractive, log }: CLIContext,
options: PushAuthOptions,
): Promise<RunCommandResult> {
const { authConfig } = await readProjectConfig();

if (authConfig.length === 0) {
log.info("No local auth config found");
return {
outroMessage:
"No auth config to push. Run `base44 auth pull` to fetch the remote config first.",
};
}

if (!hasAnyLoginMethod(authConfig[0])) {
log.warn(
"This config has no login methods enabled. Pushing it will lock out all users.",
);
}

if (!options.yes) {
if (isNonInteractive) {
throw new InvalidInputError("--yes is required in non-interactive mode");
}

const shouldPush = await confirm({
message: "Push auth config to Base44?",
});

if (isCancel(shouldPush) || !shouldPush) {
return { outroMessage: "Push cancelled" };
}
}

await runTask(
"Pushing auth config to Base44",
async () => {
return await pushAuthConfig(authConfig[0] ?? null);
},
{
successMessage: "Auth config pushed successfully",
errorMessage: "Failed to push auth config",
},
);

return {
outroMessage: "Auth config pushed to Base44",
};
}

export function getAuthPushCommand(): Command {
return new Base44Command("push")
.description("Push local auth config to Base44")
.option("-y, --yes", "Skip confirmation prompt")
.action(pushAuthAction);
}
6 changes: 5 additions & 1 deletion packages/cli/src/cli/commands/project/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ export async function deployAction(
};
}

const { project, entities, functions, agents, connectors } = projectData;
const { project, entities, functions, agents, connectors, authConfig } =
projectData;

// Build summary of what will be deployed
const summaryLines: string[] = [];
Expand All @@ -69,6 +70,9 @@ export async function deployAction(
` - ${connectors.length} ${connectors.length === 1 ? "connector" : "connectors"}`,
);
}
if (authConfig.length > 0) {
summaryLines.push(" - Auth config");
}
if (project.site?.outputDirectory) {
summaryLines.push(` - Site from ${project.site.outputDirectory}`);
}
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/cli/program.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Command } from "commander";
import { getAgentsCommand } from "@/cli/commands/agents/index.js";
import { getAuthCommand } from "@/cli/commands/auth/index.js";
import { getLoginCommand } from "@/cli/commands/auth/login.js";
import { getLogoutCommand } from "@/cli/commands/auth/logout.js";
import { getWhoamiCommand } from "@/cli/commands/auth/whoami.js";
Expand Down Expand Up @@ -69,6 +70,9 @@ export function createProgram(context: CLIContext): Command {
// Register secrets commands
program.addCommand(getSecretsCommand());

// Register auth config commands
program.addCommand(getAuthCommand(), { hidden: true });

// Register site commands
program.addCommand(getSiteCommand());

Expand Down
16 changes: 10 additions & 6 deletions packages/cli/src/core/project/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ConfigNotFoundError, SchemaValidationError } from "@/core/errors.js";
import { ProjectConfigSchema } from "@/core/project/schema.js";
import type { ProjectData, ProjectRoot } from "@/core/project/types.js";
import { agentResource } from "@/core/resources/agent/index.js";
import { authConfigResource } from "@/core/resources/auth-config/index.js";
import { connectorResource } from "@/core/resources/connector/index.js";
import { entityResource } from "@/core/resources/entity/index.js";
import { functionResource } from "@/core/resources/function/index.js";
Expand Down Expand Up @@ -92,18 +93,21 @@ export async function readProjectConfig(
const project = result.data;
const configDir = dirname(configPath);

const [entities, functions, agents, connectors] = await Promise.all([
entityResource.readAll(join(configDir, project.entitiesDir)),
functionResource.readAll(join(configDir, project.functionsDir)),
agentResource.readAll(join(configDir, project.agentsDir)),
connectorResource.readAll(join(configDir, project.connectorsDir)),
]);
const [entities, functions, agents, connectors, authConfig] =
await Promise.all([
entityResource.readAll(join(configDir, project.entitiesDir)),
functionResource.readAll(join(configDir, project.functionsDir)),
agentResource.readAll(join(configDir, project.agentsDir)),
connectorResource.readAll(join(configDir, project.connectorsDir)),
authConfigResource.readAll(join(configDir, project.authDir)),
]);

return {
project: { ...project, root, configPath },
entities,
functions,
agents,
connectors,
authConfig,
};
}
18 changes: 15 additions & 3 deletions packages/cli/src/core/project/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { resolve } from "node:path";
import type { ProjectData } from "@/core/project/types.js";
import { agentResource } from "@/core/resources/agent/index.js";
import { authConfigResource } from "@/core/resources/auth-config/index.js";
import {
type ConnectorSyncResult,
pushConnectors,
Expand All @@ -19,14 +20,23 @@ import { deploySite } from "@/core/site/index.js";
* @returns true if there are entities, functions, agents, connectors, or a configured site to deploy
*/
export function hasResourcesToDeploy(projectData: ProjectData): boolean {
const { project, entities, functions, agents, connectors } = projectData;
const { project, entities, functions, agents, connectors, authConfig } =
projectData;
const hasSite = Boolean(project.site?.outputDirectory);
const hasEntities = entities.length > 0;
const hasFunctions = functions.length > 0;
const hasAgents = agents.length > 0;
const hasConnectors = connectors.length > 0;
const hasAuthConfig = authConfig.length > 0;

return hasEntities || hasFunctions || hasAgents || hasConnectors || hasSite;
return (
hasEntities ||
hasFunctions ||
hasAgents ||
hasConnectors ||
hasAuthConfig ||
hasSite
);
}

/**
Expand Down Expand Up @@ -59,14 +69,16 @@ export async function deployAll(
projectData: ProjectData,
options?: DeployAllOptions,
): Promise<DeployAllResult> {
const { project, entities, functions, agents, connectors } = projectData;
const { project, entities, functions, agents, connectors, authConfig } =
projectData;

await entityResource.push(entities);
await deployFunctionsSequentially(functions, {
onStart: options?.onFunctionStart,
onResult: options?.onFunctionResult,
});
await agentResource.push(agents);
await authConfigResource.push(authConfig);
const { results: connectorResults } = await pushConnectors(connectors);

if (project.site?.outputDirectory) {
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/core/project/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const ProjectConfigSchema = z.object({
functionsDir: z.string().optional().default("functions"),
agentsDir: z.string().optional().default("agents"),
connectorsDir: z.string().optional().default("connectors"),
authDir: z.string().optional().default("auth"),
});

export type ProjectConfig = z.infer<typeof ProjectConfigSchema>;
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/core/project/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ProjectConfig } from "@/core/project/schema.js";
import type { AgentConfig } from "@/core/resources/agent/index.js";
import type { AuthConfig } from "@/core/resources/auth-config/index.js";
import type { ConnectorResource } from "@/core/resources/connector/index.js";
import type { Entity } from "@/core/resources/entity/index.js";
import type { BackendFunction } from "@/core/resources/function/index.js";
Expand All @@ -20,4 +21,5 @@ export interface ProjectData {
functions: BackendFunction[];
agents: AgentConfig[];
connectors: ConnectorResource[];
authConfig: AuthConfig[];
}
Loading
Loading