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
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"file-type": "^15.0.0",
"form-data": "^4.0.2",
"fs-extra": "^11.3.0",
"get-port": "^7.1.0",
"livereload": "^0.9.3",
"open": "^8.4.0",
"prettier": "^2.8.8",
Expand Down
4 changes: 2 additions & 2 deletions src/actions/portal/recipe/new-recipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ export class PortalRecipeAction {
// Check if the file exists
if (!fs.existsSync(tocFilePath)) {
return Result.failure<any, string>(
`TOC file not found at ${tocFilePath}. Please run 'apimatic:toc:new' to create your TOC file first.`
`toc.yml file not found at ${tocFilePath}. Please run 'apimatic:toc:new' to create your toc.yml file first.`
);
}

Expand All @@ -243,7 +243,7 @@ export class PortalRecipeAction {
return Result.success(parse(tocContent));
} catch {
return Result.failure(
`Unable to parse the TOC file located at ${tocFilePath}. Please make sure that the TOC is a valid YAML file.`
`Unable to parse the toc.yml file located at ${tocFilePath}. Please make sure that the toc.yml is a valid YAML file.`
);
}
}
Expand Down
24 changes: 2 additions & 22 deletions src/client-utils/sdk-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,8 @@ export class SDKClient {
*/

public async login(email: string, password: string, configDir: string): Promise<string> {
let storedAuthInfo: AuthInfo | null = await getAuthInfo(configDir);

// If no config file or no credentials exist in the config file
if (!storedAuthInfo) {
storedAuthInfo = { email: "", authKey: "" };
}

const credentials: Credentials = { email, password };
const authKey: string = await this.getAuthKey(credentials);

if (storedAuthInfo.email !== email) {
setAuthInfo(
{
email,
Expand All @@ -56,19 +47,8 @@ export class SDKClient {
configDir
);

return "Logged in";
} else if (authKey === storedAuthInfo.authKey) {
return "Already logged in";
} else {
setAuthInfo(
{
email,
authKey
},
configDir
);
return "Logged in";
}
return "✅ Logged in successfully as " + email;

}

public async logout(configDir: string): Promise<string> {
Expand Down
63 changes: 38 additions & 25 deletions src/commands/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Flags, Command } from "@oclif/core";
import { outro, password, text } from "@clack/prompts";
import { getMessageInRedColor, replaceHTML } from "../../utils/utils.js";
import { SDKClient } from "../../client-utils/sdk-client.js";
import { getAuthInfo } from "../../client-utils/auth-manager.js";

export default class Login extends Command {
static description = "Login using your APIMatic credentials or an API Key";
Expand All @@ -26,43 +27,55 @@ Authentication key successfully set`
const { flags } = await this.parse(Login);
const configDir: string = this.config.configDir;
try {
const client: SDKClient = SDKClient.getInstance();
// Check if already logged in
const storedAuthInfo = await getAuthInfo(configDir);
if (storedAuthInfo && storedAuthInfo.authKey) {
if (storedAuthInfo.email) {
return this.log(
`You are already logged in as '${storedAuthInfo.email}'. Use auth:logout to logout before logging in again.`
);
}
return this.log(
`You are already logged in with authentication key. Use auth:logout to logout before logging in again.`
);
}

const client: SDKClient = SDKClient.getInstance();
// If user is setting auth key
if (flags["auth-key"]) {
const response = client.setAuthKey(flags["auth-key"], configDir);
return this.log(response);
} else {
// If user logs in with email and password
const email = await text({
message: "Enter your registered email:",
validate: (input) => {
if (!input) {
return getMessageInRedColor("Email is required.");
}
}

const emailRegex =
/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
// If user logs in with email and password
const email = await text({
message: "Enter your registered email:",
validate: (input) => {
if (!input) {
return getMessageInRedColor("Email is required.");
}

const emailRegex =
/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;

if (!emailRegex.test(input)) {
return getMessageInRedColor("Please enter a valid email address.");
}
if (!emailRegex.test(input)) {
return getMessageInRedColor("Please enter a valid email address.");
}
});
}
});

const pass = await password({
message: "Please enter your password:",
validate: (input) => {
if (!input) {
return getMessageInRedColor("Password is required.");
}
const pass = await password({
message: "Please enter your password:",
validate: (input) => {
if (!input) {
return getMessageInRedColor("Password is required.");
}
});
}
});

const response: string = await client.login(email as string, pass as string, configDir);
const response: string = await client.login(email as string, pass as string, configDir);

outro(response);
}
outro(response);
} catch (error) {
if (error && (error as AxiosError).response) {
const apiError = error as AxiosError;
Expand Down
17 changes: 15 additions & 2 deletions src/commands/portal/serve.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as path from "path";
import axios from "axios";
import { Command, Flags } from "@oclif/core";
import getPort from 'get-port';
import { generatePortal } from "../../controllers/portal/serve.js";
import { PortalServerService } from "../../services/portal/server.js";
import { PortalServePrompts } from "../../prompts/portal/serve.js";
Expand All @@ -14,7 +15,6 @@ export default class PortalServe extends Command {
port: Flags.integer({
char: "p",
description: "Port to serve the portal.",
default: 3000
}),
destination: Flags.string({
char: "d",
Expand Down Expand Up @@ -57,12 +57,25 @@ export default class PortalServe extends Command {
const ignoredPaths = flags.ignore.split(",").map((path) => path.trim());
const portalDir = path.resolve(flags.destination);
const sourceDir = path.resolve(flags.source);
const port = flags.port;
const overrideAuthKey = flags["auth-key"] ?? null;
const serverService = new PortalServerService();
const prompts = new PortalServePrompts();
const validator = new PortalServeValidator(this.error);
const allIgnoredPaths = [...ignoredPaths, ...getGeneratedFilesPaths(sourceDir, portalDir)];
const defaultPorts = [3000, 3001, 3002];

const preferredPorts = typeof flags.port === 'number'
? [flags.port, ...defaultPorts.filter(p => p !== flags.port)]
: defaultPorts;

const availablePort = await getPort({ port: preferredPorts });

// Show warning only if user provided --port and it is not available
if (typeof flags.port === 'number' && availablePort !== flags.port) {
this.log(`⚠️ Port ${flags.port} is already in use. Available port ${availablePort} will be used.`);
}
const port = availablePort;


await validator.validate(port, flags.destination, sourceDir, portalDir);

Expand Down
8 changes: 4 additions & 4 deletions src/commands/portal/toc/new.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ https://docs.apimatic.io/platform-api/#/http/guides/generating-on-prem-api-porta
static flags = {
destination: Flags.string({
parse: async (input: string) => path.resolve(input),
description: "optional path where the generated TOC file will be saved. Defaults to the current working directory if not provided.",
description: "optional path where the generated toc.yml file will be saved. Defaults to the current working directory if not provided.",
}),
folder: Flags.string({
parse: async (input: string) => path.resolve(input),
Expand All @@ -27,15 +27,15 @@ https://docs.apimatic.io/platform-api/#/http/guides/generating-on-prem-api-porta
}),
force: Flags.boolean({
default: false,
description: "overwrite the TOC file if one already exists at the destination.",
description: "overwrite the toc.yml file if one already exists at the destination.",
}),
"expand-endpoints": Flags.boolean({
default: false,
description: "include individual entries for each endpoint in the generated TOC. Requires a valid API specification in the working directory."
description: "include individual entries for each endpoint in the generated toc.yml. Requires a valid API specification in the working directory."
}),
"expand-models": Flags.boolean({
default: false,
description: "include individual entries for each model in the generated TOC. Requires a valid API specification in the working directory."
description: "include individual entries for each model in the generated toc.yml. Requires a valid API specification in the working directory."
})
};

Expand Down
25 changes: 19 additions & 6 deletions src/prompts/portal/quickstart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,19 +86,31 @@ export class PortalQuickstartPrompts {
this.spin.stop(getMessageInCyanColor("✅ Login successful!"));
}

removeQuotes(str: string) {
const quotes = ['"', "'"];

for (const quote of quotes) {
if (str.startsWith(quote) && str.endsWith(quote) && str.length > 1) {
return str.slice(1, -1);
}
}
return str;
}

async specPrompt(): Promise<string> {
log.step(getMessageInOrangeColor(`Step 1 of 4: Import your OpenAPI Definition`));

const spec = await text({
message: `Provide a local path or a public URL for your OpenAPI Definition file:`,
placeholder: "Press Enter to use a sample OpenAPI file for APIMatic",
message: `Provide a local path or a public URL for your OpenAPI definition file:`,
placeholder: "Provide absolute URL/local path or press Enter to use sample OpenAPI file from APIMatic.",
defaultValue: "https://raw.githubusercontent.com/apimatic/static-portal-workflow/refs/heads/master/spec/Apimatic-Calculator.json",
validate: (input) => {
if (!input) return;

if (isValidUrl(input)) return;

const dirPath = path.resolve((input ?? "").trim());
const cleanedPath = this.removeQuotes(input.trim() ?? "");
const dirPath = path.resolve(cleanedPath);

if (fs.existsSync(dirPath)) {
if (fs.statSync(dirPath).isFile()) {
Expand Down Expand Up @@ -201,11 +213,12 @@ export class PortalQuickstartPrompts {
log.step(getMessageInOrangeColor(`Step 4 of 4: Generate source files for Docs as Code`));

const directory = await text({
message: "Enter the directory path where you would like to setup the API Portal :",
placeholder: "Enter absolute path to the directory or leave it empty to use the current directory.",
message: "Enter the directory path where you would like to setup the API Portal (Requires an empty directory):",
placeholder: "Provide absolute path to the directory or press Enter to use the current directory.",
defaultValue: "./",
validate: (input) => {
const dirPath = path.resolve((input ?? "").trim());
const cleanedPath = this.removeQuotes(input?.trim() ?? "");
const dirPath = path.resolve(cleanedPath);

if (!fs.existsSync(dirPath) && dirPath != this.defaultPortalDirectoryPath) {
return getMessageInRedColor("Error: The specified directory path does not exist. Please try again.");
Expand Down
4 changes: 1 addition & 3 deletions src/prompts/portal/recipe/new-recipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,7 @@ export class PortalRecipePrompts {
}

public displayRecipeGenerationSuccessMessage(buildDirectoryPath: string) {
log.step(`🎉 Recipe has been added successfully!`);
log.message(`📦 Generated recipe has been added to build directory at: ${buildDirectoryPath}`);
log.message(`🎉 Generated recipe has been added to build directory at: ${buildDirectoryPath}`);
outro(
`▶ Run the command 'apimatic portal:serve' to preview your documentation portal.`
);
Expand All @@ -232,7 +231,6 @@ export class PortalRecipePrompts {
.join("\n");

log.step(`🛠️ You can edit the following files to customize your API Recipe :\n\n` + coloredLogString);
log.message(`💡 Modify the TOC file to change the position of the API Recipes section in the navbar.`);
}

public logError(error: string): void {
Expand Down
6 changes: 3 additions & 3 deletions src/prompts/portal/toc/new-toc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export class PortalNewTocPrompts {

async overwriteExistingTocPrompt(): Promise<boolean> {
const useExistingFile = await select({
message: `A toc file already exists at the specified destination path, do you want to overwrite it?`,
message: `A toc.yml file already exists at the specified destination path, do you want to overwrite it?`,
options: [
{ value: "yes", label: "Yes" },
{ value: "no", label: "No" }
Expand All @@ -18,7 +18,7 @@ export class PortalNewTocPrompts {
}

if (useExistingFile === "no") {
outro("Please enter a different destination path or delete the existing toc file and try again.");
outro("Please enter a different destination path or delete the existing toc.yml file and try again.");
}

return useExistingFile === "yes";
Expand All @@ -33,7 +33,7 @@ export class PortalNewTocPrompts {
}

displayOutroMessage(tocPath: string): void {
outro(`✅ TOC file successfully created at: ${tocPath}`);
outro(`✅ toc.yml file successfully created at: ${tocPath}`);
}

logError(error: string): void {
Expand Down
5 changes: 5 additions & 0 deletions src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,11 @@ export async function validateAndZipPortalSource(
outputPath: string,
ignoredPaths: string[] = []
): Promise<string> {

if (await fs.pathExists(outputPath)){
await deleteFile(outputPath);
}

const output = fs.createWriteStream(outputPath);
const archive = archiver("zip", {
zlib: { level: 9 }
Expand Down