Skip to content

Commit

Permalink
Restructure: Introduce Service Classes (#25)
Browse files Browse the repository at this point in the history
There are several common patterns that suggest these services could
benefit from being organized into classes.

1. Shared State and Configuration:
  - BitteUrls is passed around multiple     
  - Authentication state is managed across multiple functions
  - Configuration like ports and URLs are reused
2. Related Methods:
  - Plugin operations (register/update/delete) are closely related
  - Tunnel operations share setup and cleanup logic
  - Authentication methods are tightly coupled
  
Each Commit will introduce a new class (with successful build at each
step).
  • Loading branch information
bh2smith authored Nov 19, 2024
1 parent 51c49c3 commit 92ed2d8
Show file tree
Hide file tree
Showing 12 changed files with 602 additions and 572 deletions.
13 changes: 6 additions & 7 deletions src/commands/delete.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { Command } from "commander";

import { getBitteUrls } from "../config/constants";
import { validateAndParseOpenApiSpec } from "../services/openapi-service";
import { deletePlugin } from "../services/plugin-service";
import { getAuthentication } from "../services/signer-service";
import { PluginService } from "../services/plugin";
import { deployedUrl } from "../utils/deployed-url";
import { validateAndParseOpenApiSpec } from "../utils/openapi";
import { getSpecUrl, getHostname } from "../utils/url-utils";

export const deleteCommand = new Command()
Expand Down Expand Up @@ -32,15 +30,16 @@ export const deleteCommand = new Command()
console.error("Failed to parse account ID from OpenAPI specification.");
return;
}

const authentication = await getAuthentication(accountId);
const pluginService = new PluginService();
const authentication =
await pluginService.auth.getAuthentication(accountId);
if (!authentication) {
console.error("Authentication failed. Unable to delete the plugin.");
return;
}

try {
await deletePlugin(pluginId, getBitteUrls().BASE_URL);
await pluginService.delete(pluginId);
console.log(`Plugin ${pluginId} deleted successfully.`);
} catch (error) {
console.error("Failed to delete the plugin:", error);
Expand Down
12 changes: 5 additions & 7 deletions src/commands/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { Command } from "commander";

import { getBitteUrls } from "../config/constants";
import { validateAndParseOpenApiSpec } from "../services/openapi-service";
import { registerPlugin, updatePlugin } from "../services/plugin-service";
import { PluginService } from "../services/plugin";
import { deployedUrl } from "../utils/deployed-url";
import { validateAndParseOpenApiSpec } from "../utils/openapi";
import { getSpecUrl, getHostname } from "../utils/url-utils";

export const deployCommand = new Command()
Expand Down Expand Up @@ -33,16 +32,15 @@ export const deployCommand = new Command()
console.error("Failed to parse account ID from OpenAPI specification.");
return;
}
const bitteUrls = getBitteUrls();
const pluginService = new PluginService();
try {
await updatePlugin(id, accountId, bitteUrls.BASE_URL);
await pluginService.update(id, accountId);
console.log(`Plugin ${id} updated successfully.`);
} catch (error) {
console.log("Plugin not found. Attempting to register...");
const result = await registerPlugin({
const result = await pluginService.register({
pluginId: id,
accountId,
bitteUrls,
});
if (result) {
console.log(`Plugin ${id} registered successfully.`);
Expand Down
9 changes: 7 additions & 2 deletions src/commands/dev.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Command } from "commander";

import { startLocalTunnelAndRegister } from "../services/tunnel-service";
import { TunnelService } from "../services/tunnel";
import { detectPort } from "../utils/port-detector";

export const devCommand = new Command()
Expand All @@ -21,5 +21,10 @@ export const devCommand = new Command()
}
console.log(`Detected port: ${port}`);
}
await startLocalTunnelAndRegister(port, options.serveo, options.testnet);
const tunnelService = new TunnelService({
port,
useServeo: options.serveo,
useTestnet: options.testnet,
});
await tunnelService.start();
});
8 changes: 3 additions & 5 deletions src/commands/register.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { Command } from "commander";

import { getBitteUrls } from "../config/constants";
import { validateAndParseOpenApiSpec } from "../services/openapi-service";
import { registerPlugin } from "../services/plugin-service";
import { PluginService } from "../services/plugin";
import { deployedUrl } from "../utils/deployed-url";
import { validateAndParseOpenApiSpec } from "../utils/openapi";
import { getHostname, getSpecUrl } from "../utils/url-utils";

export const registerCommand = new Command()
Expand Down Expand Up @@ -32,10 +31,9 @@ export const registerCommand = new Command()
return;
}

const result = await registerPlugin({
const result = await new PluginService().register({
pluginId,
accountId,
bitteUrls: getBitteUrls(),
});
if (result) {
console.log(`Plugin ${pluginId} registered successfully.`);
Expand Down
15 changes: 7 additions & 8 deletions src/commands/update.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { Command } from "commander";

import { getBitteUrls } from "../config/constants";
import { validateAndParseOpenApiSpec } from "../services/openapi-service";
import { updatePlugin } from "../services/plugin-service";
import { getAuthentication } from "../services/signer-service";
import { PluginService } from "../services/plugin";
import { deployedUrl } from "../utils/deployed-url";
import { validateAndParseOpenApiSpec } from "../utils/openapi";
import { getSpecUrl, getHostname } from "../utils/url-utils";

export const updateCommand = new Command()
Expand Down Expand Up @@ -32,15 +30,16 @@ export const updateCommand = new Command()
console.error("Failed to parse account ID from OpenAPI specification.");
return;
}

const authentication = await getAuthentication(accountId);
const pluginService = new PluginService();
const authentication =
await pluginService.auth.getAuthentication(accountId);
if (!authentication) {
console.error("Authentication failed. Unable to update the plugin.");
return;
}
const bitteUrls = getBitteUrls();

try {
await updatePlugin(pluginId, accountId, bitteUrls.BASE_URL);
await pluginService.update(pluginId, accountId);
console.log(`Plugin ${pluginId} updated successfully.`);
} catch (error) {
console.error("Failed to update the plugin:", error);
Expand Down
170 changes: 170 additions & 0 deletions src/services/authentication.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import crypto from "crypto";
import dotenv from "dotenv";
import { createServer, IncomingMessage, ServerResponse } from "http";
import open from "open";

import type { BitteUrls } from "../config/constants";
import {
BITTE_KEY_ENV_KEY,
SIGN_MESSAGE,
SIGN_MESSAGE_PORT,
} from "../config/constants";
import { appendToEnv } from "../utils/file-utils";
import {
verifyMessage,
type KeySignMessageParams,
} from "../utils/verify-msg-utils";

dotenv.config();
dotenv.config({ path: ".env.local", override: true });

export class AuthenticationService {
private readonly bitteUrls: BitteUrls;

constructor(bitteUrls: BitteUrls) {
this.bitteUrls = bitteUrls;
}

async getAuthentication(accountId?: string): Promise<string | null> {
const bitteKeyString = process.env.BITTE_KEY;
if (!bitteKeyString) return null;

const parsedKey = JSON.parse(bitteKeyString) as KeySignMessageParams;
if (
(accountId &&
(await verifyMessage({
params: parsedKey,
accountIdToVerify: accountId,
}))) ||
!accountId
) {
return bitteKeyString;
}

return null;
}

async authenticateOrCreateKey(): Promise<string | null> {
const authentication = await this.getAuthentication();
if (authentication) {
console.log("Already authenticated.");
return authentication;
}

console.log(
"Not authenticated. Redirecting to Bitte wallet for signing...",
);
const newKey = await this.createAndStoreKey();
if (newKey) {
console.log("New key created and stored successfully.");
return JSON.stringify(newKey);
} else {
console.log("Failed to create and store new key.");
return null;
}
}

async getSignedMessage(): Promise<KeySignMessageParams> {
return new Promise((resolve, reject) => {
const server = createServer((req, res) =>
this.handleRequest(req, res, resolve, reject, server),
);

server.listen(SIGN_MESSAGE_PORT, () => {
const postEndpoint = `http://localhost:${SIGN_MESSAGE_PORT}`;
const nonce = crypto.randomBytes(16).toString("hex");
const signUrl = `${this.bitteUrls.SIGN_MESSAGE_URL}?message=${encodeURIComponent(
SIGN_MESSAGE,
)}&callbackUrl=${encodeURIComponent(
this.bitteUrls.SIGN_MESSAGE_SUCCESS_URL,
)}&nonce=${encodeURIComponent(nonce)}&postEndpoint=${encodeURIComponent(
postEndpoint,
)}`;
open(signUrl).catch((error) => {
console.error("Failed to open the browser:", error);
server.close();
reject(error);
});
});
});
}

private async createAndStoreKey(): Promise<KeySignMessageParams | null> {
try {
const signedMessage = await this.getSignedMessage();
if (!signedMessage) {
console.error("Failed to get signed message");
return null;
}

const isVerified = await verifyMessage({ params: signedMessage });
if (!isVerified) {
console.warn("Message verification failed");
}

await appendToEnv(BITTE_KEY_ENV_KEY, JSON.stringify(signedMessage));
return signedMessage;
} catch (error) {
console.error("Error creating and storing key:", error);
return null;
}
}

private handleRequest(
req: IncomingMessage,
res: ServerResponse,
resolve: (value: KeySignMessageParams) => void,
reject: (reason: Error) => void,
server: ReturnType<typeof createServer>,
): void {
this.setCORSHeaders(res);

if (req.method === "OPTIONS") {
this.handlePreflight(res);
return;
}

if (req.method === "POST") {
let body = "";
req.on("data", (chunk) => {
body += chunk.toString();
});
req.on("end", () => {
try {
const jsonBody = JSON.parse(body);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ message: "Signed message received" }));
resolve(jsonBody);
server.close(() => console.log("Temporary server closed"));
} catch (error) {
console.error("Error parsing JSON:", error);
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Invalid JSON" }));
reject(error as Error);
}
});
} else {
this.handleInvalidMethod(res, reject);
}
}

private setCORSHeaders(res: ServerResponse): void {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
}

private handlePreflight(res: ServerResponse): void {
res.writeHead(204);
res.end();
}

private handleInvalidMethod(
res: ServerResponse,
reject: (reason: Error) => void,
): void {
res.writeHead(405, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Method Not Allowed" }));
reject(new Error("Method Not Allowed"));
}
}
Loading

0 comments on commit 92ed2d8

Please sign in to comment.